From 4db94f1c6c54bccf949c81b0ae3bf4f8a455929e Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 18:11:17 +0900 Subject: [PATCH 1/9] fix(google,flutter): surface BillingResult debugMessage to callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenIapError.DeveloperError and PurchaseFailed were `object` singletons with hardcoded messages, so Google Play's raw `BillingResult.debugMessage` returned from launchBillingFlow / onPurchasesUpdated had nowhere to live — fromBillingResponseCode already took the string as a parameter but dropped it on the floor. That left callers (e.g. Flutter investigating a DEFERRED subscription replacement DEVELOPER_ERROR) with only the generic "Invalid arguments provided to the API" text and no way to see which argument Play actually rejected without adb. Changes: - Make DeveloperError and PurchaseFailed data classes that carry an optional `debugMessage: String?`. OpenIapError.toJSON() now emits a `debugMessage` field so downstream framework libraries receive it. - fromBillingResponseCode (both play and horizon flavors) constructs `DeveloperError(debugMessage)` so Play's raw message reaches the JSON payload. - The play and horizon launchBillingFlow sync-failure paths now map DEVELOPER_ERROR to `DeveloperError(result.debugMessage)` (matching the onPurchasesUpdated async path) and other non-OK codes to `PurchaseFailed(result.debugMessage)`. - Flutter's convertToPurchaseError forwards `result.responseCode` and `result.debugMessage` through to `PurchaseError`; previously only `code` and `message` were copied, so even though PurchaseError has these fields they were always null. A helpers_unit_test locks this wiring in. Call-site updates: OpenIapError.DeveloperError / .PurchaseFailed are no longer singleton values, so references like `throw OpenIapError.DeveloperError` become `throw OpenIapError.DeveloperError()`. Tests updated accordingly; companion `.CODE` / `.MESSAGE` accesses and `is OpenIapError.DeveloperError` type checks keep working as before. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../flutter_inapp_purchase/lib/helpers.dart | 2 + .../test/helpers_unit_test.dart | 29 +++ .../docs/src/pages/docs/updates/releases.tsx | 170 ++++++++++++++++++ .../dev/hyo/openiap/OpenIapErrorExtensions.kt | 2 +- .../java/dev/hyo/openiap/OpenIapModule.kt | 10 +- .../main/java/dev/hyo/openiap/OpenIapError.kt | 29 ++- .../dev/hyo/openiap/OpenIapErrorExtensions.kt | 2 +- .../java/dev/hyo/openiap/OpenIapModule.kt | 22 +-- .../java/dev/hyo/openiap/OpenIapErrorTest.kt | 28 ++- 9 files changed, 262 insertions(+), 32 deletions(-) diff --git a/libraries/flutter_inapp_purchase/lib/helpers.dart b/libraries/flutter_inapp_purchase/lib/helpers.dart index d2dac1b3..e21e977a 100644 --- a/libraries/flutter_inapp_purchase/lib/helpers.dart +++ b/libraries/flutter_inapp_purchase/lib/helpers.dart @@ -408,6 +408,8 @@ iap_err.PurchaseError convertToPurchaseError( return iap_err.PurchaseError( message: result.message ?? 'Unknown error', code: code, + responseCode: result.responseCode, + debugMessage: result.debugMessage, ); } diff --git a/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart b/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart index a32dd899..a18ccb19 100644 --- a/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart +++ b/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter_inapp_purchase/enums.dart'; +import 'package:flutter_inapp_purchase/errors.dart' as iap_err; import 'package:flutter_inapp_purchase/helpers.dart'; import 'package:flutter_inapp_purchase/types.dart' as types; import 'package:flutter_test/flutter_test.dart'; @@ -582,5 +583,33 @@ void main() { ); }, ); + + test( + 'convertToPurchaseError forwards debugMessage and responseCode ' + 'from PurchaseResult', + () { + final result = PurchaseResult.fromJSON({ + 'responseCode': 5, + 'debugMessage': + 'Deferred replacement requires the base offer, got a promo offer', + 'code': 'developer-error', + 'message': 'Invalid arguments provided to the API', + }); + + final error = convertToPurchaseError( + result, + platform: types.IapPlatform.Android, + ); + + expect(error, isA()); + expect(error.code, types.ErrorCode.DeveloperError); + expect(error.message, 'Invalid arguments provided to the API'); + expect( + error.debugMessage, + 'Deferred replacement requires the base offer, got a promo offer', + ); + expect(error.responseCode, 5); + }, + ); }); } diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 46a8a9a9..221a840d 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -23,6 +23,176 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ + // Subscription replacement + debug message diagnostics - Apr 15, 2026 + { + id: 'subscription-replacement-and-debug-message-2026-04-15', + date: new Date('2026-04-15'), + element: ( +
+ + Subscription Replacement Wiring & Billing Debug Messages - April + 15, 2026 + + +

+ Two connected Android fixes. First, the newer per-product + subscription replacement path now actually reaches the native + layer on flutter_inapp_purchase. Second, Google Play's raw{' '} + BillingResult.debugMessage is now forwarded through{' '} + PurchaseError, so callers can read the specific + reason Play rejected a replacement flow instead of just seeing a + generic "Invalid arguments". +

+ +
+
+ + openiap-google 1.3.32 + +
+
    +
  • + + Feat: DeveloperError and{' '} + PurchaseFailed now carry{' '} + debugMessage + {' '} + — both errors are now data classes instead of singletons and + accept an optional debugMessage: String?.{' '} + fromBillingResponseCode forwards Google + Play's raw BillingResult.debugMessage into + the error instance, and OpenIapError.toJSON(){' '} + emits a debugMessage key so downstream framework + libraries can surface the reason Play rejected a purchase + (offer token mismatch, subscription group conflict, etc.). + The launchBillingFlow sync-failure path now also + produces DeveloperError (matching the{' '} + onPurchasesUpdated async path) instead of a + generic PurchaseFailed for{' '} + DEVELOPER_ERROR response codes. +
  • +
+

+ + openiap-google + {' '} + ·{' '} + + openiap-google-horizon + +

+
+ +
+
+ + flutter_inapp_purchase 9.0.3 + +
+
    +
  • + + Fix: forward{' '} + subscriptionProductReplacementParams on Android + {' '} + — the field was declared on{' '} + RequestSubscriptionAndroidProps and parsed + correctly by the native plugin, but{' '} + flutter_inapp_purchase.dart was dropping it when + building the method-channel payload, so the native side + received null and Google Play applied its default + replacement mode (WITHOUT_PRORATION) regardless + of what callers passed from Dart. The Billing Library 8.1.0+ + per-product replacement path now works end-to-end. ( + #97) +
  • +
  • + Channel test added to assert that{' '} + oldProductId and replacementMode{' '} + reach the native requestPurchase call, so the + wiring can't silently regress again. +
  • +
+
+ +
+
+ + flutter_inapp_purchase 9.0.4 + +
+
    +
  • + + Fix: surface Google Play's{' '} + debugMessage through{' '} + PurchaseError + {' '} + — convertToPurchaseError was only forwarding{' '} + code and message from the native + error payload, so the raw{' '} + BillingResult.debugMessage and{' '} + responseCode were being dropped. Combined with + the openiap-google 1.3.32 change, Dart callers inspecting{' '} + PurchaseError.debugMessage now see Play's + exact rejection reason — useful for diagnosing{' '} + DEVELOPER_ERROR surfaces such as{' '} + DEFERRED replacement failures without having to + attach adb. +
  • +
  • + Picks up openiap-google 1.3.32 (debug message + data class + error types). +
  • +
+
+
+ ), + }, // Monorepo patch releases - Apr 14, 2026 { id: 'monorepo-2026-04-14', diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt index 3b47f52f..f5269aef 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt @@ -12,7 +12,7 @@ fun OpenIapError.Companion.fromBillingResponseCode(responseCode: Int, debugMessa BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> OpenIapError.ServiceUnavailable BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> OpenIapError.BillingUnavailable BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> OpenIapError.ItemUnavailable - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> OpenIapError.DeveloperError + BillingClient.BillingResponseCode.DEVELOPER_ERROR -> OpenIapError.DeveloperError(debugMessage) BillingClient.BillingResponseCode.ERROR -> OpenIapError.BillingError BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> OpenIapError.ItemAlreadyOwned BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> OpenIapError.ItemNotOwned diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index 70c3eccc..95957c26 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -385,7 +385,7 @@ class OpenIapModule( } if (!currentPurchaseCallback.compareAndSet(null, callback)) { OpenIapLog.w("requestPurchase rejected: another purchase is already in progress", TAG) - if (continuation.isActive) continuation.resumeWithException(OpenIapError.DeveloperError) + if (continuation.isActive) continuation.resumeWithException(OpenIapError.DeveloperError()) return@suspendCancellableCoroutine } continuation.invokeOnCancellation { currentPurchaseCallback.compareAndSet(callback, null) } @@ -512,10 +512,10 @@ class OpenIapModule( val err = when (result.responseCode) { BillingClient.BillingResponseCode.DEVELOPER_ERROR -> { OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) - OpenIapError.PurchaseFailed + OpenIapError.DeveloperError(result.debugMessage) } BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled - else -> OpenIapError.PurchaseFailed + else -> OpenIapError.PurchaseFailed(result.debugMessage) } purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) @@ -702,7 +702,7 @@ class OpenIapModule( override val verifyPurchase: MutationVerifyPurchaseHandler = { props -> // Use Horizon API if horizon options provided, otherwise fallback to Google Play if (props.horizon != null) { - val horizonAppId = appId ?: throw OpenIapError.DeveloperError + val horizonAppId = appId ?: throw OpenIapError.DeveloperError() val horizonResult = verifyPurchaseWithHorizon(props, horizonAppId, TAG) if (!horizonResult.success) { throw OpenIapError.InvalidPurchaseVerification @@ -717,7 +717,7 @@ class OpenIapModule( if (props.provider != PurchaseVerificationProvider.Iapkit) { throw OpenIapError.FeatureNotSupported } - val options = props.iapkit ?: throw OpenIapError.DeveloperError + val options = props.iapkit ?: throw OpenIapError.DeveloperError() VerifyPurchaseWithProviderResult( iapkit = verifyPurchaseWithIapkit(options, TAG), provider = props.provider diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt index 983ffb57..c42fcbd2 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt @@ -7,10 +7,19 @@ sealed class OpenIapError : Exception() { abstract val code: String abstract override val message: String + /** + * Optional raw debug message surfaced from the underlying billing layer + * (e.g. Google Play's `BillingResult.debugMessage`). Subclasses that + * carry per-call diagnostics should override this so callers can see + * the exact argument the store rejected. + */ + open val debugMessage: String? = null + fun toJSON(): Map = mapOf( "code" to toCode(this), "message" to (this.message ?: ""), "platform" to "android", + "debugMessage" to debugMessage, ) class ProductNotFound(val productId: String) : OpenIapError() { @@ -24,12 +33,14 @@ sealed class OpenIapError : Exception() { } } - object PurchaseFailed : OpenIapError() { - val CODE = ErrorCode.PurchaseError.rawValue - override val code = CODE - override val message = MESSAGE + class PurchaseFailed(override val debugMessage: String? = null) : OpenIapError() { + override val code: String = CODE + override val message: String = MESSAGE - const val MESSAGE = "Purchase failed" + companion object { + val CODE = ErrorCode.PurchaseError.rawValue + const val MESSAGE = "Purchase failed" + } } object PurchaseCancelled : OpenIapError() { @@ -232,12 +243,14 @@ sealed class OpenIapError : Exception() { const val MESSAGE = "Requested product is not available for purchase" } - object DeveloperError : OpenIapError() { - val CODE = ErrorCode.DeveloperError.rawValue + class DeveloperError(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "Invalid arguments provided to the API" + companion object { + val CODE = ErrorCode.DeveloperError.rawValue + const val MESSAGE = "Invalid arguments provided to the API" + } } object FeatureNotSupported : OpenIapError() { diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt index 90cd11bb..95ff1786 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt @@ -12,7 +12,7 @@ fun OpenIapError.Companion.fromBillingResponseCode(responseCode: Int, debugMessa BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> OpenIapError.ServiceUnavailable BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> OpenIapError.BillingUnavailable BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> OpenIapError.ItemUnavailable - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> OpenIapError.DeveloperError + BillingClient.BillingResponseCode.DEVELOPER_ERROR -> OpenIapError.DeveloperError(debugMessage) BillingClient.BillingResponseCode.ERROR -> OpenIapError.BillingError BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> OpenIapError.ItemAlreadyOwned BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> OpenIapError.ItemNotOwned diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt index fcf67f80..b78a7aa0 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt @@ -555,15 +555,15 @@ class OpenIapModule( externalTransactionToken = token )) } else if (continuation.isActive) { - continuation.resumeWithException(OpenIapError.PurchaseFailed) + continuation.resumeWithException(OpenIapError.PurchaseFailed()) } } catch (e: Exception) { OpenIapLog.e("Failed to extract token: ${e.message}", e, TAG) - if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed) + if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed()) } } else { OpenIapLog.e("Reporting details creation failed: ${result?.debugMessage}", tag = TAG) - if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed) + if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed()) } } null @@ -599,7 +599,7 @@ class OpenIapModule( throw OpenIapError.FeatureNotSupported } catch (e: Exception) { OpenIapLog.e("Failed to create billing program reporting details: ${e.message}", e, TAG) - throw OpenIapError.PurchaseFailed + throw OpenIapError.PurchaseFailed() } } } @@ -825,7 +825,7 @@ class OpenIapModule( // Return empty list - app should handle purchase via alternative billing return@withContext emptyList() } else { - val err = OpenIapError.PurchaseFailed + val err = OpenIapError.PurchaseFailed() for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() } @@ -865,7 +865,7 @@ class OpenIapModule( } if (!currentPurchaseCallback.compareAndSet(null, callback)) { OpenIapLog.w("requestPurchase rejected: another purchase is already in progress", TAG) - if (continuation.isActive) continuation.resumeWithException(OpenIapError.DeveloperError) + if (continuation.isActive) continuation.resumeWithException(OpenIapError.DeveloperError()) return@suspendCancellableCoroutine } continuation.invokeOnCancellation { currentPurchaseCallback.compareAndSet(callback, null) } @@ -1044,10 +1044,10 @@ class OpenIapModule( val err = when (result.responseCode) { BillingClient.BillingResponseCode.DEVELOPER_ERROR -> { OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) - OpenIapError.PurchaseFailed + OpenIapError.DeveloperError(result.debugMessage) } BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled - else -> OpenIapError.PurchaseFailed + else -> OpenIapError.PurchaseFailed(result.debugMessage) } for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) @@ -1113,7 +1113,7 @@ class OpenIapModule( if (!client.isReady) throw OpenIapError.NotPrepared val token = purchase.purchaseToken.orEmpty() if (token.isBlank()) { - throw OpenIapError.PurchaseFailed + throw OpenIapError.PurchaseFailed() } val result = if (isConsumable == true) { @@ -1133,7 +1133,7 @@ class OpenIapModule( } if (result.responseCode != BillingClient.BillingResponseCode.OK) { - throw OpenIapError.PurchaseFailed + throw OpenIapError.PurchaseFailed() } } } @@ -1203,7 +1203,7 @@ class OpenIapModule( if (props.provider != PurchaseVerificationProvider.Iapkit) { throw OpenIapError.FeatureNotSupported } - val options = props.iapkit ?: throw OpenIapError.DeveloperError + val options = props.iapkit ?: throw OpenIapError.DeveloperError() VerifyPurchaseWithProviderResult( iapkit = verifyPurchaseWithIapkit(options, TAG), provider = props.provider diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt index 43c0f830..7e3fbe51 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt @@ -16,9 +16,17 @@ class OpenIapErrorTest { @Test fun `PurchaseFailed has correct code and message`() { - val error = OpenIapError.PurchaseFailed + val error = OpenIapError.PurchaseFailed() assertEquals(ErrorCode.PurchaseError.rawValue, error.code) assertEquals("Purchase failed", error.message) + assertEquals(null, error.debugMessage) + } + + @Test + fun `PurchaseFailed carries debug message when provided`() { + val error = OpenIapError.PurchaseFailed("Billing client disconnected") + assertEquals("Billing client disconnected", error.debugMessage) + assertEquals("Billing client disconnected", error.toJSON()["debugMessage"]) } @Test @@ -179,9 +187,17 @@ class OpenIapErrorTest { @Test fun `DeveloperError has correct code and message`() { - val error = OpenIapError.DeveloperError + val error = OpenIapError.DeveloperError() assertEquals(ErrorCode.DeveloperError.rawValue, error.code) assertEquals("Invalid arguments provided to the API", error.message) + assertEquals(null, error.debugMessage) + } + + @Test + fun `DeveloperError carries debug message when provided`() { + val error = OpenIapError.DeveloperError("Offer token doesn't match product") + assertEquals("Offer token doesn't match product", error.debugMessage) + assertEquals("Offer token doesn't match product", error.toJSON()["debugMessage"]) } @Test @@ -210,7 +226,7 @@ class OpenIapErrorTest { fun `toJSON returns correct map for all error types`() { val errors = listOf( OpenIapError.ProductNotFound("test"), - OpenIapError.PurchaseFailed, + OpenIapError.PurchaseFailed(), OpenIapError.PurchaseCancelled, OpenIapError.PurchaseDeferred, OpenIapError.PaymentNotAllowed, @@ -233,7 +249,7 @@ class OpenIapErrorTest { OpenIapError.ServiceUnavailable, OpenIapError.BillingUnavailable, OpenIapError.ItemUnavailable, - OpenIapError.DeveloperError, + OpenIapError.DeveloperError(), OpenIapError.FeatureNotSupported, OpenIapError.ServiceDisconnected, OpenIapError.ServiceTimeout @@ -317,7 +333,7 @@ class OpenIapErrorTest { fun `toCode returns correct code for all error types`() { val errors = listOf( OpenIapError.ProductNotFound("test") to ErrorCode.SkuNotFound.rawValue, - OpenIapError.PurchaseFailed to ErrorCode.PurchaseError.rawValue, + OpenIapError.PurchaseFailed() to ErrorCode.PurchaseError.rawValue, OpenIapError.PurchaseCancelled to ErrorCode.UserCancelled.rawValue, OpenIapError.PurchaseDeferred to ErrorCode.DeferredPayment.rawValue, OpenIapError.PaymentNotAllowed to ErrorCode.UserError.rawValue, @@ -340,7 +356,7 @@ class OpenIapErrorTest { OpenIapError.ServiceUnavailable to ErrorCode.ServiceError.rawValue, OpenIapError.BillingUnavailable to ErrorCode.BillingUnavailable.rawValue, OpenIapError.ItemUnavailable to ErrorCode.ItemUnavailable.rawValue, - OpenIapError.DeveloperError to ErrorCode.DeveloperError.rawValue, + OpenIapError.DeveloperError() to ErrorCode.DeveloperError.rawValue, OpenIapError.FeatureNotSupported to ErrorCode.FeatureNotSupported.rawValue, OpenIapError.ServiceDisconnected to ErrorCode.ServiceDisconnected.rawValue, OpenIapError.ServiceTimeout to "service-timeout" From 913664bc69018c37045ed650bd526c937492c506 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 19:03:40 +0900 Subject: [PATCH 2/9] fix(review): address PR #98 feedback + extend debugMessage parity Addresses CodeRabbit/Gemini review on #98 plus cross-platform alignment so debugMessage isn't Android-only. Kotlin (openiap-google): - Add `data` keyword to PurchaseFailed / DeveloperError (style-guide). - Expand debug-capable error types: UserCancelled, ServiceUnavailable, BillingUnavailable, ItemUnavailable, BillingError, ItemAlreadyOwned, ItemNotOwned, ServiceDisconnected, FeatureNotSupported, ServiceTimeout, UnknownError all become data classes that accept `debugMessage: String? = null`. `fromBillingResponseCode` now forwards BillingResult.debugMessage into every returned error, not just DEVELOPER_ERROR. Call sites in both play and horizon flavors updated to `()` instantiation; tests assert the debugMessage flows through fromBillingResponseCode for all response codes. - reporting-details failure path (`createBillingProgramReportingDetails`) and `finishTransaction` now populate PurchaseFailed(debugMessage) instead of a bare instance so the underlying reason reaches callers. GQL schema + iOS parity: - Add optional `debugMessage` field to `type PurchaseError` in `packages/gql/src/error.graphql`. Regenerate Swift/Kotlin/Dart/GDScript/ TypeScript types (Apple and Google packages + all framework libraries). - Swift codegen plugin: emit `= nil` defaults for nullable struct properties so the synthesized memberwise initializer stays source-compatible when new optional fields are added (needed to keep existing `PurchaseError(code:, message:, productId:)` call sites compiling on the iOS side). - iOS `makePurchaseError` accepts an optional `debugMessage: String?`; populate it from `error.localizedDescription` at the two StoreKit catch sites (queryProduct and promoted-product retrieval) plus the enhanced promotional-offer failure path. - New Swift test `testPurchaseErrorCarriesDebugMessage` round-trips a PurchaseError through JSONEncoder/Decoder to lock the wire format. KMP: - Re-sync Types.kt from gql with the KMP-specific package declaration (`io.github.hyochan.kmpiap.openiap`). Apply the same enum-needs-semicolon regex the Google post-process uses. - Add `ErrorCode.DuplicatePurchase` branch to `ErrorMapping.kt` so the newly-synced enum compiles (the monorepo gql schema was ahead of what KMP had shipped). Flutter: - `convertToPurchaseError` now forwards `result.debugMessage`, `result.responseCode`, and the resolved `platform` onto PurchaseError; previously `PurchaseError.platform` was always null even though the helper already knew it. New helpers_unit_test case locks the full set of forwarded fields in. Docs: - Re-run Prettier on the 2026-04-15 release notes entry (CI had been red on `prettier --check`). Co-Authored-By: Claude Opus 4.6 (1M context) --- libraries/expo-iap/src/types.ts | 1 + .../flutter_inapp_purchase/lib/helpers.dart | 1 + .../flutter_inapp_purchase/lib/types.dart | 1127 +++++++++++++---- .../test/helpers_unit_test.dart | 1 + libraries/godot-iap/addons/godot-iap/types.gd | 4 + .../io/github/hyochan/kmpiap/openiap/Types.kt | 38 +- .../hyochan/kmpiap/utils/ErrorMapping.kt | 1 + libraries/react-native-iap/src/types.ts | 1 + packages/apple/Sources/Models/Types.swift | 313 ++--- packages/apple/Sources/OpenIapModule.swift | 24 +- packages/apple/Tests/OpenIapTests.swift | 16 + .../docs/src/pages/docs/updates/releases.tsx | 72 +- .../dev/hyo/openiap/OpenIapErrorExtensions.kt | 22 +- .../java/dev/hyo/openiap/OpenIapModule.kt | 4 +- .../main/java/dev/hyo/openiap/OpenIapError.kt | 102 +- .../src/main/java/dev/hyo/openiap/Types.kt | 3 + .../dev/hyo/openiap/store/OpenIapStore.kt | 2 +- .../dev/hyo/openiap/OpenIapErrorExtensions.kt | 22 +- .../java/dev/hyo/openiap/OpenIapModule.kt | 32 +- .../java/dev/hyo/openiap/OpenIapErrorTest.kt | 90 +- packages/gql/codegen/plugins/swift.ts | 6 + packages/gql/src/error.graphql | 4 + packages/gql/src/generated/Types.kt | 3 + packages/gql/src/generated/Types.swift | 313 ++--- packages/gql/src/generated/types.dart | 4 + packages/gql/src/generated/types.gd | 4 + packages/gql/src/generated/types.ts | 1 + 27 files changed, 1508 insertions(+), 703 deletions(-) diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index 7bb7aec7..dabfb9b6 100644 --- a/libraries/expo-iap/src/types.ts +++ b/libraries/expo-iap/src/types.ts @@ -1105,6 +1105,7 @@ export interface PurchaseCommon { export interface PurchaseError { code: ErrorCode; + debugMessage?: (string | null); message: string; productId?: (string | null); } diff --git a/libraries/flutter_inapp_purchase/lib/helpers.dart b/libraries/flutter_inapp_purchase/lib/helpers.dart index e21e977a..91e922dc 100644 --- a/libraries/flutter_inapp_purchase/lib/helpers.dart +++ b/libraries/flutter_inapp_purchase/lib/helpers.dart @@ -410,6 +410,7 @@ iap_err.PurchaseError convertToPurchaseError( code: code, responseCode: result.responseCode, debugMessage: result.debugMessage, + platform: platform, ); } diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index a9384c92..40f02bae 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -16,10 +16,12 @@ import 'dart:async'; enum AlternativeBillingModeAndroid { /// Standard Google Play billing (default) None('none'), + /// User choice billing - user can select between Google Play or alternative /// Requires Google Play Billing Library 7.0+ /// @deprecated Use BillingProgramAndroid.USER_CHOICE_BILLING instead UserChoice('user-choice'), + /// Alternative billing only - no Google Play billing option /// Requires Google Play Billing Library 6.2+ /// @deprecated Use BillingProgramAndroid.EXTERNAL_OFFER instead @@ -49,18 +51,22 @@ enum AlternativeBillingModeAndroid { enum BillingProgramAndroid { /// Unspecified billing program. Do not use. Unspecified('unspecified'), + /// User Choice Billing program. /// User can select between Google Play Billing or alternative billing. /// Available in Google Play Billing Library 7.0+ UserChoiceBilling('user-choice-billing'), + /// External Content Links program. /// Allows linking to external content outside the app. /// Available in Google Play Billing Library 8.2.0+ ExternalContentLink('external-content-link'), + /// External Offers program. /// Allows offering digital content purchases outside the app. /// Available in Google Play Billing Library 8.2.0+ ExternalOffer('external-offer'), + /// External Payments program (Japan only). /// Allows presenting a side-by-side choice between Google Play Billing and developer's external payment option. /// Users can choose to complete the purchase on the developer's website. @@ -96,9 +102,11 @@ enum BillingProgramAndroid { enum DeveloperBillingLaunchModeAndroid { /// Unspecified launch mode. Do not use. Unspecified('unspecified'), + /// Google Play will launch the link in an external browser or eligible app. /// Use this when you want Play to handle launching the external payment URL. LaunchInExternalBrowserOrApp('launch-in-external-browser-or-app'), + /// The caller app will launch the link after Play returns control. /// Use this when you want to handle launching the external payment URL yourself. CallerWillLaunchLink('caller-will-launch-link'); @@ -116,7 +124,8 @@ enum DeveloperBillingLaunchModeAndroid { case 'caller-will-launch-link': return DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink; } - throw ArgumentError('Unknown DeveloperBillingLaunchModeAndroid value: $value'); + throw ArgumentError( + 'Unknown DeveloperBillingLaunchModeAndroid value: $value'); } String toJson() => value; @@ -127,8 +136,10 @@ enum DeveloperBillingLaunchModeAndroid { enum DiscountOfferType { /// Introductory offer for new subscribers (first-time purchase discount) Introductory('introductory'), + /// Promotional offer for existing or returning subscribers Promotional('promotional'), + /// One-time product discount (Android only, Google Play Billing 7.0+) OneTime('one-time'); @@ -286,8 +297,10 @@ enum ErrorCode { enum ExternalLinkLaunchModeAndroid { /// Unspecified launch mode. Do not use. Unspecified('unspecified'), + /// Play will launch the URL in an external browser or eligible app LaunchInExternalBrowserOrApp('launch-in-external-browser-or-app'), + /// Play will not launch the URL. The app handles launching the URL after Play returns control. CallerWillLaunchLink('caller-will-launch-link'); @@ -316,8 +329,10 @@ enum ExternalLinkLaunchModeAndroid { enum ExternalLinkTypeAndroid { /// Unspecified link type. Do not use. Unspecified('unspecified'), + /// The link will direct users to a digital content offer LinkToDigitalContentOffer('link-to-digital-content-offer'), + /// The link will direct users to download an app LinkToAppDownload('link-to-app-download'); @@ -357,7 +372,8 @@ enum ExternalPurchaseCustomLinkNoticeTypeIOS { case 'browser': return ExternalPurchaseCustomLinkNoticeTypeIOS.Browser; } - throw ArgumentError('Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value'); + throw ArgumentError( + 'Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value'); } String toJson() => value; @@ -370,6 +386,7 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { /// Token for customer acquisition tracking. /// Use this when a new customer makes their first purchase through external link. Acquisition('acquisition'), + /// Token for ongoing services tracking. /// Use this for existing customers making additional purchases. Services('services'); @@ -385,7 +402,8 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { case 'services': return ExternalPurchaseCustomLinkTokenTypeIOS.Services; } - throw ArgumentError('Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value'); + throw ArgumentError( + 'Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value'); } String toJson() => value; @@ -395,6 +413,7 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { enum ExternalPurchaseNoticeAction { /// User chose to continue to external purchase Continue('continue'), + /// User dismissed the notice sheet Dismissed('dismissed'); @@ -420,6 +439,7 @@ enum IapEvent { PurchaseError('purchase-error'), PromotedProductIOS('promoted-product-ios'), UserChoiceBillingAndroid('user-choice-billing-android'), + /// Fired when user selects developer-provided billing option in external payments flow. /// Available on Android with Google Play Billing Library 8.3.0+ DeveloperProvidedBillingAndroid('developer-provided-billing-android'); @@ -451,20 +471,28 @@ enum IapEvent { enum IapkitPurchaseState { /// User is entitled to the product (purchase is complete and active). Entitled('entitled'), + /// Receipt is valid but still needs server acknowledgment. PendingAcknowledgment('pending-acknowledgment'), + /// Purchase is in progress or awaiting confirmation. Pending('pending'), + /// Purchase was cancelled or refunded. Canceled('canceled'), + /// Subscription or entitlement has expired. Expired('expired'), + /// Consumable purchase is ready to be fulfilled. ReadyToConsume('ready-to-consume'), + /// Consumable item has been fulfilled/consumed. Consumed('consumed'), + /// Purchase state could not be determined. Unknown('unknown'), + /// Purchase receipt is not authentic (fraudulent or tampered). Inauthentic('inauthentic'); @@ -552,10 +580,13 @@ enum IapStore { enum PaymentMode { /// Free trial period - no charge during offer FreeTrial('free-trial'), + /// Pay each period at reduced price PayAsYouGo('pay-as-you-go'), + /// Pay full discounted amount upfront PayUpFront('pay-up-front'), + /// Unknown or unspecified payment mode Unknown('unknown'); @@ -638,10 +669,13 @@ enum ProductQueryType { enum ProductStatusAndroid { /// Product was successfully fetched Ok('ok'), + /// Product not found - the SKU doesn't exist in the Play Console NotFound('not-found'), + /// No offers available for the user - product exists but user is not eligible for any offers NoOffersAvailable('no-offers-available'), + /// Unknown error occurred while fetching the product Unknown('unknown'); @@ -761,8 +795,11 @@ enum PurchaseVerificationProvider { enum SubResponseCodeAndroid { /// No specific sub-response code applies NoApplicableSubResponseCode('no-applicable-sub-response-code'), + /// User's payment method has insufficient funds - PaymentDeclinedDueToInsufficientFunds('payment-declined-due-to-insufficient-funds'), + PaymentDeclinedDueToInsufficientFunds( + 'payment-declined-due-to-insufficient-funds'), + /// User doesn't meet subscription offer eligibility requirements UserIneligible('user-ineligible'); @@ -788,6 +825,7 @@ enum SubResponseCodeAndroid { enum SubscriptionOfferTypeIOS { Introductory('introductory'), Promotional('promotional'), + /// Win-back offer type (iOS 18+) /// Used to re-engage churned subscribers with a discount or free trial. WinBack('win-back'); @@ -878,16 +916,22 @@ enum SubscriptionPeriodUnit { enum SubscriptionReplacementModeAndroid { /// Unknown replacement mode. Do not use. UnknownReplacementMode('unknown-replacement-mode'), + /// Replacement takes effect immediately, and the new expiration time will be prorated. WithTimeProration('with-time-proration'), + /// Replacement takes effect immediately, and the billing cycle remains the same. ChargeProratedPrice('charge-prorated-price'), + /// Replacement takes effect immediately, and the user is charged full price immediately. ChargeFullPrice('charge-full-price'), + /// Replacement takes effect when the old plan expires. WithoutProration('without-proration'), + /// Replacement takes effect when the old plan expires, and the user is not charged. Deferred('deferred'), + /// Keep the existing payment schedule unchanged for the item (8.1.0+) KeepExisting('keep-existing'); @@ -912,7 +956,8 @@ enum SubscriptionReplacementModeAndroid { case 'keep-existing': return SubscriptionReplacementModeAndroid.KeepExisting; } - throw ArgumentError('Unknown SubscriptionReplacementModeAndroid value: $value'); + throw ArgumentError( + 'Unknown SubscriptionReplacementModeAndroid value: $value'); } String toJson() => value; @@ -945,9 +990,11 @@ abstract class PurchaseCommon { IapPlatform get platform; String get productId; PurchaseState get purchaseState; + /// Unified purchase token (iOS JWS, Android purchaseToken) String? get purchaseToken; int get quantity; + /// Store where purchase was made IapStore get store; double get transactionDate; @@ -975,6 +1022,7 @@ class ActiveSubscription { final bool? autoRenewingAndroid; final String? basePlanIdAndroid; + /// The current plan identifier. This is: /// - On Android: the basePlanId (e.g., "premium", "premium-year") /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") @@ -986,13 +1034,16 @@ class ActiveSubscription { final bool isActive; final String productId; final String? purchaseToken; + /// Required for subscription upgrade/downgrade on Android final String? purchaseTokenAndroid; + /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, /// pending upgrades/downgrades, and auto-renewal preferences. final RenewalInfoIOS? renewalInfoIOS; final double transactionDate; final String transactionId; + /// @deprecated iOS only - use daysUntilExpirationIOS instead. /// Whether the subscription will expire soon (within 7 days). /// Consider using daysUntilExpirationIOS for more precise control. @@ -1003,14 +1054,18 @@ class ActiveSubscription { autoRenewingAndroid: json['autoRenewingAndroid'] as bool?, basePlanIdAndroid: json['basePlanIdAndroid'] as String?, currentPlanId: json['currentPlanId'] as String?, - daysUntilExpirationIOS: (json['daysUntilExpirationIOS'] as num?)?.toDouble(), + daysUntilExpirationIOS: + (json['daysUntilExpirationIOS'] as num?)?.toDouble(), environmentIOS: json['environmentIOS'] as String?, expirationDateIOS: (json['expirationDateIOS'] as num?)?.toDouble(), isActive: json['isActive'] as bool, productId: json['productId'] as String, purchaseToken: json['purchaseToken'] as String?, purchaseTokenAndroid: json['purchaseTokenAndroid'] as String?, - renewalInfoIOS: json['renewalInfoIOS'] != null ? RenewalInfoIOS.fromJson(json['renewalInfoIOS'] as Map) : null, + renewalInfoIOS: json['renewalInfoIOS'] != null + ? RenewalInfoIOS.fromJson( + json['renewalInfoIOS'] as Map) + : null, transactionDate: (json['transactionDate'] as num).toDouble(), transactionId: json['transactionId'] as String, willExpireSoon: json['willExpireSoon'] as bool?, @@ -1117,12 +1172,15 @@ class BillingProgramAvailabilityResultAndroid { /// The billing program that was checked final BillingProgramAndroid billingProgram; + /// Whether the billing program is available for the user final bool isAvailable; - factory BillingProgramAvailabilityResultAndroid.fromJson(Map json) { + factory BillingProgramAvailabilityResultAndroid.fromJson( + Map json) { return BillingProgramAvailabilityResultAndroid( - billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), + billingProgram: + BillingProgramAndroid.fromJson(json['billingProgram'] as String), isAvailable: json['isAvailable'] as bool, ); } @@ -1147,13 +1205,16 @@ class BillingProgramReportingDetailsAndroid { /// The billing program that the reporting details are associated with final BillingProgramAndroid billingProgram; + /// External transaction token used to report transactions made outside of Google Play Billing. /// This token must be used when reporting the external transaction to Google. final String externalTransactionToken; - factory BillingProgramReportingDetailsAndroid.fromJson(Map json) { + factory BillingProgramReportingDetailsAndroid.fromJson( + Map json) { return BillingProgramReportingDetailsAndroid( - billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), + billingProgram: + BillingProgramAndroid.fromJson(json['billingProgram'] as String), externalTransactionToken: json['externalTransactionToken'] as String, ); } @@ -1178,8 +1239,10 @@ class BillingResultAndroid { /// Debug message from the billing library final String? debugMessage; + /// The response code from the billing operation final int responseCode; + /// Sub-response code for more granular error information (8.0+). /// Provides additional context when responseCode indicates an error. final SubResponseCodeAndroid? subResponseCode; @@ -1188,7 +1251,9 @@ class BillingResultAndroid { return BillingResultAndroid( debugMessage: json['debugMessage'] as String?, responseCode: json['responseCode'] as int, - subResponseCode: json['subResponseCode'] != null ? SubResponseCodeAndroid.fromJson(json['subResponseCode'] as String) : null, + subResponseCode: json['subResponseCode'] != null + ? SubResponseCodeAndroid.fromJson(json['subResponseCode'] as String) + : null, ); } @@ -1215,7 +1280,8 @@ class DeveloperProvidedBillingDetailsAndroid { /// Must be reported within 24 hours of the transaction. final String externalTransactionToken; - factory DeveloperProvidedBillingDetailsAndroid.fromJson(Map json) { + factory DeveloperProvidedBillingDetailsAndroid.fromJson( + Map json) { return DeveloperProvidedBillingDetailsAndroid( externalTransactionToken: json['externalTransactionToken'] as String, ); @@ -1239,6 +1305,7 @@ class DiscountAmountAndroid { /// Discount amount in micro-units (1,000,000 = 1 unit of currency) final String discountAmountMicros; + /// Formatted discount amount with currency sign (e.g., "$4.99") final String formattedDiscountAmount; @@ -1269,13 +1336,17 @@ class DiscountDisplayInfoAndroid { /// Absolute discount amount details /// Only returned for fixed amount discounts final DiscountAmountAndroid? discountAmount; + /// Percentage discount (e.g., 33 for 33% off) /// Only returned for percentage-based discounts final int? percentageDiscount; factory DiscountDisplayInfoAndroid.fromJson(Map json) { return DiscountDisplayInfoAndroid( - discountAmount: json['discountAmount'] != null ? DiscountAmountAndroid.fromJson(json['discountAmount'] as Map) : null, + discountAmount: json['discountAmount'] != null + ? DiscountAmountAndroid.fromJson( + json['discountAmount'] as Map) + : null, percentageDiscount: json['percentageDiscount'] as int?, ); } @@ -1343,10 +1414,10 @@ class DiscountIOS { /// Standardized one-time product discount offer. /// Provides a unified interface for one-time purchase discounts across platforms. -/// +/// /// Currently supported on Android (Google Play Billing 7.0+). /// iOS does not support one-time purchase discounts in the same way. -/// +/// /// @see https://openiap.dev/docs/features/discount class DiscountOffer { const DiscountOffer({ @@ -1370,45 +1441,60 @@ class DiscountOffer { /// Currency code (ISO 4217, e.g., "USD") final String currency; + /// [Android] Fixed discount amount in micro-units. /// Only present for fixed amount discounts. final String? discountAmountMicrosAndroid; + /// Formatted display price string (e.g., "$4.99") final String displayPrice; + /// [Android] Formatted discount amount string (e.g., "$5.00 OFF"). final String? formattedDiscountAmountAndroid; + /// [Android] Original full price in micro-units before discount. /// Divide by 1,000,000 to get the actual price. /// Use for displaying strikethrough original price. final String? fullPriceMicrosAndroid; + /// Unique identifier for the offer. /// - iOS: Not applicable (one-time discounts not supported) /// - Android: offerId from ProductAndroidOneTimePurchaseOfferDetail final String? id; + /// [Android] Limited quantity information. /// Contains maximumQuantity and remainingQuantity. final LimitedQuantityInfoAndroid? limitedQuantityInfoAndroid; + /// [Android] List of tags associated with this offer. final List? offerTagsAndroid; + /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. final String? offerTokenAndroid; + /// [Android] Percentage discount (e.g., 33 for 33% off). /// Only present for percentage-based discounts. final int? percentageDiscountAndroid; + /// [Android] Pre-order details if this is a pre-order offer. /// Available in Google Play Billing Library 8.1.0+ final PreorderDetailsAndroid? preorderDetailsAndroid; + /// Numeric price value final double price; + /// [Android] Purchase option ID for this offer. /// Used to identify which purchase option the user selected. /// Available in Google Play Billing Library 7.0+ final String? purchaseOptionIdAndroid; + /// [Android] Rental details if this is a rental offer. final RentalDetailsAndroid? rentalDetailsAndroid; + /// Type of discount offer final DiscountOfferType type; + /// [Android] Valid time window for the offer. /// Contains startTimeMillis and endTimeMillis. final ValidTimeWindowAndroid? validTimeWindowAndroid; @@ -1416,21 +1502,39 @@ class DiscountOffer { factory DiscountOffer.fromJson(Map json) { return DiscountOffer( currency: json['currency'] as String, - discountAmountMicrosAndroid: json['discountAmountMicrosAndroid'] as String?, + discountAmountMicrosAndroid: + json['discountAmountMicrosAndroid'] as String?, displayPrice: json['displayPrice'] as String, - formattedDiscountAmountAndroid: json['formattedDiscountAmountAndroid'] as String?, + formattedDiscountAmountAndroid: + json['formattedDiscountAmountAndroid'] as String?, fullPriceMicrosAndroid: json['fullPriceMicrosAndroid'] as String?, id: json['id'] as String?, - limitedQuantityInfoAndroid: json['limitedQuantityInfoAndroid'] != null ? LimitedQuantityInfoAndroid.fromJson(json['limitedQuantityInfoAndroid'] as Map) : null, - offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null ? null : (json['offerTagsAndroid'] as List?)!.map((e) => e as String).toList(), + limitedQuantityInfoAndroid: json['limitedQuantityInfoAndroid'] != null + ? LimitedQuantityInfoAndroid.fromJson( + json['limitedQuantityInfoAndroid'] as Map) + : null, + offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null + ? null + : (json['offerTagsAndroid'] as List?)! + .map((e) => e as String) + .toList(), offerTokenAndroid: json['offerTokenAndroid'] as String?, percentageDiscountAndroid: json['percentageDiscountAndroid'] as int?, - preorderDetailsAndroid: json['preorderDetailsAndroid'] != null ? PreorderDetailsAndroid.fromJson(json['preorderDetailsAndroid'] as Map) : null, + preorderDetailsAndroid: json['preorderDetailsAndroid'] != null + ? PreorderDetailsAndroid.fromJson( + json['preorderDetailsAndroid'] as Map) + : null, price: (json['price'] as num).toDouble(), purchaseOptionIdAndroid: json['purchaseOptionIdAndroid'] as String?, - rentalDetailsAndroid: json['rentalDetailsAndroid'] != null ? RentalDetailsAndroid.fromJson(json['rentalDetailsAndroid'] as Map) : null, + rentalDetailsAndroid: json['rentalDetailsAndroid'] != null + ? RentalDetailsAndroid.fromJson( + json['rentalDetailsAndroid'] as Map) + : null, type: DiscountOfferType.fromJson(json['type'] as String), - validTimeWindowAndroid: json['validTimeWindowAndroid'] != null ? ValidTimeWindowAndroid.fromJson(json['validTimeWindowAndroid'] as Map) : null, + validTimeWindowAndroid: json['validTimeWindowAndroid'] != null + ? ValidTimeWindowAndroid.fromJson( + json['validTimeWindowAndroid'] as Map) + : null, ); } @@ -1471,12 +1575,16 @@ class DiscountOfferIOS { /// Discount identifier final String identifier; + /// Key identifier for validation final String keyIdentifier; + /// Cryptographic nonce final String nonce; + /// Signature for validation final String signature; + /// Timestamp of discount offer final double timestamp; @@ -1542,7 +1650,8 @@ class ExternalOfferAvailabilityResultAndroid { /// Whether external offers are available for the user final bool isAvailable; - factory ExternalOfferAvailabilityResultAndroid.fromJson(Map json) { + factory ExternalOfferAvailabilityResultAndroid.fromJson( + Map json) { return ExternalOfferAvailabilityResultAndroid( isAvailable: json['isAvailable'] as bool, ); @@ -1567,7 +1676,8 @@ class ExternalOfferReportingDetailsAndroid { /// External transaction token for reporting external offer transactions final String externalTransactionToken; - factory ExternalOfferReportingDetailsAndroid.fromJson(Map json) { + factory ExternalOfferReportingDetailsAndroid.fromJson( + Map json) { return ExternalOfferReportingDetailsAndroid( externalTransactionToken: json['externalTransactionToken'] as String, ); @@ -1590,10 +1700,12 @@ class ExternalPurchaseCustomLinkNoticeResultIOS { /// Whether the user chose to continue to external purchase final bool continued; + /// Optional error message if the presentation failed final String? error; - factory ExternalPurchaseCustomLinkNoticeResultIOS.fromJson(Map json) { + factory ExternalPurchaseCustomLinkNoticeResultIOS.fromJson( + Map json) { return ExternalPurchaseCustomLinkNoticeResultIOS( continued: json['continued'] as bool, error: json['error'] as String?, @@ -1618,11 +1730,13 @@ class ExternalPurchaseCustomLinkTokenResultIOS { /// Optional error message if token retrieval failed final String? error; + /// The external purchase token string. /// Report this token to Apple's External Purchase Server API. final String? token; - factory ExternalPurchaseCustomLinkTokenResultIOS.fromJson(Map json) { + factory ExternalPurchaseCustomLinkTokenResultIOS.fromJson( + Map json) { return ExternalPurchaseCustomLinkTokenResultIOS( error: json['error'] as String?, token: json['token'] as String?, @@ -1647,6 +1761,7 @@ class ExternalPurchaseLinkResultIOS { /// Optional error message if the presentation failed final String? error; + /// Whether the user completed the external purchase flow final bool success; @@ -1677,10 +1792,12 @@ class ExternalPurchaseNoticeResultIOS { /// Optional error message if the presentation failed final String? error; + /// External purchase token returned when user continues (iOS 17.4+). /// This token should be reported to Apple's External Purchase Server API. /// Only present when result is Continue. final String? externalPurchaseToken; + /// Notice result indicating user action final ExternalPurchaseNoticeAction result; @@ -1734,6 +1851,7 @@ class InstallmentPlanDetailsAndroid { /// For example, for a monthly subscription with commitmentPaymentsCount of 12, /// users will be charged monthly for 12 months after signup. final int commitmentPaymentsCount; + /// 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. @@ -1743,7 +1861,8 @@ class InstallmentPlanDetailsAndroid { factory InstallmentPlanDetailsAndroid.fromJson(Map json) { return InstallmentPlanDetailsAndroid( commitmentPaymentsCount: json['commitmentPaymentsCount'] as int, - subsequentCommitmentPaymentsCount: json['subsequentCommitmentPaymentsCount'] as int, + subsequentCommitmentPaymentsCount: + json['subsequentCommitmentPaymentsCount'] as int, ); } @@ -1766,6 +1885,7 @@ class LimitedQuantityInfoAndroid { /// Maximum quantity a user can purchase final int maximumQuantity; + /// Remaining quantity the user can still purchase final int remainingQuantity; @@ -1799,13 +1919,15 @@ class PendingPurchaseUpdateAndroid { /// Product IDs for the pending purchase update. /// These are the new products the user is switching to. final List products; + /// Purchase token for the pending transaction. /// Use this token to track or manage the pending purchase update. final String purchaseToken; factory PendingPurchaseUpdateAndroid.fromJson(Map json) { return PendingPurchaseUpdateAndroid( - products: (json['products'] as List).map((e) => e as String).toList(), + products: + (json['products'] as List).map((e) => e as String).toList(), purchaseToken: json['purchaseToken'] as String, ); } @@ -1830,13 +1952,15 @@ class PreorderDetailsAndroid { /// Pre-order presale end time in milliseconds since epoch. /// This is when the presale period ends and the product will be released. final String preorderPresaleEndTimeMillis; + /// Pre-order release time in milliseconds since epoch. /// This is when the product will be available to users who pre-ordered. final String preorderReleaseTimeMillis; factory PreorderDetailsAndroid.fromJson(Map json) { return PreorderDetailsAndroid( - preorderPresaleEndTimeMillis: json['preorderPresaleEndTimeMillis'] as String, + preorderPresaleEndTimeMillis: + json['preorderPresaleEndTimeMillis'] as String, preorderReleaseTimeMillis: json['preorderReleaseTimeMillis'] as String, ); } @@ -1900,7 +2024,9 @@ class PricingPhasesAndroid { factory PricingPhasesAndroid.fromJson(Map json) { return PricingPhasesAndroid( - pricingPhaseList: (json['pricingPhaseList'] as List).map((e) => PricingPhaseAndroid.fromJson(e as Map)).toList(), + pricingPhaseList: (json['pricingPhaseList'] as List) + .map((e) => PricingPhaseAndroid.fromJson(e as Map)) + .toList(), ); } @@ -1935,6 +2061,7 @@ class ProductAndroid extends Product implements ProductCommon { final String currency; final String? debugDescription; final String description; + /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer @@ -1943,20 +2070,26 @@ class ProductAndroid extends Product implements ProductCommon { final String displayPrice; final String id; final String nameAndroid; + /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - final List? oneTimePurchaseOfferDetailsAndroid; + final List? + oneTimePurchaseOfferDetailsAndroid; final IapPlatform platform; final double? price; + /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ final ProductStatusAndroid? productStatusAndroid; + /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - final List? subscriptionOfferDetailsAndroid; + final List? + subscriptionOfferDetailsAndroid; + /// Standardized subscription offers. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer @@ -1969,17 +2102,40 @@ class ProductAndroid extends Product implements ProductCommon { currency: json['currency'] as String, debugDescription: json['debugDescription'] as String?, description: json['description'] as String, - discountOffers: (json['discountOffers'] as List?) == null ? null : (json['discountOffers'] as List?)!.map((e) => DiscountOffer.fromJson(e as Map)).toList(), + discountOffers: (json['discountOffers'] as List?) == null + ? null + : (json['discountOffers'] as List?)! + .map((e) => DiscountOffer.fromJson(e as Map)) + .toList(), displayName: json['displayName'] as String?, displayPrice: json['displayPrice'] as String, id: json['id'] as String, nameAndroid: json['nameAndroid'] as String, - oneTimePurchaseOfferDetailsAndroid: (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null ? null : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)!.map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson(e as Map)).toList(), + oneTimePurchaseOfferDetailsAndroid: + (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null + ? null + : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)! + .map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson( + e as Map)) + .toList(), platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - productStatusAndroid: json['productStatusAndroid'] != null ? ProductStatusAndroid.fromJson(json['productStatusAndroid'] as String) : null, - subscriptionOfferDetailsAndroid: (json['subscriptionOfferDetailsAndroid'] as List?) == null ? null : (json['subscriptionOfferDetailsAndroid'] as List?)!.map((e) => ProductSubscriptionAndroidOfferDetails.fromJson(e as Map)).toList(), - subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(), + productStatusAndroid: json['productStatusAndroid'] != null + ? ProductStatusAndroid.fromJson( + json['productStatusAndroid'] as String) + : null, + subscriptionOfferDetailsAndroid: + (json['subscriptionOfferDetailsAndroid'] as List?) == null + ? null + : (json['subscriptionOfferDetailsAndroid'] as List?)! + .map((e) => ProductSubscriptionAndroidOfferDetails.fromJson( + e as Map)) + .toList(), + subscriptionOffers: (json['subscriptionOffers'] as List?) == null + ? null + : (json['subscriptionOffers'] as List?)! + .map((e) => SubscriptionOffer.fromJson(e as Map)) + .toList(), title: json['title'] as String, type: ProductType.fromJson(json['type'] as String), ); @@ -1992,17 +2148,28 @@ class ProductAndroid extends Product implements ProductCommon { 'currency': currency, 'debugDescription': debugDescription, 'description': description, - 'discountOffers': discountOffers == null ? null : discountOffers!.map((e) => e.toJson()).toList(), + 'discountOffers': discountOffers == null + ? null + : discountOffers!.map((e) => e.toJson()).toList(), 'displayName': displayName, 'displayPrice': displayPrice, 'id': id, 'nameAndroid': nameAndroid, - 'oneTimePurchaseOfferDetailsAndroid': oneTimePurchaseOfferDetailsAndroid == null ? null : oneTimePurchaseOfferDetailsAndroid!.map((e) => e.toJson()).toList(), + 'oneTimePurchaseOfferDetailsAndroid': + oneTimePurchaseOfferDetailsAndroid == null + ? null + : oneTimePurchaseOfferDetailsAndroid! + .map((e) => e.toJson()) + .toList(), 'platform': platform.toJson(), 'price': price, 'productStatusAndroid': productStatusAndroid?.toJson(), - 'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid == null ? null : subscriptionOfferDetailsAndroid!.map((e) => e.toJson()).toList(), - 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid == null + ? null + : subscriptionOfferDetailsAndroid!.map((e) => e.toJson()).toList(), + 'subscriptionOffers': subscriptionOffers == null + ? null + : subscriptionOffers!.map((e) => e.toJson()).toList(), 'title': title, 'type': type.toJson(), }; @@ -2034,46 +2201,72 @@ class ProductAndroidOneTimePurchaseOfferDetail { /// Only available for discounted offers final DiscountDisplayInfoAndroid? discountDisplayInfo; final String formattedPrice; + /// Full (non-discounted) price in micro-units /// Only available for discounted offers final String? fullPriceMicros; + /// Limited quantity information final LimitedQuantityInfoAndroid? limitedQuantityInfo; + /// Offer ID final String? offerId; + /// List of offer tags final List offerTags; + /// Offer token for use in BillingFlowParams when purchasing final String offerToken; + /// Pre-order details for products available for pre-order /// Available in Google Play Billing Library 8.1.0+ final PreorderDetailsAndroid? preorderDetailsAndroid; final String priceAmountMicros; final String priceCurrencyCode; + /// Purchase option ID for this offer (Android) /// Used to identify which purchase option the user selected. /// Available in Google Play Billing Library 7.0+ final String? purchaseOptionId; + /// Rental details for rental offers final RentalDetailsAndroid? rentalDetailsAndroid; + /// Valid time window for the offer final ValidTimeWindowAndroid? validTimeWindow; - factory ProductAndroidOneTimePurchaseOfferDetail.fromJson(Map json) { + factory ProductAndroidOneTimePurchaseOfferDetail.fromJson( + Map json) { return ProductAndroidOneTimePurchaseOfferDetail( - discountDisplayInfo: json['discountDisplayInfo'] != null ? DiscountDisplayInfoAndroid.fromJson(json['discountDisplayInfo'] as Map) : null, + discountDisplayInfo: json['discountDisplayInfo'] != null + ? DiscountDisplayInfoAndroid.fromJson( + json['discountDisplayInfo'] as Map) + : null, formattedPrice: json['formattedPrice'] as String, fullPriceMicros: json['fullPriceMicros'] as String?, - limitedQuantityInfo: json['limitedQuantityInfo'] != null ? LimitedQuantityInfoAndroid.fromJson(json['limitedQuantityInfo'] as Map) : null, + limitedQuantityInfo: json['limitedQuantityInfo'] != null + ? LimitedQuantityInfoAndroid.fromJson( + json['limitedQuantityInfo'] as Map) + : null, offerId: json['offerId'] as String?, - offerTags: (json['offerTags'] as List).map((e) => e as String).toList(), + offerTags: + (json['offerTags'] as List).map((e) => e as String).toList(), offerToken: json['offerToken'] as String, - preorderDetailsAndroid: json['preorderDetailsAndroid'] != null ? PreorderDetailsAndroid.fromJson(json['preorderDetailsAndroid'] as Map) : null, + preorderDetailsAndroid: json['preorderDetailsAndroid'] != null + ? PreorderDetailsAndroid.fromJson( + json['preorderDetailsAndroid'] as Map) + : null, priceAmountMicros: json['priceAmountMicros'] as String, priceCurrencyCode: json['priceCurrencyCode'] as String, purchaseOptionId: json['purchaseOptionId'] as String?, - rentalDetailsAndroid: json['rentalDetailsAndroid'] != null ? RentalDetailsAndroid.fromJson(json['rentalDetailsAndroid'] as Map) : null, - validTimeWindow: json['validTimeWindow'] != null ? ValidTimeWindowAndroid.fromJson(json['validTimeWindow'] as Map) : null, + rentalDetailsAndroid: json['rentalDetailsAndroid'] != null + ? RentalDetailsAndroid.fromJson( + json['rentalDetailsAndroid'] as Map) + : null, + validTimeWindow: json['validTimeWindow'] != null + ? ValidTimeWindowAndroid.fromJson( + json['validTimeWindow'] as Map) + : null, ); } @@ -2128,8 +2321,10 @@ class ProductIOS extends Product implements ProductCommon { final String jsonRepresentationIOS; final IapPlatform platform; final double? price; + /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. final SubscriptionInfoIOS? subscriptionInfoIOS; + /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// Note: iOS does not support one-time product discounts. @@ -2152,8 +2347,15 @@ class ProductIOS extends Product implements ProductCommon { jsonRepresentationIOS: json['jsonRepresentationIOS'] as String, platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - 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(), + 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, type: ProductType.fromJson(json['type'] as String), typeIOS: ProductTypeIOS.fromJson(json['typeIOS'] as String), @@ -2176,7 +2378,9 @@ class ProductIOS extends Product implements ProductCommon { 'platform': platform.toJson(), 'price': price, 'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(), - 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionOffers': subscriptionOffers == null + ? null + : subscriptionOffers!.map((e) => e.toJson()).toList(), 'title': title, 'type': type.toJson(), 'typeIOS': typeIOS.toJson(), @@ -2184,7 +2388,8 @@ class ProductIOS extends Product implements ProductCommon { } } -class ProductSubscriptionAndroid extends ProductSubscription implements ProductCommon { +class ProductSubscriptionAndroid extends ProductSubscription + implements ProductCommon { const ProductSubscriptionAndroid({ required this.currency, this.debugDescription, @@ -2207,6 +2412,7 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC final String currency; final String? debugDescription; final String description; + /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer @@ -2215,20 +2421,26 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC final String displayPrice; final String id; final String nameAndroid; + /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - final List? oneTimePurchaseOfferDetailsAndroid; + final List? + oneTimePurchaseOfferDetailsAndroid; final IapPlatform platform; final double? price; + /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ final ProductStatusAndroid? productStatusAndroid; + /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - final List subscriptionOfferDetailsAndroid; + final List + subscriptionOfferDetailsAndroid; + /// Standardized subscription offers. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer @@ -2241,17 +2453,36 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC currency: json['currency'] as String, debugDescription: json['debugDescription'] as String?, description: json['description'] as String, - discountOffers: (json['discountOffers'] as List?) == null ? null : (json['discountOffers'] as List?)!.map((e) => DiscountOffer.fromJson(e as Map)).toList(), + discountOffers: (json['discountOffers'] as List?) == null + ? null + : (json['discountOffers'] as List?)! + .map((e) => DiscountOffer.fromJson(e as Map)) + .toList(), displayName: json['displayName'] as String?, displayPrice: json['displayPrice'] as String, id: json['id'] as String, nameAndroid: json['nameAndroid'] as String, - oneTimePurchaseOfferDetailsAndroid: (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null ? null : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)!.map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson(e as Map)).toList(), + oneTimePurchaseOfferDetailsAndroid: + (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null + ? null + : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)! + .map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson( + e as Map)) + .toList(), platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - productStatusAndroid: json['productStatusAndroid'] != null ? ProductStatusAndroid.fromJson(json['productStatusAndroid'] as String) : null, - subscriptionOfferDetailsAndroid: (json['subscriptionOfferDetailsAndroid'] as List).map((e) => ProductSubscriptionAndroidOfferDetails.fromJson(e as Map)).toList(), - subscriptionOffers: (json['subscriptionOffers'] as List).map((e) => SubscriptionOffer.fromJson(e as Map)).toList(), + productStatusAndroid: json['productStatusAndroid'] != null + ? ProductStatusAndroid.fromJson( + json['productStatusAndroid'] as String) + : null, + subscriptionOfferDetailsAndroid: + (json['subscriptionOfferDetailsAndroid'] as List) + .map((e) => ProductSubscriptionAndroidOfferDetails.fromJson( + e as Map)) + .toList(), + subscriptionOffers: (json['subscriptionOffers'] as List) + .map((e) => SubscriptionOffer.fromJson(e as Map)) + .toList(), title: json['title'] as String, type: ProductType.fromJson(json['type'] as String), ); @@ -2264,16 +2495,24 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC 'currency': currency, 'debugDescription': debugDescription, 'description': description, - 'discountOffers': discountOffers == null ? null : discountOffers!.map((e) => e.toJson()).toList(), + 'discountOffers': discountOffers == null + ? null + : discountOffers!.map((e) => e.toJson()).toList(), 'displayName': displayName, 'displayPrice': displayPrice, 'id': id, 'nameAndroid': nameAndroid, - 'oneTimePurchaseOfferDetailsAndroid': oneTimePurchaseOfferDetailsAndroid == null ? null : oneTimePurchaseOfferDetailsAndroid!.map((e) => e.toJson()).toList(), + 'oneTimePurchaseOfferDetailsAndroid': + oneTimePurchaseOfferDetailsAndroid == null + ? null + : oneTimePurchaseOfferDetailsAndroid! + .map((e) => e.toJson()) + .toList(), 'platform': platform.toJson(), 'price': price, 'productStatusAndroid': productStatusAndroid?.toJson(), - 'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid.map((e) => e.toJson()).toList(), + 'subscriptionOfferDetailsAndroid': + subscriptionOfferDetailsAndroid.map((e) => e.toJson()).toList(), 'subscriptionOffers': subscriptionOffers.map((e) => e.toJson()).toList(), 'title': title, 'type': type.toJson(), @@ -2295,6 +2534,7 @@ class ProductSubscriptionAndroidOfferDetails { }); final String basePlanId; + /// 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+ @@ -2304,14 +2544,20 @@ class ProductSubscriptionAndroidOfferDetails { final String offerToken; final PricingPhasesAndroid pricingPhases; - factory ProductSubscriptionAndroidOfferDetails.fromJson(Map json) { + factory ProductSubscriptionAndroidOfferDetails.fromJson( + Map json) { return ProductSubscriptionAndroidOfferDetails( basePlanId: json['basePlanId'] as String, - installmentPlanDetails: json['installmentPlanDetails'] != null ? InstallmentPlanDetailsAndroid.fromJson(json['installmentPlanDetails'] as Map) : null, + installmentPlanDetails: json['installmentPlanDetails'] != null + ? InstallmentPlanDetailsAndroid.fromJson( + json['installmentPlanDetails'] as Map) + : null, offerId: json['offerId'] as String?, - offerTags: (json['offerTags'] as List).map((e) => e as String).toList(), + offerTags: + (json['offerTags'] as List).map((e) => e as String).toList(), offerToken: json['offerToken'] as String, - pricingPhases: PricingPhasesAndroid.fromJson(json['pricingPhases'] as Map), + pricingPhases: PricingPhasesAndroid.fromJson( + json['pricingPhases'] as Map), ); } @@ -2328,7 +2574,8 @@ class ProductSubscriptionAndroidOfferDetails { } } -class ProductSubscriptionIOS extends ProductSubscription implements ProductCommon { +class ProductSubscriptionIOS extends ProductSubscription + implements ProductCommon { const ProductSubscriptionIOS({ required this.currency, this.debugDescription, @@ -2359,6 +2606,7 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo final String currency; final String? debugDescription; final String description; + /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. final List? discountsIOS; final String? displayName; @@ -2374,8 +2622,10 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo final String jsonRepresentationIOS; final IapPlatform platform; final double? price; + /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. final SubscriptionInfoIOS? subscriptionInfoIOS; + /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer @@ -2391,24 +2641,46 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo currency: json['currency'] as String, debugDescription: json['debugDescription'] as String?, description: json['description'] as String, - discountsIOS: (json['discountsIOS'] as List?) == null ? null : (json['discountsIOS'] as List?)!.map((e) => DiscountIOS.fromJson(e as Map)).toList(), + discountsIOS: (json['discountsIOS'] as List?) == null + ? null + : (json['discountsIOS'] as List?)! + .map((e) => DiscountIOS.fromJson(e as Map)) + .toList(), displayName: json['displayName'] as String?, displayNameIOS: json['displayNameIOS'] as String, displayPrice: json['displayPrice'] as String, id: json['id'] as String, - introductoryPriceAsAmountIOS: json['introductoryPriceAsAmountIOS'] as String?, + introductoryPriceAsAmountIOS: + json['introductoryPriceAsAmountIOS'] as String?, introductoryPriceIOS: json['introductoryPriceIOS'] as String?, - introductoryPriceNumberOfPeriodsIOS: json['introductoryPriceNumberOfPeriodsIOS'] as String?, - introductoryPricePaymentModeIOS: PaymentModeIOS.fromJson(json['introductoryPricePaymentModeIOS'] as String), - introductoryPriceSubscriptionPeriodIOS: json['introductoryPriceSubscriptionPeriodIOS'] != null ? SubscriptionPeriodIOS.fromJson(json['introductoryPriceSubscriptionPeriodIOS'] as String) : null, + introductoryPriceNumberOfPeriodsIOS: + json['introductoryPriceNumberOfPeriodsIOS'] as String?, + introductoryPricePaymentModeIOS: PaymentModeIOS.fromJson( + json['introductoryPricePaymentModeIOS'] as String), + introductoryPriceSubscriptionPeriodIOS: + json['introductoryPriceSubscriptionPeriodIOS'] != null + ? SubscriptionPeriodIOS.fromJson( + json['introductoryPriceSubscriptionPeriodIOS'] as String) + : null, isFamilyShareableIOS: json['isFamilyShareableIOS'] as bool, jsonRepresentationIOS: json['jsonRepresentationIOS'] as String, platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - 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(), - subscriptionPeriodNumberIOS: json['subscriptionPeriodNumberIOS'] as String?, - subscriptionPeriodUnitIOS: json['subscriptionPeriodUnitIOS'] != null ? SubscriptionPeriodIOS.fromJson(json['subscriptionPeriodUnitIOS'] as String) : null, + 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(), + subscriptionPeriodNumberIOS: + json['subscriptionPeriodNumberIOS'] as String?, + subscriptionPeriodUnitIOS: json['subscriptionPeriodUnitIOS'] != null + ? SubscriptionPeriodIOS.fromJson( + json['subscriptionPeriodUnitIOS'] as String) + : null, title: json['title'] as String, type: ProductType.fromJson(json['type'] as String), typeIOS: ProductTypeIOS.fromJson(json['typeIOS'] as String), @@ -2422,22 +2694,29 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo 'currency': currency, 'debugDescription': debugDescription, 'description': description, - 'discountsIOS': discountsIOS == null ? null : discountsIOS!.map((e) => e.toJson()).toList(), + 'discountsIOS': discountsIOS == null + ? null + : discountsIOS!.map((e) => e.toJson()).toList(), 'displayName': displayName, 'displayNameIOS': displayNameIOS, 'displayPrice': displayPrice, 'id': id, 'introductoryPriceAsAmountIOS': introductoryPriceAsAmountIOS, 'introductoryPriceIOS': introductoryPriceIOS, - 'introductoryPriceNumberOfPeriodsIOS': introductoryPriceNumberOfPeriodsIOS, - 'introductoryPricePaymentModeIOS': introductoryPricePaymentModeIOS.toJson(), - 'introductoryPriceSubscriptionPeriodIOS': introductoryPriceSubscriptionPeriodIOS?.toJson(), + 'introductoryPriceNumberOfPeriodsIOS': + introductoryPriceNumberOfPeriodsIOS, + 'introductoryPricePaymentModeIOS': + introductoryPricePaymentModeIOS.toJson(), + 'introductoryPriceSubscriptionPeriodIOS': + introductoryPriceSubscriptionPeriodIOS?.toJson(), 'isFamilyShareableIOS': isFamilyShareableIOS, 'jsonRepresentationIOS': jsonRepresentationIOS, 'platform': platform.toJson(), 'price': price, 'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(), - 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionOffers': subscriptionOffers == null + ? null + : subscriptionOffers!.map((e) => e.toJson()).toList(), 'subscriptionPeriodNumberIOS': subscriptionPeriodNumberIOS, 'subscriptionPeriodUnitIOS': subscriptionPeriodUnitIOS?.toJson(), 'title': title, @@ -2482,6 +2761,7 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { final List? ids; final bool? isAcknowledgedAndroid; final bool isAutoRenewing; + /// Whether the subscription is suspended (Android) /// A suspended subscription means the user's payment method failed and they need to fix it. /// Users should be directed to the subscription center to resolve the issue. @@ -2491,6 +2771,7 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { final String? obfuscatedAccountIdAndroid; final String? obfuscatedProfileIdAndroid; final String? packageNameAndroid; + /// 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. @@ -2502,6 +2783,7 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { final String? purchaseToken; final int quantity; final String? signatureAndroid; + /// Store where purchase was made final IapStore store; final double transactionDate; @@ -2515,14 +2797,19 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { dataAndroid: json['dataAndroid'] as String?, developerPayloadAndroid: json['developerPayloadAndroid'] as String?, id: json['id'] as String, - ids: (json['ids'] as List?) == null ? null : (json['ids'] as List?)!.map((e) => e as String).toList(), + ids: (json['ids'] as List?) == null + ? null + : (json['ids'] as List?)!.map((e) => e as String).toList(), isAcknowledgedAndroid: json['isAcknowledgedAndroid'] as bool?, isAutoRenewing: json['isAutoRenewing'] as bool, isSuspendedAndroid: json['isSuspendedAndroid'] as bool?, obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, packageNameAndroid: json['packageNameAndroid'] as String?, - pendingPurchaseUpdateAndroid: json['pendingPurchaseUpdateAndroid'] != null ? PendingPurchaseUpdateAndroid.fromJson(json['pendingPurchaseUpdateAndroid'] as Map) : null, + pendingPurchaseUpdateAndroid: json['pendingPurchaseUpdateAndroid'] != null + ? PendingPurchaseUpdateAndroid.fromJson( + json['pendingPurchaseUpdateAndroid'] as Map) + : null, platform: IapPlatform.fromJson(json['platform'] as String), productId: json['productId'] as String, purchaseState: PurchaseState.fromJson(json['purchaseState'] as String), @@ -2570,17 +2857,20 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { class PurchaseError { const PurchaseError({ required this.code, + this.debugMessage, required this.message, this.productId, }); final ErrorCode code; + final String? debugMessage; final String message; final String? productId; factory PurchaseError.fromJson(Map json) { return PurchaseError( code: ErrorCode.fromJson(json['code'] as String), + debugMessage: json['debugMessage'] as String?, message: json['message'] as String, productId: json['productId'] as String?, ); @@ -2590,6 +2880,7 @@ class PurchaseError { return { '__typename': 'PurchaseError', 'code': code.toJson(), + 'debugMessage': debugMessage, 'message': message, 'productId': productId, }; @@ -2662,6 +2953,7 @@ class PurchaseIOS extends Purchase implements PurchaseCommon { final RenewalInfoIOS? renewalInfoIOS; final double? revocationDateIOS; final String? revocationReasonIOS; + /// Store where purchase was made final IapStore store; final String? storefrontCountryCodeIOS; @@ -2683,12 +2975,18 @@ class PurchaseIOS extends Purchase implements PurchaseCommon { environmentIOS: json['environmentIOS'] as String?, expirationDateIOS: (json['expirationDateIOS'] as num?)?.toDouble(), id: json['id'] as String, - ids: (json['ids'] as List?) == null ? null : (json['ids'] as List?)!.map((e) => e as String).toList(), + ids: (json['ids'] as List?) == null + ? null + : (json['ids'] as List?)!.map((e) => e as String).toList(), isAutoRenewing: json['isAutoRenewing'] as bool, isUpgradedIOS: json['isUpgradedIOS'] as bool?, - offerIOS: json['offerIOS'] != null ? PurchaseOfferIOS.fromJson(json['offerIOS'] as Map) : null, - originalTransactionDateIOS: (json['originalTransactionDateIOS'] as num?)?.toDouble(), - originalTransactionIdentifierIOS: json['originalTransactionIdentifierIOS'] as String?, + offerIOS: json['offerIOS'] != null + ? PurchaseOfferIOS.fromJson(json['offerIOS'] as Map) + : null, + originalTransactionDateIOS: + (json['originalTransactionDateIOS'] as num?)?.toDouble(), + originalTransactionIdentifierIOS: + json['originalTransactionIdentifierIOS'] as String?, ownershipTypeIOS: json['ownershipTypeIOS'] as String?, platform: IapPlatform.fromJson(json['platform'] as String), productId: json['productId'] as String, @@ -2697,8 +2995,12 @@ class PurchaseIOS extends Purchase implements PurchaseCommon { quantity: json['quantity'] as int, quantityIOS: json['quantityIOS'] as int?, reasonIOS: json['reasonIOS'] as String?, - reasonStringRepresentationIOS: json['reasonStringRepresentationIOS'] as String?, - renewalInfoIOS: json['renewalInfoIOS'] != null ? RenewalInfoIOS.fromJson(json['renewalInfoIOS'] as Map) : null, + reasonStringRepresentationIOS: + json['reasonStringRepresentationIOS'] as String?, + renewalInfoIOS: json['renewalInfoIOS'] != null + ? RenewalInfoIOS.fromJson( + json['renewalInfoIOS'] as Map) + : null, revocationDateIOS: (json['revocationDateIOS'] as num?)?.toDouble(), revocationReasonIOS: json['revocationReasonIOS'] as String?, store: IapStore.fromJson(json['store'] as String), @@ -2827,27 +3129,35 @@ class RenewalInfoIOS { }); final String? autoRenewPreference; + /// 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; + /// Grace period expiration date (milliseconds since epoch) /// When set, subscription is in grace period (billing issue but still has access) final double? gracePeriodExpirationDate; + /// True if subscription failed to renew due to billing issue and is retrying /// Note: Not directly available in RenewalInfo, available in Status final bool? isInBillingRetry; final String? jsonRepresentation; + /// Product ID that will be used on next renewal (when user upgrades/downgrades) /// If set and different from current productId, subscription will change on expiration final String? pendingUpgradeProductId; + /// User's response to subscription price increase /// Possible values: "AGREED", "PENDING", null (no price increase) final String? priceIncreaseStatus; + /// Expected renewal date (milliseconds since epoch) /// For active subscriptions, when the next renewal/charge will occur final double? renewalDate; + /// Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) final String? renewalOfferId; + /// Type of offer applied to next renewal /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. final String? renewalOfferType; @@ -2857,7 +3167,8 @@ class RenewalInfoIOS { return RenewalInfoIOS( autoRenewPreference: json['autoRenewPreference'] as String?, expirationReason: json['expirationReason'] as String?, - gracePeriodExpirationDate: (json['gracePeriodExpirationDate'] as num?)?.toDouble(), + gracePeriodExpirationDate: + (json['gracePeriodExpirationDate'] as num?)?.toDouble(), isInBillingRetry: json['isInBillingRetry'] as bool?, jsonRepresentation: json['jsonRepresentation'] as String?, pendingUpgradeProductId: json['pendingUpgradeProductId'] as String?, @@ -2898,6 +3209,7 @@ class RentalDetailsAndroid { /// Rental expiration period in ISO 8601 format /// Time after rental period ends when user can still extend final String? rentalExpirationPeriod; + /// Rental period in ISO 8601 format (e.g., P7D for 7 days) final String rentalPeriod; @@ -2940,11 +3252,13 @@ class RequestVerifyPurchaseWithIapkitResult { /// Whether the purchase is valid (not falsified). final bool isValid; + /// The current state of the purchase. final IapkitPurchaseState state; final IapStore store; - factory RequestVerifyPurchaseWithIapkitResult.fromJson(Map json) { + factory RequestVerifyPurchaseWithIapkitResult.fromJson( + Map json) { return RequestVerifyPurchaseWithIapkitResult( isValid: json['isValid'] as bool, state: IapkitPurchaseState.fromJson(json['state'] as String), @@ -2977,10 +3291,19 @@ class SubscriptionInfoIOS { factory SubscriptionInfoIOS.fromJson(Map json) { return SubscriptionInfoIOS( - introductoryOffer: json['introductoryOffer'] != null ? SubscriptionOfferIOS.fromJson(json['introductoryOffer'] as Map) : null, - promotionalOffers: (json['promotionalOffers'] as List?) == null ? null : (json['promotionalOffers'] as List?)!.map((e) => SubscriptionOfferIOS.fromJson(e as Map)).toList(), + introductoryOffer: json['introductoryOffer'] != null + ? SubscriptionOfferIOS.fromJson( + json['introductoryOffer'] as Map) + : null, + 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), + subscriptionPeriod: SubscriptionPeriodValueIOS.fromJson( + json['subscriptionPeriod'] as Map), ); } @@ -2988,7 +3311,9 @@ class SubscriptionInfoIOS { return { '__typename': 'SubscriptionInfoIOS', 'introductoryOffer': introductoryOffer?.toJson(), - 'promotionalOffers': promotionalOffers == null ? null : promotionalOffers!.map((e) => e.toJson()).toList(), + 'promotionalOffers': promotionalOffers == null + ? null + : promotionalOffers!.map((e) => e.toJson()).toList(), 'subscriptionGroupId': subscriptionGroupId, 'subscriptionPeriod': subscriptionPeriod.toJson(), }; @@ -2997,11 +3322,11 @@ class SubscriptionInfoIOS { /// Standardized subscription discount/promotional offer. /// Provides a unified interface for subscription offers across iOS and Android. -/// +/// /// Both platforms support subscription offers with different implementations: /// - iOS: Introductory offers, promotional offers with server-side signatures /// - Android: Offer tokens with pricing phases -/// +/// /// @see https://openiap.dev/docs/types/ios#discount-offer /// @see https://openiap.dev/docs/types/android#subscription-offer class SubscriptionOffer { @@ -3030,50 +3355,68 @@ class SubscriptionOffer { /// [Android] Base plan identifier. /// Identifies which base plan this offer belongs to. final String? basePlanIdAndroid; + /// Currency code (ISO 4217, e.g., "USD") final String? currency; + /// Formatted display price string (e.g., "$9.99/month") final String displayPrice; + /// Unique identifier for the offer. /// - iOS: Discount identifier from App Store Connect /// - Android: offerId from ProductSubscriptionAndroidOfferDetails final String id; + /// [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+ final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid; + /// [iOS] Key identifier for signature validation. /// Used with server-side signature generation for promotional offers. final String? keyIdentifierIOS; + /// [iOS] Localized price string. final String? localizedPriceIOS; + /// [iOS] Cryptographic nonce (UUID) for signature validation. /// Must be generated server-side for each purchase attempt. final String? nonceIOS; + /// [iOS] Number of billing periods for this discount. final int? numberOfPeriodsIOS; + /// [Android] List of tags associated with this offer. final List? offerTagsAndroid; + /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. final String? offerTokenAndroid; + /// Payment mode during the offer period final PaymentMode? paymentMode; + /// Subscription period for this offer final SubscriptionPeriod? period; + /// Number of periods the offer applies final int? periodCount; + /// Numeric price value final double price; + /// [Android] Pricing phases for this subscription offer. /// Contains detailed pricing information for each phase (trial, intro, regular). final PricingPhasesAndroid? pricingPhasesAndroid; + /// [iOS] Server-generated signature for promotional offer validation. /// Required when applying promotional offers on iOS. final String? signatureIOS; + /// [iOS] Timestamp when the signature was generated. /// Used for signature validation. final double? timestampIOS; + /// Type of subscription offer (Introductory or Promotional) final DiscountOfferType type; @@ -3083,18 +3426,33 @@ class SubscriptionOffer { currency: json['currency'] as String?, displayPrice: json['displayPrice'] as String, id: json['id'] as String, - installmentPlanDetailsAndroid: json['installmentPlanDetailsAndroid'] != null ? InstallmentPlanDetailsAndroid.fromJson(json['installmentPlanDetailsAndroid'] as Map) : null, + installmentPlanDetailsAndroid: + json['installmentPlanDetailsAndroid'] != null + ? InstallmentPlanDetailsAndroid.fromJson( + json['installmentPlanDetailsAndroid'] as Map) + : null, keyIdentifierIOS: json['keyIdentifierIOS'] as String?, localizedPriceIOS: json['localizedPriceIOS'] as String?, nonceIOS: json['nonceIOS'] as String?, numberOfPeriodsIOS: json['numberOfPeriodsIOS'] as int?, - offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null ? null : (json['offerTagsAndroid'] as List?)!.map((e) => e as String).toList(), + offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null + ? null + : (json['offerTagsAndroid'] as List?)! + .map((e) => e as String) + .toList(), offerTokenAndroid: json['offerTokenAndroid'] as String?, - paymentMode: json['paymentMode'] != null ? PaymentMode.fromJson(json['paymentMode'] as String) : null, - period: json['period'] != null ? SubscriptionPeriod.fromJson(json['period'] as Map) : null, + paymentMode: json['paymentMode'] != null + ? PaymentMode.fromJson(json['paymentMode'] as String) + : null, + period: json['period'] != null + ? SubscriptionPeriod.fromJson(json['period'] as Map) + : null, periodCount: json['periodCount'] as int?, price: (json['price'] as num).toDouble(), - pricingPhasesAndroid: json['pricingPhasesAndroid'] != null ? PricingPhasesAndroid.fromJson(json['pricingPhasesAndroid'] as Map) : null, + pricingPhasesAndroid: json['pricingPhasesAndroid'] != null + ? PricingPhasesAndroid.fromJson( + json['pricingPhasesAndroid'] as Map) + : null, signatureIOS: json['signatureIOS'] as String?, timestampIOS: (json['timestampIOS'] as num?)?.toDouble(), type: DiscountOfferType.fromJson(json['type'] as String), @@ -3154,7 +3512,8 @@ class SubscriptionOfferIOS { displayPrice: json['displayPrice'] as String, id: json['id'] as String, paymentMode: PaymentModeIOS.fromJson(json['paymentMode'] as String), - period: SubscriptionPeriodValueIOS.fromJson(json['period'] as Map), + period: SubscriptionPeriodValueIOS.fromJson( + json['period'] as Map), periodCount: json['periodCount'] as int, price: (json['price'] as num).toDouble(), type: SubscriptionOfferTypeIOS.fromJson(json['type'] as String), @@ -3184,6 +3543,7 @@ class SubscriptionPeriod { /// The period unit (day, week, month, year) final SubscriptionPeriodUnit unit; + /// The number of units (e.g., 1 for monthly, 3 for quarterly) final int value; @@ -3239,7 +3599,9 @@ class SubscriptionStatusIOS { factory SubscriptionStatusIOS.fromJson(Map json) { return SubscriptionStatusIOS( - renewalInfo: json['renewalInfo'] != null ? RenewalInfoIOS.fromJson(json['renewalInfo'] as Map) : null, + renewalInfo: json['renewalInfo'] != null + ? RenewalInfoIOS.fromJson(json['renewalInfo'] as Map) + : null, state: json['state'] as String, ); } @@ -3263,13 +3625,15 @@ class UserChoiceBillingDetails { /// Token that must be reported to Google Play within 24 hours final String externalTransactionToken; + /// List of product IDs selected by the user final List products; factory UserChoiceBillingDetails.fromJson(Map json) { return UserChoiceBillingDetails( externalTransactionToken: json['externalTransactionToken'] as String, - products: (json['products'] as List).map((e) => e as String).toList(), + products: + (json['products'] as List).map((e) => e as String).toList(), ); } @@ -3292,6 +3656,7 @@ class ValidTimeWindowAndroid { /// End time in milliseconds since epoch final String endTimeMillis; + /// Start time in milliseconds since epoch final String startTimeMillis; @@ -3411,6 +3776,7 @@ class VerifyPurchaseResultHorizon extends VerifyPurchaseResult { /// Unix timestamp (seconds) when the entitlement was granted. final double? grantTime; + /// Whether the entitlement verification succeeded. final bool success; @@ -3441,10 +3807,13 @@ class VerifyPurchaseResultIOS extends VerifyPurchaseResult { /// Whether the receipt is valid final bool isValid; + /// JWS representation final String jwsRepresentation; + /// Latest transaction if available final Purchase? latestTransaction; + /// Receipt data string final String receiptData; @@ -3452,7 +3821,9 @@ class VerifyPurchaseResultIOS extends VerifyPurchaseResult { return VerifyPurchaseResultIOS( isValid: json['isValid'] as bool, jwsRepresentation: json['jwsRepresentation'] as String, - latestTransaction: json['latestTransaction'] != null ? Purchase.fromJson(json['latestTransaction'] as Map) : null, + latestTransaction: json['latestTransaction'] != null + ? Purchase.fromJson(json['latestTransaction'] as Map) + : null, receiptData: json['receiptData'] as String, ); } @@ -3503,15 +3874,25 @@ class VerifyPurchaseWithProviderResult { /// Error details if verification failed final List? errors; + /// IAPKit verification result final RequestVerifyPurchaseWithIapkitResult? iapkit; final PurchaseVerificationProvider provider; factory VerifyPurchaseWithProviderResult.fromJson(Map json) { return VerifyPurchaseWithProviderResult( - errors: (json['errors'] as List?) == null ? null : (json['errors'] as List?)!.map((e) => VerifyPurchaseWithProviderError.fromJson(e as Map)).toList(), - iapkit: json['iapkit'] != null ? RequestVerifyPurchaseWithIapkitResult.fromJson(json['iapkit'] as Map) : null, - provider: PurchaseVerificationProvider.fromJson(json['provider'] as String), + errors: (json['errors'] as List?) == null + ? null + : (json['errors'] as List?)! + .map((e) => VerifyPurchaseWithProviderError.fromJson( + e as Map)) + .toList(), + iapkit: json['iapkit'] != null + ? RequestVerifyPurchaseWithIapkitResult.fromJson( + json['iapkit'] as Map) + : null, + provider: + PurchaseVerificationProvider.fromJson(json['provider'] as String), ); } @@ -3537,6 +3918,7 @@ class AndroidSubscriptionOfferInput { /// Offer token final String offerToken; + /// Product SKU final String sku; @@ -3563,6 +3945,7 @@ class DeepLinkOptions { /// Android package name to target (required on Android) final String? packageNameAndroid; + /// Android SKU to open (required on Android) final String? skuAndroid; @@ -3593,15 +3976,20 @@ class DeveloperBillingOptionParamsAndroid { /// The billing program (should be EXTERNAL_PAYMENTS for external payments flow) final BillingProgramAndroid billingProgram; + /// The launch mode for the external payment link final DeveloperBillingLaunchModeAndroid launchMode; + /// The URI where the external payment will be processed final String linkUri; - factory DeveloperBillingOptionParamsAndroid.fromJson(Map json) { + factory DeveloperBillingOptionParamsAndroid.fromJson( + Map json) { return DeveloperBillingOptionParamsAndroid( - billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), - launchMode: DeveloperBillingLaunchModeAndroid.fromJson(json['launchMode'] as String), + billingProgram: + BillingProgramAndroid.fromJson(json['billingProgram'] as String), + launchMode: DeveloperBillingLaunchModeAndroid.fromJson( + json['launchMode'] as String), linkUri: json['linkUri'] as String, ); } @@ -3626,12 +4014,16 @@ class DiscountOfferInputIOS { /// Discount identifier final String identifier; + /// Key identifier for validation final String keyIdentifier; + /// Cryptographic nonce final String nonce; + /// Signature for validation final String signature; + /// Timestamp of discount offer final double timestamp; @@ -3668,6 +4060,7 @@ class InitConnectionConfig { /// @deprecated Use enableBillingProgramAndroid instead. /// Use USER_CHOICE_BILLING for user choice billing, EXTERNAL_OFFER for alternative only. final AlternativeBillingModeAndroid? alternativeBillingModeAndroid; + /// Enable a specific billing program for Android (7.0+) /// When set, enables the specified billing program for external transactions. /// - USER_CHOICE_BILLING: User can select between Google Play or alternative (7.0+) @@ -3678,8 +4071,15 @@ class InitConnectionConfig { factory InitConnectionConfig.fromJson(Map json) { return InitConnectionConfig( - alternativeBillingModeAndroid: json['alternativeBillingModeAndroid'] != null ? AlternativeBillingModeAndroid.fromJson(json['alternativeBillingModeAndroid'] as String) : null, - enableBillingProgramAndroid: json['enableBillingProgramAndroid'] != null ? BillingProgramAndroid.fromJson(json['enableBillingProgramAndroid'] as String) : null, + alternativeBillingModeAndroid: + json['alternativeBillingModeAndroid'] != null + ? AlternativeBillingModeAndroid.fromJson( + json['alternativeBillingModeAndroid'] as String) + : null, + enableBillingProgramAndroid: json['enableBillingProgramAndroid'] != null + ? BillingProgramAndroid.fromJson( + json['enableBillingProgramAndroid'] as String) + : null, ); } @@ -3704,17 +4104,22 @@ class LaunchExternalLinkParamsAndroid { /// The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER) final BillingProgramAndroid billingProgram; + /// The external link launch mode final ExternalLinkLaunchModeAndroid launchMode; + /// The type of the external link final ExternalLinkTypeAndroid linkType; + /// The URI where the content will be accessed from final String linkUri; factory LaunchExternalLinkParamsAndroid.fromJson(Map json) { return LaunchExternalLinkParamsAndroid( - billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), - launchMode: ExternalLinkLaunchModeAndroid.fromJson(json['launchMode'] as String), + billingProgram: + BillingProgramAndroid.fromJson(json['billingProgram'] as String), + launchMode: + ExternalLinkLaunchModeAndroid.fromJson(json['launchMode'] as String), linkType: ExternalLinkTypeAndroid.fromJson(json['linkType'] as String), linkUri: json['linkUri'] as String, ); @@ -3742,7 +4147,9 @@ class ProductRequest { factory ProductRequest.fromJson(Map json) { return ProductRequest( skus: (json['skus'] as List).map((e) => e as String).toList(), - type: json['type'] != null ? ProductQueryType.fromJson(json['type'] as String) : null, + type: json['type'] != null + ? ProductQueryType.fromJson(json['type'] as String) + : null, ); } @@ -3768,6 +4175,7 @@ class PromotionalOfferJWSInputIOS { /// The JWS should contain the promotional offer signature data. /// Format: header.payload.signature (base64url encoded) final String jws; + /// The promotional offer identifier from App Store Connect final String offerId; @@ -3797,17 +4205,20 @@ class PurchaseOptions { /// Also emit results through the iOS event listeners final bool? alsoPublishToEventListenerIOS; + /// Include suspended subscriptions in the result (Android 8.1+). /// Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. /// Users should be directed to the subscription center to resolve payment issues. /// Default: false (only active subscriptions are returned) final bool? includeSuspendedAndroid; + /// Limit to currently active items on iOS final bool? onlyIncludeActiveItemsIOS; factory PurchaseOptions.fromJson(Map json) { return PurchaseOptions( - alsoPublishToEventListenerIOS: json['alsoPublishToEventListenerIOS'] as bool?, + alsoPublishToEventListenerIOS: + json['alsoPublishToEventListenerIOS'] as bool?, includeSuspendedAndroid: json['includeSuspendedAndroid'] as bool?, onlyIncludeActiveItemsIOS: json['onlyIncludeActiveItemsIOS'] as bool?, ); @@ -3836,23 +4247,31 @@ class RequestPurchaseAndroidProps { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. final DeveloperBillingOptionParamsAndroid? developerBillingOption; + /// Personalized offer flag. /// When true, indicates the price was customized for this user. final bool? isOfferPersonalized; + /// Obfuscated account ID final String? obfuscatedAccountId; + /// Obfuscated profile ID final String? obfuscatedProfileId; + /// Offer token for one-time purchase discounts (7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. final String? offerToken; + /// List of product SKUs final List skus; factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] != null + ? DeveloperBillingOptionParamsAndroid.fromJson( + json['developerBillingOption'] as Map) + : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, @@ -3888,14 +4307,19 @@ class RequestPurchaseIosProps { /// campaign tokens, affiliate IDs, or other attribution data. /// The data is formatted as JSON: {"signatureInfo": {"token": ""}} final String? advancedCommerceData; + /// Auto-finish transaction (dangerous) final bool? andDangerouslyFinishTransactionAutomatically; + /// App account token for user tracking final String? appAccountToken; + /// Purchase quantity final int? quantity; + /// Product SKU final String sku; + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). /// iOS only supports promotional offers for auto-renewable subscriptions. final DiscountOfferInputIOS? withOffer; @@ -3903,18 +4327,23 @@ class RequestPurchaseIosProps { factory RequestPurchaseIosProps.fromJson(Map json) { return RequestPurchaseIosProps( advancedCommerceData: json['advancedCommerceData'] as String?, - andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?, + andDangerouslyFinishTransactionAutomatically: + json['andDangerouslyFinishTransactionAutomatically'] as bool?, appAccountToken: json['appAccountToken'] as String?, quantity: json['quantity'] as int?, sku: json['sku'] as String, - withOffer: json['withOffer'] != null ? DiscountOfferInputIOS.fromJson(json['withOffer'] as Map) : null, + withOffer: json['withOffer'] != null + ? DiscountOfferInputIOS.fromJson( + json['withOffer'] as Map) + : null, ); } Map toJson() { return { 'advancedCommerceData': advancedCommerceData, - 'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically, + 'andDangerouslyFinishTransactionAutomatically': + andDangerouslyFinishTransactionAutomatically, 'appAccountToken': appAccountToken, 'quantity': quantity, 'sku': sku, @@ -3926,17 +4355,19 @@ class RequestPurchaseIosProps { sealed class RequestPurchaseProps { const RequestPurchaseProps._(); - const factory RequestPurchaseProps.inApp(({ - RequestPurchaseIosProps? apple, - RequestPurchaseAndroidProps? google, - bool? useAlternativeBilling, - }) props) = _InAppPurchase; + const factory RequestPurchaseProps.inApp( + ({ + RequestPurchaseIosProps? apple, + RequestPurchaseAndroidProps? google, + bool? useAlternativeBilling, + }) props) = _InAppPurchase; - const factory RequestPurchaseProps.subs(({ - RequestSubscriptionIosProps? apple, - RequestSubscriptionAndroidProps? google, - bool? useAlternativeBilling, - }) props) = _SubsPurchase; + const factory RequestPurchaseProps.subs( + ({ + RequestSubscriptionIosProps? apple, + RequestSubscriptionAndroidProps? google, + bool? useAlternativeBilling, + }) props) = _SubsPurchase; Map toJson(); } @@ -3957,7 +4388,8 @@ class _InAppPurchase extends RequestPurchaseProps { if (props.google != null) 'android': props.google!.toJson(), }, 'type': ProductQueryType.InApp.toJson(), - if (props.useAlternativeBilling != null) 'useAlternativeBilling': props.useAlternativeBilling, + if (props.useAlternativeBilling != null) + 'useAlternativeBilling': props.useAlternativeBilling, }; } } @@ -3978,13 +4410,14 @@ class _SubsPurchase extends RequestPurchaseProps { if (props.google != null) 'android': props.google!.toJson(), }, 'type': ProductQueryType.Subs.toJson(), - if (props.useAlternativeBilling != null) 'useAlternativeBilling': props.useAlternativeBilling, + if (props.useAlternativeBilling != null) + 'useAlternativeBilling': props.useAlternativeBilling, }; } } /// Platform-specific purchase request parameters. -/// +/// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store /// - google: Targets Play Store by default, or Horizon when built with horizon flavor @@ -3999,19 +4432,34 @@ class RequestPurchasePropsByPlatforms { /// @deprecated Use google instead final RequestPurchaseAndroidProps? android; + /// Apple-specific purchase parameters final RequestPurchaseIosProps? apple; + /// Google-specific purchase parameters final RequestPurchaseAndroidProps? google; + /// @deprecated Use apple instead final RequestPurchaseIosProps? ios; factory RequestPurchasePropsByPlatforms.fromJson(Map json) { return RequestPurchasePropsByPlatforms( - android: json['android'] != null ? RequestPurchaseAndroidProps.fromJson(json['android'] as Map) : null, - apple: json['apple'] != null ? RequestPurchaseIosProps.fromJson(json['apple'] as Map) : null, - google: json['google'] != null ? RequestPurchaseAndroidProps.fromJson(json['google'] as Map) : null, - ios: json['ios'] != null ? RequestPurchaseIosProps.fromJson(json['ios'] as Map) : null, + android: json['android'] != null + ? RequestPurchaseAndroidProps.fromJson( + json['android'] as Map) + : null, + apple: json['apple'] != null + ? RequestPurchaseIosProps.fromJson( + json['apple'] as Map) + : null, + google: json['google'] != null + ? RequestPurchaseAndroidProps.fromJson( + json['google'] as Map) + : null, + ios: json['ios'] != null + ? RequestPurchaseIosProps.fromJson( + json['ios'] as Map) + : null, ); } @@ -4042,37 +4490,59 @@ class RequestSubscriptionAndroidProps { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. final DeveloperBillingOptionParamsAndroid? developerBillingOption; + /// Personalized offer flag. /// When true, indicates the price was customized for this user. final bool? isOfferPersonalized; + /// Obfuscated account ID final String? obfuscatedAccountId; + /// Obfuscated profile ID final String? obfuscatedProfileId; + /// Purchase token for upgrades/downgrades final String? purchaseToken; + /// Replacement mode for subscription changes /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) final int? replacementMode; + /// List of subscription SKUs final List skus; + /// Subscription offers final List? subscriptionOffers; + /// Product-level replacement parameters (8.1.0+) /// Use this instead of replacementMode for item-level replacement - final SubscriptionProductReplacementParamsAndroid? subscriptionProductReplacementParams; + final SubscriptionProductReplacementParamsAndroid? + subscriptionProductReplacementParams; factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] != null + ? DeveloperBillingOptionParamsAndroid.fromJson( + json['developerBillingOption'] as Map) + : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, purchaseToken: json['purchaseToken'] as String?, replacementMode: json['replacementMode'] as int?, skus: (json['skus'] as List).map((e) => e as String).toList(), - subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => AndroidSubscriptionOfferInput.fromJson(e as Map)).toList(), - subscriptionProductReplacementParams: json['subscriptionProductReplacementParams'] != null ? SubscriptionProductReplacementParamsAndroid.fromJson(json['subscriptionProductReplacementParams'] as Map) : null, + subscriptionOffers: (json['subscriptionOffers'] as List?) == null + ? null + : (json['subscriptionOffers'] as List?)! + .map((e) => AndroidSubscriptionOfferInput.fromJson( + e as Map)) + .toList(), + subscriptionProductReplacementParams: + json['subscriptionProductReplacementParams'] != null + ? SubscriptionProductReplacementParamsAndroid.fromJson( + json['subscriptionProductReplacementParams'] + as Map) + : null, ); } @@ -4085,8 +4555,11 @@ class RequestSubscriptionAndroidProps { 'purchaseToken': purchaseToken, 'replacementMode': replacementMode, 'skus': skus, - 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), - 'subscriptionProductReplacementParams': subscriptionProductReplacementParams?.toJson(), + 'subscriptionOffers': subscriptionOffers == null + ? null + : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionProductReplacementParams': + subscriptionProductReplacementParams?.toJson(), }; } } @@ -4111,22 +4584,26 @@ class RequestSubscriptionIosProps { final String? advancedCommerceData; final bool? andDangerouslyFinishTransactionAutomatically; 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; final int? quantity; final String sku; + /// Win-back offer to apply (iOS 18+) /// Used to re-engage churned subscribers with a discount or free trial. /// 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; @@ -4134,21 +4611,33 @@ class RequestSubscriptionIosProps { factory RequestSubscriptionIosProps.fromJson(Map json) { return RequestSubscriptionIosProps( advancedCommerceData: json['advancedCommerceData'] as String?, - andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?, + 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, + 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, + winBackOffer: json['winBackOffer'] != null + ? WinBackOfferInputIOS.fromJson( + json['winBackOffer'] as Map) + : null, + withOffer: json['withOffer'] != null + ? DiscountOfferInputIOS.fromJson( + json['withOffer'] as Map) + : null, ); } Map toJson() { return { 'advancedCommerceData': advancedCommerceData, - 'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically, + 'andDangerouslyFinishTransactionAutomatically': + andDangerouslyFinishTransactionAutomatically, 'appAccountToken': appAccountToken, 'introductoryOfferEligibility': introductoryOfferEligibility, 'promotionalOfferJWS': promotionalOfferJWS?.toJson(), @@ -4161,7 +4650,7 @@ class RequestSubscriptionIosProps { } /// Platform-specific subscription request parameters. -/// +/// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store /// - google: Targets Play Store by default, or Horizon when built with horizon flavor @@ -4176,19 +4665,35 @@ class RequestSubscriptionPropsByPlatforms { /// @deprecated Use google instead final RequestSubscriptionAndroidProps? android; + /// Apple-specific subscription parameters final RequestSubscriptionIosProps? apple; + /// Google-specific subscription parameters final RequestSubscriptionAndroidProps? google; + /// @deprecated Use apple instead final RequestSubscriptionIosProps? ios; - factory RequestSubscriptionPropsByPlatforms.fromJson(Map json) { + factory RequestSubscriptionPropsByPlatforms.fromJson( + Map json) { return RequestSubscriptionPropsByPlatforms( - android: json['android'] != null ? RequestSubscriptionAndroidProps.fromJson(json['android'] as Map) : null, - apple: json['apple'] != null ? RequestSubscriptionIosProps.fromJson(json['apple'] as Map) : null, - google: json['google'] != null ? RequestSubscriptionAndroidProps.fromJson(json['google'] as Map) : null, - ios: json['ios'] != null ? RequestSubscriptionIosProps.fromJson(json['ios'] as Map) : null, + android: json['android'] != null + ? RequestSubscriptionAndroidProps.fromJson( + json['android'] as Map) + : null, + apple: json['apple'] != null + ? RequestSubscriptionIosProps.fromJson( + json['apple'] as Map) + : null, + google: json['google'] != null + ? RequestSubscriptionAndroidProps.fromJson( + json['google'] as Map) + : null, + ios: json['ios'] != null + ? RequestSubscriptionIosProps.fromJson( + json['ios'] as Map) + : null, ); } @@ -4210,7 +4715,8 @@ class RequestVerifyPurchaseWithIapkitAppleProps { /// The JWS token returned with the purchase response. final String jws; - factory RequestVerifyPurchaseWithIapkitAppleProps.fromJson(Map json) { + factory RequestVerifyPurchaseWithIapkitAppleProps.fromJson( + Map json) { return RequestVerifyPurchaseWithIapkitAppleProps( jws: json['jws'] as String, ); @@ -4231,7 +4737,8 @@ class RequestVerifyPurchaseWithIapkitGoogleProps { /// The token provided to the user's device when the product or subscription was purchased. final String purchaseToken; - factory RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(Map json) { + factory RequestVerifyPurchaseWithIapkitGoogleProps.fromJson( + Map json) { return RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken: json['purchaseToken'] as String, ); @@ -4245,7 +4752,7 @@ class RequestVerifyPurchaseWithIapkitGoogleProps { } /// Platform-specific verification parameters for IAPKit. -/// +/// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) class RequestVerifyPurchaseWithIapkitProps { @@ -4257,16 +4764,25 @@ class RequestVerifyPurchaseWithIapkitProps { /// API key used for the Authorization header (Bearer {apiKey}). final String? apiKey; + /// Apple App Store verification parameters. final RequestVerifyPurchaseWithIapkitAppleProps? apple; + /// Google Play Store verification parameters. final RequestVerifyPurchaseWithIapkitGoogleProps? google; - factory RequestVerifyPurchaseWithIapkitProps.fromJson(Map json) { + factory RequestVerifyPurchaseWithIapkitProps.fromJson( + Map json) { return RequestVerifyPurchaseWithIapkitProps( apiKey: json['apiKey'] as String?, - apple: json['apple'] != null ? RequestVerifyPurchaseWithIapkitAppleProps.fromJson(json['apple'] as Map) : null, - google: json['google'] != null ? RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(json['google'] as Map) : null, + apple: json['apple'] != null + ? RequestVerifyPurchaseWithIapkitAppleProps.fromJson( + json['apple'] as Map) + : null, + google: json['google'] != null + ? RequestVerifyPurchaseWithIapkitGoogleProps.fromJson( + json['google'] as Map) + : null, ); } @@ -4290,13 +4806,16 @@ class SubscriptionProductReplacementParamsAndroid { /// The old product ID that needs to be replaced final String oldProductId; + /// The replacement mode for this product change final SubscriptionReplacementModeAndroid replacementMode; - factory SubscriptionProductReplacementParamsAndroid.fromJson(Map json) { + factory SubscriptionProductReplacementParamsAndroid.fromJson( + Map json) { return SubscriptionProductReplacementParamsAndroid( oldProductId: json['oldProductId'] as String, - replacementMode: SubscriptionReplacementModeAndroid.fromJson(json['replacementMode'] as String), + replacementMode: SubscriptionReplacementModeAndroid.fromJson( + json['replacementMode'] as String), ); } @@ -4333,7 +4852,7 @@ class VerifyPurchaseAppleOptions { /// Google Play Store verification parameters. /// Used for server-side receipt validation via Google Play Developer API. -/// +/// /// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. class VerifyPurchaseGoogleOptions { const VerifyPurchaseGoogleOptions({ @@ -4347,13 +4866,17 @@ class VerifyPurchaseGoogleOptions { /// Google OAuth2 access token for API authentication. /// ⚠️ Sensitive: Do not log this value. final String accessToken; + /// Whether this is a subscription purchase (affects API endpoint used) final bool? isSub; + /// Android package name (e.g., com.example.app) final String packageName; + /// Purchase token from the purchase response. /// ⚠️ Sensitive: Do not log this value. final String purchaseToken; + /// Product SKU to validate final String sku; @@ -4381,7 +4904,7 @@ class VerifyPurchaseGoogleOptions { /// Meta Horizon (Quest) verification parameters. /// Used for server-side entitlement verification via Meta's S2S API. /// POST https://graph.oculus.com/$APP_ID/verify_entitlement -/// +/// /// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. class VerifyPurchaseHorizonOptions { const VerifyPurchaseHorizonOptions({ @@ -4393,8 +4916,10 @@ class VerifyPurchaseHorizonOptions { /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). /// ⚠️ Sensitive: Do not log this value. final String accessToken; + /// The SKU for the add-on item, defined in Meta Developer Dashboard final String sku; + /// The user ID of the user whose purchase you want to verify final String userId; @@ -4416,7 +4941,7 @@ class VerifyPurchaseHorizonOptions { } /// Platform-specific purchase verification parameters. -/// +/// /// - apple: Verifies via App Store Server API /// - google: Verifies via Google Play Developer API /// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) @@ -4429,16 +4954,27 @@ class VerifyPurchaseProps { /// Apple App Store verification parameters. final VerifyPurchaseAppleOptions? apple; + /// Google Play Store verification parameters. final VerifyPurchaseGoogleOptions? google; + /// Meta Horizon (Quest) verification parameters. final VerifyPurchaseHorizonOptions? horizon; factory VerifyPurchaseProps.fromJson(Map json) { return VerifyPurchaseProps( - apple: json['apple'] != null ? VerifyPurchaseAppleOptions.fromJson(json['apple'] as Map) : null, - google: json['google'] != null ? VerifyPurchaseGoogleOptions.fromJson(json['google'] as Map) : null, - horizon: json['horizon'] != null ? VerifyPurchaseHorizonOptions.fromJson(json['horizon'] as Map) : null, + apple: json['apple'] != null + ? VerifyPurchaseAppleOptions.fromJson( + json['apple'] as Map) + : null, + google: json['google'] != null + ? VerifyPurchaseGoogleOptions.fromJson( + json['google'] as Map) + : null, + horizon: json['horizon'] != null + ? VerifyPurchaseHorizonOptions.fromJson( + json['horizon'] as Map) + : null, ); } @@ -4462,8 +4998,12 @@ class VerifyPurchaseWithProviderProps { factory VerifyPurchaseWithProviderProps.fromJson(Map json) { return VerifyPurchaseWithProviderProps( - iapkit: json['iapkit'] != null ? RequestVerifyPurchaseWithIapkitProps.fromJson(json['iapkit'] as Map) : null, - provider: PurchaseVerificationProvider.fromJson(json['provider'] as String), + iapkit: json['iapkit'] != null + ? RequestVerifyPurchaseWithIapkitProps.fromJson( + json['iapkit'] as Map) + : null, + provider: + PurchaseVerificationProvider.fromJson(json['provider'] as String), ); } @@ -4551,11 +5091,14 @@ sealed class ProductOrSubscription { case 'ProductIOS': return ProductOrSubscriptionProduct(Product.fromJson(json)); case 'ProductSubscriptionAndroid': - return ProductOrSubscriptionProductSubscription(ProductSubscription.fromJson(json)); + return ProductOrSubscriptionProductSubscription( + ProductSubscription.fromJson(json)); case 'ProductSubscriptionIOS': - return ProductOrSubscriptionProductSubscription(ProductSubscription.fromJson(json)); + return ProductOrSubscriptionProductSubscription( + ProductSubscription.fromJson(json)); } - throw ArgumentError('Unknown __typename for ProductOrSubscription: $typeName'); + throw ArgumentError( + 'Unknown __typename for ProductOrSubscription: $typeName'); } Map toJson(); @@ -4588,7 +5131,8 @@ sealed class ProductSubscription implements ProductCommon { case 'ProductSubscriptionIOS': return ProductSubscriptionIOS.fromJson(json); } - throw ArgumentError('Unknown __typename for ProductSubscription: $typeName'); + throw ArgumentError( + 'Unknown __typename for ProductSubscription: $typeName'); } @override @@ -4647,11 +5191,13 @@ sealed class Purchase implements PurchaseCommon { String get productId; @override PurchaseState get purchaseState; + /// Unified purchase token (iOS JWS, Android purchaseToken) @override String? get purchaseToken; @override int get quantity; + /// Store where purchase was made @override IapStore get store; @@ -4674,7 +5220,8 @@ sealed class VerifyPurchaseResult { case 'VerifyPurchaseResultIOS': return VerifyPurchaseResultIOS.fromJson(json); } - throw ArgumentError('Unknown __typename for VerifyPurchaseResult: $typeName'); + throw ArgumentError( + 'Unknown __typename for VerifyPurchaseResult: $typeName'); } Map toJson(); @@ -4686,60 +5233,75 @@ sealed class VerifyPurchaseResult { abstract class MutationResolver { /// Acknowledge a non-consumable purchase or subscription Future acknowledgePurchaseAndroid(String purchaseToken); + /// Initiate a refund request for a product (iOS 15+) Future beginRefundRequestIOS(String sku); + /// Check if alternative billing is available for this user/device /// Step 1 of alternative billing flow - /// + /// /// Returns true if available, false otherwise /// Throws OpenIapError.NotPrepared if billing client not ready Future checkAlternativeBillingAvailabilityAndroid(); + /// Clear pending transactions from the StoreKit payment queue Future clearTransactionIOS(); + /// Consume a purchase token so it can be repurchased Future consumePurchaseAndroid(String purchaseToken); + /// Create external transaction token for Google Play reporting /// Step 3 of alternative billing flow /// Must be called AFTER successful payment in your payment system /// Token must be reported to Google Play backend within 24 hours - /// + /// /// Returns token string, or null if creation failed /// Throws OpenIapError.NotPrepared if billing client not ready Future createAlternativeBillingTokenAndroid(); + /// Create reporting details for a billing program /// Replaces the deprecated createExternalOfferReportingDetailsAsync API - /// + /// /// Available in Google Play Billing Library 8.2.0+ /// Returns external transaction token needed for reporting external transactions /// Throws OpenIapError.NotPrepared if billing client not ready - Future createBillingProgramReportingDetailsAndroid(BillingProgramAndroid program); + Future + createBillingProgramReportingDetailsAndroid( + BillingProgramAndroid program); + /// Open the native subscription management surface Future deepLinkToSubscriptions({ String? packageNameAndroid, String? skuAndroid, }); + /// Close the platform billing connection Future endConnection(); + /// Finish a transaction after validating receipts Future finishTransaction({ required PurchaseInput purchase, bool? isConsumable, }); + /// Establish the platform billing connection Future initConnection({ AlternativeBillingModeAndroid? alternativeBillingModeAndroid, BillingProgramAndroid? enableBillingProgramAndroid, }); + /// Check if a billing program is available for the current user /// Replaces the deprecated isExternalOfferAvailableAsync API - /// + /// /// Available in Google Play Billing Library 8.2.0+ /// Returns availability result with isAvailable flag /// Throws OpenIapError.NotPrepared if billing client not ready - Future isBillingProgramAvailableAndroid(BillingProgramAndroid program); + Future + isBillingProgramAvailableAndroid(BillingProgramAndroid program); + /// Launch external link flow for external billing programs /// Replaces the deprecated showExternalOfferInformationDialog API - /// + /// /// Available in Google Play Billing Library 8.2.0+ /// Shows Play Store dialog and optionally launches external URL /// Throws OpenIapError.NotPrepared if billing client not ready @@ -4749,52 +5311,69 @@ abstract class MutationResolver { required ExternalLinkTypeAndroid linkType, required String linkUri, }); + /// Present the App Store code redemption sheet Future presentCodeRedemptionSheetIOS(); + /// Present external purchase custom link with StoreKit UI - Future presentExternalPurchaseLinkIOS(String url); + Future presentExternalPurchaseLinkIOS( + String url); + /// Present external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when user continues. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - Future presentExternalPurchaseNoticeSheetIOS(); + Future + presentExternalPurchaseNoticeSheetIOS(); + /// Initiate a purchase flow; rely on events for final state Future requestPurchase(RequestPurchaseProps params); + /// Purchase the promoted product surfaced by the App Store. - /// + /// /// @deprecated Use promotedProductListenerIOS to receive the productId, /// then call requestPurchase with that SKU instead. In StoreKit 2, /// promoted products can be purchased directly via the standard purchase flow. Future requestPurchaseOnPromotedProductIOS(); + /// Restore completed purchases across platforms Future restorePurchases(); + /// Show alternative billing information dialog to user /// Step 2 of alternative billing flow /// Must be called BEFORE processing payment in your payment system - /// + /// /// Returns true if user accepted, false if user canceled /// Throws OpenIapError.NotPrepared if billing client not ready Future showAlternativeBillingDialogAndroid(); + /// Show ExternalPurchaseCustomLink notice sheet (iOS 18.1+). /// Displays the system disclosure notice sheet for custom external purchase links. /// Call this after a deliberate customer interaction before linking out to external purchases. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - Future showExternalPurchaseCustomLinkNoticeIOS(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); + Future + showExternalPurchaseCustomLinkNoticeIOS( + ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); + /// Open subscription management UI and return changed purchases (iOS 15+) Future> showManageSubscriptionsIOS(); + /// Force a StoreKit sync for transactions (iOS 15+) Future syncIOS(); + /// Validate purchase receipts with the configured providers Future validateReceipt({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, }); + /// Verify purchases with the configured providers Future verifyPurchase({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, }); + /// Verify purchases with a specific provider (e.g., IAPKit) Future verifyPurchaseWithProvider({ RequestVerifyPurchaseWithIapkitProps? iapkit, @@ -4807,53 +5386,75 @@ abstract class QueryResolver { /// Check if external purchase notice sheet can be presented (iOS 17.4+) /// Uses ExternalPurchase.canPresent Future canPresentExternalPurchaseNoticeIOS(); + /// Get current StoreKit 2 entitlements (iOS 15+) Future currentEntitlementIOS(String sku); + /// Retrieve products or subscriptions from the store Future fetchProducts({ required List skus, ProductQueryType? type, }); + /// Get active subscriptions (filters by subscriptionIds when provided) - Future> getActiveSubscriptions([List? subscriptionIds]); + Future> getActiveSubscriptions( + [List? subscriptionIds]); + /// Fetch the current app transaction (iOS 16+) Future getAppTransactionIOS(); + /// Get all available purchases for the current user Future> getAvailablePurchases({ bool? alsoPublishToEventListenerIOS, bool? includeSuspendedAndroid, bool? onlyIncludeActiveItemsIOS, }); + /// Get external purchase token for reporting to Apple (iOS 18.1+). /// Use this token with Apple's External Purchase Server API to report transactions. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - Future getExternalPurchaseCustomLinkTokenIOS(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); + Future + getExternalPurchaseCustomLinkTokenIOS( + ExternalPurchaseCustomLinkTokenTypeIOS tokenType); + /// Retrieve all pending transactions in the StoreKit queue Future> getPendingTransactionsIOS(); + /// Get the currently promoted product (iOS 11+) Future getPromotedProductIOS(); + /// Get base64-encoded receipt data for validation Future getReceiptDataIOS(); + /// Get the current storefront country code Future getStorefront(); + /// Get the current App Store storefront country code Future getStorefrontIOS(); + /// Get the transaction JWS (StoreKit 2) Future getTransactionJwsIOS(String sku); + /// Check whether the user has active subscriptions Future hasActiveSubscriptions([List? subscriptionIds]); + /// Check if app is eligible for ExternalPurchaseCustomLink API (iOS 18.1+). /// Returns true if the app can use custom external purchase links. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible Future isEligibleForExternalPurchaseCustomLinkIOS(); + /// Check introductory offer eligibility for a subscription group Future isEligibleForIntroOfferIOS(String groupID); + /// Verify a StoreKit 2 transaction signature Future isTransactionVerifiedIOS(String sku); + /// Get the latest transaction for a product using StoreKit 2 Future latestTransactionIOS(String sku); + /// Get StoreKit 2 subscription status details (iOS 15+) Future> subscriptionStatusIOS(String sku); + /// Validate a receipt for a specific product Future validateReceiptIOS({ VerifyPurchaseAppleOptions? apple, @@ -4869,13 +5470,18 @@ abstract class SubscriptionResolver { /// instead of Google Play Billing in the side-by-side choice dialog. /// Contains the externalTransactionToken needed to report the transaction. /// Available in Google Play Billing Library 8.3.0+ - Future developerProvidedBillingAndroid(); + Future + developerProvidedBillingAndroid(); + /// Fires when the App Store surfaces a promoted product (iOS only) Future promotedProductIOS(); + /// Fires when a purchase fails or is cancelled Future purchaseError(); + /// Fires when a purchase completes successfully or a pending purchase resolves Future purchaseUpdated(); + /// Fires when a user selects alternative billing in the User Choice Billing dialog (Android only) /// Only triggered when the user selects alternative billing instead of Google Play billing Future userChoiceBillingAndroid(); @@ -4885,13 +5491,20 @@ abstract class SubscriptionResolver { // MARK: - Mutation Helpers -typedef MutationAcknowledgePurchaseAndroidHandler = Future Function(String purchaseToken); -typedef MutationBeginRefundRequestIOSHandler = Future Function(String sku); -typedef MutationCheckAlternativeBillingAvailabilityAndroidHandler = Future Function(); +typedef MutationAcknowledgePurchaseAndroidHandler = Future Function( + String purchaseToken); +typedef MutationBeginRefundRequestIOSHandler = Future Function( + String sku); +typedef MutationCheckAlternativeBillingAvailabilityAndroidHandler = Future + Function(); typedef MutationClearTransactionIOSHandler = Future Function(); -typedef MutationConsumePurchaseAndroidHandler = Future Function(String purchaseToken); -typedef MutationCreateAlternativeBillingTokenAndroidHandler = Future Function(); -typedef MutationCreateBillingProgramReportingDetailsAndroidHandler = Future Function(BillingProgramAndroid program); +typedef MutationConsumePurchaseAndroidHandler = Future Function( + String purchaseToken); +typedef MutationCreateAlternativeBillingTokenAndroidHandler = Future + Function(); +typedef MutationCreateBillingProgramReportingDetailsAndroidHandler + = Future Function( + BillingProgramAndroid program); typedef MutationDeepLinkToSubscriptionsHandler = Future Function({ String? packageNameAndroid, String? skuAndroid, @@ -4905,7 +5518,9 @@ typedef MutationInitConnectionHandler = Future Function({ AlternativeBillingModeAndroid? alternativeBillingModeAndroid, BillingProgramAndroid? enableBillingProgramAndroid, }); -typedef MutationIsBillingProgramAvailableAndroidHandler = Future Function(BillingProgramAndroid program); +typedef MutationIsBillingProgramAvailableAndroidHandler + = Future Function( + BillingProgramAndroid program); typedef MutationLaunchExternalLinkAndroidHandler = Future Function({ required BillingProgramAndroid billingProgram, required ExternalLinkLaunchModeAndroid launchMode, @@ -4913,14 +5528,22 @@ typedef MutationLaunchExternalLinkAndroidHandler = Future Function({ required String linkUri, }); typedef MutationPresentCodeRedemptionSheetIOSHandler = Future Function(); -typedef MutationPresentExternalPurchaseLinkIOSHandler = Future Function(String url); -typedef MutationPresentExternalPurchaseNoticeSheetIOSHandler = Future Function(); -typedef MutationRequestPurchaseHandler = Future Function(RequestPurchaseProps params); -typedef MutationRequestPurchaseOnPromotedProductIOSHandler = Future Function(); +typedef MutationPresentExternalPurchaseLinkIOSHandler + = Future Function(String url); +typedef MutationPresentExternalPurchaseNoticeSheetIOSHandler + = Future Function(); +typedef MutationRequestPurchaseHandler = Future + Function(RequestPurchaseProps params); +typedef MutationRequestPurchaseOnPromotedProductIOSHandler = Future + Function(); typedef MutationRestorePurchasesHandler = Future Function(); -typedef MutationShowAlternativeBillingDialogAndroidHandler = Future Function(); -typedef MutationShowExternalPurchaseCustomLinkNoticeIOSHandler = Future Function(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); -typedef MutationShowManageSubscriptionsIOSHandler = Future> Function(); +typedef MutationShowAlternativeBillingDialogAndroidHandler = Future + Function(); +typedef MutationShowExternalPurchaseCustomLinkNoticeIOSHandler + = Future Function( + ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); +typedef MutationShowManageSubscriptionsIOSHandler = Future> + Function(); typedef MutationSyncIOSHandler = Future Function(); typedef MutationValidateReceiptHandler = Future Function({ VerifyPurchaseAppleOptions? apple, @@ -4932,7 +5555,8 @@ typedef MutationVerifyPurchaseHandler = Future Function({ VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, }); -typedef MutationVerifyPurchaseWithProviderHandler = Future Function({ +typedef MutationVerifyPurchaseWithProviderHandler + = Future Function({ RequestVerifyPurchaseWithIapkitProps? iapkit, required PurchaseVerificationProvider provider, }); @@ -4969,25 +5593,35 @@ class MutationHandlers { final MutationAcknowledgePurchaseAndroidHandler? acknowledgePurchaseAndroid; final MutationBeginRefundRequestIOSHandler? beginRefundRequestIOS; - final MutationCheckAlternativeBillingAvailabilityAndroidHandler? checkAlternativeBillingAvailabilityAndroid; + final MutationCheckAlternativeBillingAvailabilityAndroidHandler? + checkAlternativeBillingAvailabilityAndroid; final MutationClearTransactionIOSHandler? clearTransactionIOS; final MutationConsumePurchaseAndroidHandler? consumePurchaseAndroid; - final MutationCreateAlternativeBillingTokenAndroidHandler? createAlternativeBillingTokenAndroid; - final MutationCreateBillingProgramReportingDetailsAndroidHandler? createBillingProgramReportingDetailsAndroid; + final MutationCreateAlternativeBillingTokenAndroidHandler? + createAlternativeBillingTokenAndroid; + final MutationCreateBillingProgramReportingDetailsAndroidHandler? + createBillingProgramReportingDetailsAndroid; final MutationDeepLinkToSubscriptionsHandler? deepLinkToSubscriptions; final MutationEndConnectionHandler? endConnection; final MutationFinishTransactionHandler? finishTransaction; final MutationInitConnectionHandler? initConnection; - final MutationIsBillingProgramAvailableAndroidHandler? isBillingProgramAvailableAndroid; + final MutationIsBillingProgramAvailableAndroidHandler? + isBillingProgramAvailableAndroid; final MutationLaunchExternalLinkAndroidHandler? launchExternalLinkAndroid; - final MutationPresentCodeRedemptionSheetIOSHandler? presentCodeRedemptionSheetIOS; - final MutationPresentExternalPurchaseLinkIOSHandler? presentExternalPurchaseLinkIOS; - final MutationPresentExternalPurchaseNoticeSheetIOSHandler? presentExternalPurchaseNoticeSheetIOS; + final MutationPresentCodeRedemptionSheetIOSHandler? + presentCodeRedemptionSheetIOS; + final MutationPresentExternalPurchaseLinkIOSHandler? + presentExternalPurchaseLinkIOS; + final MutationPresentExternalPurchaseNoticeSheetIOSHandler? + presentExternalPurchaseNoticeSheetIOS; final MutationRequestPurchaseHandler? requestPurchase; - final MutationRequestPurchaseOnPromotedProductIOSHandler? requestPurchaseOnPromotedProductIOS; + final MutationRequestPurchaseOnPromotedProductIOSHandler? + requestPurchaseOnPromotedProductIOS; final MutationRestorePurchasesHandler? restorePurchases; - final MutationShowAlternativeBillingDialogAndroidHandler? showAlternativeBillingDialogAndroid; - final MutationShowExternalPurchaseCustomLinkNoticeIOSHandler? showExternalPurchaseCustomLinkNoticeIOS; + final MutationShowAlternativeBillingDialogAndroidHandler? + showAlternativeBillingDialogAndroid; + final MutationShowExternalPurchaseCustomLinkNoticeIOSHandler? + showExternalPurchaseCustomLinkNoticeIOS; final MutationShowManageSubscriptionsIOSHandler? showManageSubscriptionsIOS; final MutationSyncIOSHandler? syncIOS; final MutationValidateReceiptHandler? validateReceipt; @@ -4997,33 +5631,46 @@ class MutationHandlers { // MARK: - Query Helpers -typedef QueryCanPresentExternalPurchaseNoticeIOSHandler = Future Function(); -typedef QueryCurrentEntitlementIOSHandler = Future Function(String sku); +typedef QueryCanPresentExternalPurchaseNoticeIOSHandler = Future + Function(); +typedef QueryCurrentEntitlementIOSHandler = Future Function( + String sku); typedef QueryFetchProductsHandler = Future Function({ required List skus, ProductQueryType? type, }); -typedef QueryGetActiveSubscriptionsHandler = Future> Function([List? subscriptionIds]); +typedef QueryGetActiveSubscriptionsHandler = Future> + Function([List? subscriptionIds]); typedef QueryGetAppTransactionIOSHandler = Future Function(); typedef QueryGetAvailablePurchasesHandler = Future> Function({ bool? alsoPublishToEventListenerIOS, bool? includeSuspendedAndroid, bool? onlyIncludeActiveItemsIOS, }); -typedef QueryGetExternalPurchaseCustomLinkTokenIOSHandler = Future Function(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); -typedef QueryGetPendingTransactionsIOSHandler = Future> Function(); +typedef QueryGetExternalPurchaseCustomLinkTokenIOSHandler + = Future Function( + ExternalPurchaseCustomLinkTokenTypeIOS tokenType); +typedef QueryGetPendingTransactionsIOSHandler = Future> + Function(); typedef QueryGetPromotedProductIOSHandler = Future Function(); typedef QueryGetReceiptDataIOSHandler = Future Function(); typedef QueryGetStorefrontHandler = Future Function(); typedef QueryGetStorefrontIOSHandler = Future Function(); typedef QueryGetTransactionJwsIOSHandler = Future Function(String sku); -typedef QueryHasActiveSubscriptionsHandler = Future Function([List? subscriptionIds]); -typedef QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler = Future Function(); -typedef QueryIsEligibleForIntroOfferIOSHandler = Future Function(String groupID); -typedef QueryIsTransactionVerifiedIOSHandler = Future Function(String sku); -typedef QueryLatestTransactionIOSHandler = Future Function(String sku); -typedef QuerySubscriptionStatusIOSHandler = Future> Function(String sku); -typedef QueryValidateReceiptIOSHandler = Future Function({ +typedef QueryHasActiveSubscriptionsHandler = Future Function( + [List? subscriptionIds]); +typedef QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler = Future + Function(); +typedef QueryIsEligibleForIntroOfferIOSHandler = Future Function( + String groupID); +typedef QueryIsTransactionVerifiedIOSHandler = Future Function( + String sku); +typedef QueryLatestTransactionIOSHandler = Future Function( + String sku); +typedef QuerySubscriptionStatusIOSHandler = Future> + Function(String sku); +typedef QueryValidateReceiptIOSHandler = Future + Function({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, @@ -5053,13 +5700,15 @@ class QueryHandlers { this.validateReceiptIOS, }); - final QueryCanPresentExternalPurchaseNoticeIOSHandler? canPresentExternalPurchaseNoticeIOS; + final QueryCanPresentExternalPurchaseNoticeIOSHandler? + canPresentExternalPurchaseNoticeIOS; final QueryCurrentEntitlementIOSHandler? currentEntitlementIOS; final QueryFetchProductsHandler? fetchProducts; final QueryGetActiveSubscriptionsHandler? getActiveSubscriptions; final QueryGetAppTransactionIOSHandler? getAppTransactionIOS; final QueryGetAvailablePurchasesHandler? getAvailablePurchases; - final QueryGetExternalPurchaseCustomLinkTokenIOSHandler? getExternalPurchaseCustomLinkTokenIOS; + final QueryGetExternalPurchaseCustomLinkTokenIOSHandler? + getExternalPurchaseCustomLinkTokenIOS; final QueryGetPendingTransactionsIOSHandler? getPendingTransactionsIOS; final QueryGetPromotedProductIOSHandler? getPromotedProductIOS; final QueryGetReceiptDataIOSHandler? getReceiptDataIOS; @@ -5067,7 +5716,8 @@ class QueryHandlers { final QueryGetStorefrontIOSHandler? getStorefrontIOS; final QueryGetTransactionJwsIOSHandler? getTransactionJwsIOS; final QueryHasActiveSubscriptionsHandler? hasActiveSubscriptions; - final QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler? isEligibleForExternalPurchaseCustomLinkIOS; + final QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler? + isEligibleForExternalPurchaseCustomLinkIOS; final QueryIsEligibleForIntroOfferIOSHandler? isEligibleForIntroOfferIOS; final QueryIsTransactionVerifiedIOSHandler? isTransactionVerifiedIOS; final QueryLatestTransactionIOSHandler? latestTransactionIOS; @@ -5077,11 +5727,13 @@ class QueryHandlers { // MARK: - Subscription Helpers -typedef SubscriptionDeveloperProvidedBillingAndroidHandler = Future Function(); +typedef SubscriptionDeveloperProvidedBillingAndroidHandler + = Future Function(); typedef SubscriptionPromotedProductIOSHandler = Future Function(); typedef SubscriptionPurchaseErrorHandler = Future Function(); typedef SubscriptionPurchaseUpdatedHandler = Future Function(); -typedef SubscriptionUserChoiceBillingAndroidHandler = Future Function(); +typedef SubscriptionUserChoiceBillingAndroidHandler + = Future Function(); class SubscriptionHandlers { const SubscriptionHandlers({ @@ -5092,7 +5744,8 @@ class SubscriptionHandlers { this.userChoiceBillingAndroid, }); - final SubscriptionDeveloperProvidedBillingAndroidHandler? developerProvidedBillingAndroid; + final SubscriptionDeveloperProvidedBillingAndroidHandler? + developerProvidedBillingAndroid; final SubscriptionPromotedProductIOSHandler? promotedProductIOS; final SubscriptionPurchaseErrorHandler? purchaseError; final SubscriptionPurchaseUpdatedHandler? purchaseUpdated; diff --git a/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart b/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart index a18ccb19..70446e32 100644 --- a/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart +++ b/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart @@ -609,6 +609,7 @@ void main() { 'Deferred replacement requires the base offer, got a promo offer', ); expect(error.responseCode, 5); + expect(error.platform, types.IapPlatform.Android); }, ); }); diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 9690a8ac..4c7c7a66 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -2000,6 +2000,7 @@ class PurchaseError: var code: ErrorCode var message: String = "" var product_id: String = "" + var debug_message: String = "" static func from_dict(data: Dictionary) -> PurchaseError: var obj = PurchaseError.new() @@ -2013,6 +2014,8 @@ class PurchaseError: obj.message = data["message"] if data.has("productId") and data["productId"] != null: obj.product_id = data["productId"] + if data.has("debugMessage") and data["debugMessage"] != null: + obj.debug_message = data["debugMessage"] return obj func to_dict() -> Dictionary: @@ -2023,6 +2026,7 @@ class PurchaseError: dict["code"] = code dict["message"] = message dict["productId"] = product_id + dict["debugMessage"] = debug_message return dict class PurchaseIOS: 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 8a45bfab..4afc517e 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 @@ -7,8 +7,6 @@ @file:Suppress("UNCHECKED_CAST") package io.github.hyochan.kmpiap.openiap - - // MARK: - Enums /** @@ -34,6 +32,7 @@ public enum class AlternativeBillingModeAndroid(val rawValue: String) { * @deprecated Use BillingProgramAndroid.EXTERNAL_OFFER instead */ AlternativeOnly("alternative-only"); + companion object { fun fromJson(value: String): AlternativeBillingModeAndroid = when (value) { "none" -> AlternativeBillingModeAndroid.None @@ -83,6 +82,7 @@ public enum class BillingProgramAndroid(val rawValue: String) { * Available in Google Play Billing Library 8.3.0+ */ ExternalPayments("external-payments"); + companion object { fun fromJson(value: String): BillingProgramAndroid = when (value) { "unspecified" -> BillingProgramAndroid.Unspecified @@ -122,6 +122,7 @@ public enum class DeveloperBillingLaunchModeAndroid(val rawValue: String) { * Use this when you want to handle launching the external payment URL yourself. */ CallerWillLaunchLink("caller-will-launch-link"); + companion object { fun fromJson(value: String): DeveloperBillingLaunchModeAndroid = when (value) { "unspecified" -> DeveloperBillingLaunchModeAndroid.Unspecified @@ -154,6 +155,7 @@ public enum class DiscountOfferType(val rawValue: String) { * One-time product discount (Android only, Google Play Billing 7.0+) */ OneTime("one-time"); + companion object { fun fromJson(value: String): DiscountOfferType = when (value) { "introductory" -> DiscountOfferType.Introductory @@ -209,7 +211,9 @@ public enum class ErrorCode(val rawValue: String) { ItemNotOwned("item-not-owned"), BillingUnavailable("billing-unavailable"), FeatureNotSupported("feature-not-supported"), - EmptySkuList("empty-sku-list"); + EmptySkuList("empty-sku-list"), + DuplicatePurchase("duplicate-purchase"); + companion object { fun fromJson(value: String): ErrorCode = when (value) { "unknown" -> ErrorCode.Unknown @@ -323,6 +327,9 @@ public enum class ErrorCode(val rawValue: String) { "empty-sku-list" -> ErrorCode.EmptySkuList "EMPTY_SKU_LIST" -> ErrorCode.EmptySkuList "EmptySkuList" -> ErrorCode.EmptySkuList + "duplicate-purchase" -> ErrorCode.DuplicatePurchase + "DUPLICATE_PURCHASE" -> ErrorCode.DuplicatePurchase + "DuplicatePurchase" -> ErrorCode.DuplicatePurchase else -> throw IllegalArgumentException("Unknown ErrorCode value: $value") } } @@ -348,6 +355,7 @@ public enum class ExternalLinkLaunchModeAndroid(val rawValue: String) { * Play will not launch the URL. The app handles launching the URL after Play returns control. */ CallerWillLaunchLink("caller-will-launch-link"); + companion object { fun fromJson(value: String): ExternalLinkLaunchModeAndroid = when (value) { "unspecified" -> ExternalLinkLaunchModeAndroid.Unspecified @@ -381,6 +389,7 @@ public enum class ExternalLinkTypeAndroid(val rawValue: String) { * The link will direct users to download an app */ LinkToAppDownload("link-to-app-download"); + companion object { fun fromJson(value: String): ExternalLinkTypeAndroid = when (value) { "unspecified" -> ExternalLinkTypeAndroid.Unspecified @@ -407,6 +416,7 @@ public enum class ExternalPurchaseCustomLinkNoticeTypeIOS(val rawValue: String) * or destination of the app's choice. */ Browser("browser"); + companion object { fun fromJson(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS = when (value) { "browser" -> ExternalPurchaseCustomLinkNoticeTypeIOS.Browser @@ -435,6 +445,7 @@ public enum class ExternalPurchaseCustomLinkTokenTypeIOS(val rawValue: String) { * Use this for existing customers making additional purchases. */ Services("services"); + companion object { fun fromJson(value: String): ExternalPurchaseCustomLinkTokenTypeIOS = when (value) { "acquisition" -> ExternalPurchaseCustomLinkTokenTypeIOS.Acquisition @@ -462,6 +473,7 @@ public enum class ExternalPurchaseNoticeAction(val rawValue: String) { * User dismissed the notice sheet */ Dismissed("dismissed"); + companion object { fun fromJson(value: String): ExternalPurchaseNoticeAction = when (value) { "continue" -> ExternalPurchaseNoticeAction.Continue @@ -487,6 +499,7 @@ public enum class IapEvent(val rawValue: String) { * Available on Android with Google Play Billing Library 8.3.0+ */ DeveloperProvidedBillingAndroid("developer-provided-billing-android"); + companion object { fun fromJson(value: String): IapEvent = when (value) { "purchase-updated" -> IapEvent.PurchaseUpdated @@ -551,6 +564,7 @@ public enum class IapkitPurchaseState(val rawValue: String) { * Purchase receipt is not authentic (fraudulent or tampered). */ Inauthentic("inauthentic"); + companion object { fun fromJson(value: String): IapkitPurchaseState = when (value) { "entitled" -> IapkitPurchaseState.Entitled @@ -581,6 +595,7 @@ public enum class IapkitPurchaseState(val rawValue: String) { public enum class IapPlatform(val rawValue: String) { Ios("ios"), Android("android"); + companion object { fun fromJson(value: String): IapPlatform = when (value) { "ios" -> IapPlatform.Ios @@ -600,6 +615,7 @@ public enum class IapStore(val rawValue: String) { Apple("apple"), Google("google"), Horizon("horizon"); + companion object { fun fromJson(value: String): IapStore = when (value) { "unknown" -> IapStore.Unknown @@ -642,6 +658,7 @@ public enum class PaymentMode(val rawValue: String) { * Unknown or unspecified payment mode */ Unknown("unknown"); + companion object { fun fromJson(value: String): PaymentMode = when (value) { "free-trial" -> PaymentMode.FreeTrial @@ -668,6 +685,7 @@ public enum class PaymentModeIOS(val rawValue: String) { FreeTrial("free-trial"), PayAsYouGo("pay-as-you-go"), PayUpFront("pay-up-front"); + companion object { fun fromJson(value: String): PaymentModeIOS = when (value) { "empty" -> PaymentModeIOS.Empty @@ -693,6 +711,7 @@ public enum class ProductQueryType(val rawValue: String) { InApp("in-app"), Subs("subs"), All("all"); + companion object { fun fromJson(value: String): ProductQueryType = when (value) { "in-app" -> ProductQueryType.InApp @@ -734,6 +753,7 @@ public enum class ProductStatusAndroid(val rawValue: String) { * Unknown error occurred while fetching the product */ Unknown("unknown"); + companion object { fun fromJson(value: String): ProductStatusAndroid = when (value) { "ok" -> ProductStatusAndroid.Ok @@ -754,6 +774,7 @@ public enum class ProductStatusAndroid(val rawValue: String) { public enum class ProductType(val rawValue: String) { InApp("in-app"), Subs("subs"); + companion object { fun fromJson(value: String): ProductType = when (value) { "in-app" -> ProductType.InApp @@ -774,6 +795,7 @@ public enum class ProductTypeIOS(val rawValue: String) { NonConsumable("non-consumable"), AutoRenewableSubscription("auto-renewable-subscription"), NonRenewingSubscription("non-renewing-subscription"); + companion object { fun fromJson(value: String): ProductTypeIOS = when (value) { "consumable" -> ProductTypeIOS.Consumable @@ -799,6 +821,7 @@ public enum class PurchaseState(val rawValue: String) { Pending("pending"), Purchased("purchased"), Unknown("unknown"); + companion object { fun fromJson(value: String): PurchaseState = when (value) { "pending" -> PurchaseState.Pending @@ -819,6 +842,7 @@ public enum class PurchaseState(val rawValue: String) { public enum class PurchaseVerificationProvider(val rawValue: String) { Iapkit("iapkit"); + companion object { fun fromJson(value: String): PurchaseVerificationProvider = when (value) { "iapkit" -> PurchaseVerificationProvider.Iapkit @@ -848,6 +872,7 @@ public enum class SubResponseCodeAndroid(val rawValue: String) { * User doesn't meet subscription offer eligibility requirements */ UserIneligible("user-ineligible"); + companion object { fun fromJson(value: String): SubResponseCodeAndroid = when (value) { "no-applicable-sub-response-code" -> SubResponseCodeAndroid.NoApplicableSubResponseCode @@ -871,6 +896,7 @@ public enum class SubscriptionOfferTypeIOS(val rawValue: String) { * Used to re-engage churned subscribers with a discount or free trial. */ WinBack("win-back"); + companion object { fun fromJson(value: String): SubscriptionOfferTypeIOS = when (value) { "introductory" -> SubscriptionOfferTypeIOS.Introductory @@ -895,6 +921,7 @@ public enum class SubscriptionPeriodIOS(val rawValue: String) { Month("month"), Year("year"), Empty("empty"); + companion object { fun fromJson(value: String): SubscriptionPeriodIOS = when (value) { "day" -> SubscriptionPeriodIOS.Day @@ -928,6 +955,7 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { Month("month"), Year("year"), Unknown("unknown"); + companion object { fun fromJson(value: String): SubscriptionPeriodUnit = when (value) { "day" -> SubscriptionPeriodUnit.Day @@ -986,6 +1014,7 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { * Keep the existing payment schedule unchanged for the item (8.1.0+) */ KeepExisting("keep-existing"); + companion object { fun fromJson(value: String): SubscriptionReplacementModeAndroid = when (value) { "unknown-replacement-mode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode @@ -2586,6 +2615,7 @@ public data class PurchaseAndroid( public data class PurchaseError( val code: ErrorCode, + val debugMessage: String? = null, val message: String, val productId: String? = null ) { @@ -2594,6 +2624,7 @@ public data class PurchaseError( fun fromJson(json: Map): PurchaseError { return PurchaseError( code = (json["code"] as? String)?.let { ErrorCode.fromJson(it) } ?: ErrorCode.Unknown, + debugMessage = json["debugMessage"] as? String, message = json["message"] as? String ?: "", productId = json["productId"] as? String, ) @@ -2603,6 +2634,7 @@ public data class PurchaseError( fun toJson(): Map = mapOf( "__typename" to "PurchaseError", "code" to code.toJson(), + "debugMessage" to debugMessage, "message" to message, "productId" to productId, ) diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt index 21e93e15..19fb1f48 100644 --- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt +++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt @@ -153,5 +153,6 @@ object ErrorCodeUtils { ErrorCode.PurchaseVerificationFailed -> "Purchase verification failed" ErrorCode.PurchaseVerificationFinished -> "Purchase verification completed" ErrorCode.PurchaseVerificationFinishFailed -> "Failed to complete purchase verification" + ErrorCode.DuplicatePurchase -> "Duplicate purchase update detected" } } diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts index 7bb7aec7..dabfb9b6 100644 --- a/libraries/react-native-iap/src/types.ts +++ b/libraries/react-native-iap/src/types.ts @@ -1105,6 +1105,7 @@ export interface PurchaseCommon { export interface PurchaseError { code: ErrorCode; + debugMessage?: (string | null); message: string; productId?: (string | null); } diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 9fa68ab6..d639053f 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -454,35 +454,35 @@ public protocol PurchaseCommon: Codable { // MARK: - Objects public struct ActiveSubscription: Codable { - public var autoRenewingAndroid: Bool? - public var basePlanIdAndroid: String? + public var autoRenewingAndroid: Bool? = nil + public var basePlanIdAndroid: String? = nil /// The current plan identifier. This is: /// - On Android: the basePlanId (e.g., "premium", "premium-year") /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") /// This provides a unified way to identify which specific plan/tier the user is subscribed to. - public var currentPlanId: String? - public var daysUntilExpirationIOS: Double? - public var environmentIOS: String? - public var expirationDateIOS: Double? + public var currentPlanId: String? = nil + public var daysUntilExpirationIOS: Double? = nil + public var environmentIOS: String? = nil + public var expirationDateIOS: Double? = nil public var isActive: Bool public var productId: String - public var purchaseToken: String? + public var purchaseToken: String? = nil /// Required for subscription upgrade/downgrade on Android - public var purchaseTokenAndroid: String? + public var purchaseTokenAndroid: String? = nil /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, /// pending upgrades/downgrades, and auto-renewal preferences. - public var renewalInfoIOS: RenewalInfoIOS? + public var renewalInfoIOS: RenewalInfoIOS? = nil public var transactionDate: Double public var transactionId: String /// @deprecated iOS only - use daysUntilExpirationIOS instead. /// Whether the subscription will expire soon (within 7 days). /// Consider using daysUntilExpirationIOS for more precise control. - public var willExpireSoon: Bool? + public var willExpireSoon: Bool? = nil } public struct AppTransaction: Codable { public var appId: Double - public var appTransactionId: String? + public var appTransactionId: String? = nil public var appVersion: String public var appVersionId: Double public var bundleId: String @@ -490,9 +490,9 @@ public struct AppTransaction: Codable { public var deviceVerificationNonce: String public var environment: String public var originalAppVersion: String - public var originalPlatform: String? + public var originalPlatform: String? = nil public var originalPurchaseDate: Double - public var preorderDate: Double? + public var preorderDate: Double? = nil public var signedDate: Double } @@ -520,12 +520,12 @@ public struct BillingProgramReportingDetailsAndroid: Codable { /// Available in Google Play Billing Library 8.0.0+ public struct BillingResultAndroid: Codable { /// Debug message from the billing library - public var debugMessage: String? + public var debugMessage: String? = nil /// The response code from the billing operation public var responseCode: Int /// Sub-response code for more granular error information (8.0+). /// Provides additional context when responseCode indicates an error. - public var subResponseCode: SubResponseCodeAndroid? + public var subResponseCode: SubResponseCodeAndroid? = nil } /// Details provided when user selects developer billing option (Android) @@ -552,10 +552,10 @@ public struct DiscountAmountAndroid: Codable { public struct DiscountDisplayInfoAndroid: Codable { /// Absolute discount amount details /// Only returned for fixed amount discounts - public var discountAmount: DiscountAmountAndroid? + public var discountAmount: DiscountAmountAndroid? = nil /// Percentage discount (e.g., 33 for 33% off) /// Only returned for percentage-based discounts - public var percentageDiscount: Int? + public var percentageDiscount: Int? = nil } /// Discount information returned from the store. @@ -563,7 +563,7 @@ public struct DiscountDisplayInfoAndroid: Codable { /// @see https://openiap.dev/docs/types#subscription-offer public struct DiscountIOS: Codable { public var identifier: String - public var localizedPrice: String? + public var localizedPrice: String? = nil public var numberOfPeriods: Int public var paymentMode: PaymentModeIOS public var price: String @@ -584,46 +584,46 @@ public struct DiscountOffer: Codable { public var currency: String /// [Android] Fixed discount amount in micro-units. /// Only present for fixed amount discounts. - public var discountAmountMicrosAndroid: String? + public var discountAmountMicrosAndroid: String? = nil /// Formatted display price string (e.g., "$4.99") public var displayPrice: String /// [Android] Formatted discount amount string (e.g., "$5.00 OFF"). - public var formattedDiscountAmountAndroid: String? + public var formattedDiscountAmountAndroid: String? = nil /// [Android] Original full price in micro-units before discount. /// Divide by 1,000,000 to get the actual price. /// Use for displaying strikethrough original price. - public var fullPriceMicrosAndroid: String? + public var fullPriceMicrosAndroid: String? = nil /// Unique identifier for the offer. /// - iOS: Not applicable (one-time discounts not supported) /// - Android: offerId from ProductAndroidOneTimePurchaseOfferDetail - public var id: String? + public var id: String? = nil /// [Android] Limited quantity information. /// Contains maximumQuantity and remainingQuantity. - public var limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? + public var limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? = nil /// [Android] List of tags associated with this offer. - public var offerTagsAndroid: [String]? + public var offerTagsAndroid: [String]? = nil /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. - public var offerTokenAndroid: String? + public var offerTokenAndroid: String? = nil /// [Android] Percentage discount (e.g., 33 for 33% off). /// Only present for percentage-based discounts. - public var percentageDiscountAndroid: Int? + public var percentageDiscountAndroid: Int? = nil /// [Android] Pre-order details if this is a pre-order offer. /// Available in Google Play Billing Library 8.1.0+ - public var preorderDetailsAndroid: PreorderDetailsAndroid? + public var preorderDetailsAndroid: PreorderDetailsAndroid? = nil /// 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? + public var purchaseOptionIdAndroid: String? = nil /// [Android] Rental details if this is a rental offer. - public var rentalDetailsAndroid: RentalDetailsAndroid? + public var rentalDetailsAndroid: RentalDetailsAndroid? = nil /// Type of discount offer public var type: DiscountOfferType /// [Android] Valid time window for the offer. /// Contains startTimeMillis and endTimeMillis. - public var validTimeWindowAndroid: ValidTimeWindowAndroid? + public var validTimeWindowAndroid: ValidTimeWindowAndroid? = nil } /// iOS DiscountOffer (output type). @@ -669,22 +669,22 @@ public struct ExternalPurchaseCustomLinkNoticeResultIOS: Codable { /// Whether the user chose to continue to external purchase public var continued: Bool /// Optional error message if the presentation failed - public var error: String? + public var error: String? = nil } /// Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). public struct ExternalPurchaseCustomLinkTokenResultIOS: Codable { /// Optional error message if token retrieval failed - public var error: String? + public var error: String? = nil /// The external purchase token string. /// Report this token to Apple's External Purchase Server API. - public var token: String? + public var token: String? = nil } /// Result of presenting an external purchase link public struct ExternalPurchaseLinkResultIOS: Codable { /// Optional error message if the presentation failed - public var error: String? + public var error: String? = nil /// Whether the user completed the external purchase flow public var success: Bool } @@ -693,11 +693,11 @@ public struct ExternalPurchaseLinkResultIOS: Codable { /// Returns the token when user continues to external purchase. public struct ExternalPurchaseNoticeResultIOS: Codable { /// Optional error message if the presentation failed - public var error: String? + public var error: String? = nil /// External purchase token returned when user continues (iOS 17.4+). /// This token should be reported to Apple's External Purchase Server API. /// Only present when result is Continue. - public var externalPurchaseToken: String? + public var externalPurchaseToken: String? = nil /// Notice result indicating user action public var result: ExternalPurchaseNoticeAction } @@ -772,34 +772,34 @@ public struct PricingPhasesAndroid: Codable { public struct ProductAndroid: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer - public var discountOffers: [DiscountOffer]? - public var displayName: String? + public var discountOffers: [DiscountOffer]? = nil + public var displayName: String? = nil public var displayPrice: String public var id: String public var nameAndroid: String /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? + public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? = nil public var platform: IapPlatform = .android - public var price: Double? + public var price: Double? = nil /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ - public var productStatusAndroid: ProductStatusAndroid? + public var productStatusAndroid: ProductStatusAndroid? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]? + public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]? = nil /// Standardized subscription offers. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer - public var subscriptionOffers: [SubscriptionOffer]? + public var subscriptionOffers: [SubscriptionOffer]? = nil public var title: String public var type: ProductType = .inApp } @@ -811,53 +811,53 @@ public struct ProductAndroid: Codable, ProductCommon { public struct ProductAndroidOneTimePurchaseOfferDetail: Codable { /// Discount display information /// Only available for discounted offers - public var discountDisplayInfo: DiscountDisplayInfoAndroid? + public var discountDisplayInfo: DiscountDisplayInfoAndroid? = nil public var formattedPrice: String /// Full (non-discounted) price in micro-units /// Only available for discounted offers - public var fullPriceMicros: String? + public var fullPriceMicros: String? = nil /// Limited quantity information - public var limitedQuantityInfo: LimitedQuantityInfoAndroid? + public var limitedQuantityInfo: LimitedQuantityInfoAndroid? = nil /// Offer ID - public var offerId: String? + public var offerId: String? = nil /// List of offer tags public var offerTags: [String] /// Offer token for use in BillingFlowParams when purchasing public var offerToken: String /// Pre-order details for products available for pre-order /// Available in Google Play Billing Library 8.1.0+ - public var preorderDetailsAndroid: PreorderDetailsAndroid? + public var preorderDetailsAndroid: PreorderDetailsAndroid? = nil 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? + public var purchaseOptionId: String? = nil /// Rental details for rental offers - public var rentalDetailsAndroid: RentalDetailsAndroid? + public var rentalDetailsAndroid: RentalDetailsAndroid? = nil /// Valid time window for the offer - public var validTimeWindow: ValidTimeWindowAndroid? + public var validTimeWindow: ValidTimeWindowAndroid? = nil } public struct ProductIOS: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String - public var displayName: String? + public var displayName: String? = nil public var displayNameIOS: String public var displayPrice: String public var id: String public var isFamilyShareableIOS: Bool public var jsonRepresentationIOS: String public var platform: IapPlatform = .ios - public var price: Double? + public var price: Double? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var subscriptionInfoIOS: SubscriptionInfoIOS? + public var subscriptionInfoIOS: SubscriptionInfoIOS? = nil /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// Note: iOS does not support one-time product discounts. /// @see https://openiap.dev/docs/types#subscription-offer - public var subscriptionOffers: [SubscriptionOffer]? + public var subscriptionOffers: [SubscriptionOffer]? = nil public var title: String public var type: ProductType = .inApp public var typeIOS: ProductTypeIOS @@ -865,28 +865,28 @@ public struct ProductIOS: Codable, ProductCommon { public struct ProductSubscriptionAndroid: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer - public var discountOffers: [DiscountOffer]? - public var displayName: String? + public var discountOffers: [DiscountOffer]? = nil + public var displayName: String? = nil public var displayPrice: String public var id: String public var nameAndroid: String /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? + public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? = nil public var platform: IapPlatform = .android - public var price: Double? + public var price: Double? = nil /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ - public var productStatusAndroid: ProductStatusAndroid? + public var productStatusAndroid: ProductStatusAndroid? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails] /// Standardized subscription offers. @@ -905,8 +905,8 @@ public struct ProductSubscriptionAndroidOfferDetails: Codable { /// 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 installmentPlanDetails: InstallmentPlanDetailsAndroid? = nil + public var offerId: String? = nil public var offerTags: [String] public var offerToken: String public var pricingPhases: PricingPhasesAndroid @@ -914,113 +914,114 @@ public struct ProductSubscriptionAndroidOfferDetails: Codable { public struct ProductSubscriptionIOS: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var discountsIOS: [DiscountIOS]? - public var displayName: String? + public var discountsIOS: [DiscountIOS]? = nil + public var displayName: String? = nil public var displayNameIOS: String public var displayPrice: String public var id: String - public var introductoryPriceAsAmountIOS: String? - public var introductoryPriceIOS: String? - public var introductoryPriceNumberOfPeriodsIOS: String? + public var introductoryPriceAsAmountIOS: String? = nil + public var introductoryPriceIOS: String? = nil + public var introductoryPriceNumberOfPeriodsIOS: String? = nil public var introductoryPricePaymentModeIOS: PaymentModeIOS - public var introductoryPriceSubscriptionPeriodIOS: SubscriptionPeriodIOS? + public var introductoryPriceSubscriptionPeriodIOS: SubscriptionPeriodIOS? = nil public var isFamilyShareableIOS: Bool public var jsonRepresentationIOS: String public var platform: IapPlatform = .ios - public var price: Double? + public var price: Double? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var subscriptionInfoIOS: SubscriptionInfoIOS? + public var subscriptionInfoIOS: SubscriptionInfoIOS? = nil /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer - public var subscriptionOffers: [SubscriptionOffer]? - public var subscriptionPeriodNumberIOS: String? - public var subscriptionPeriodUnitIOS: SubscriptionPeriodIOS? + public var subscriptionOffers: [SubscriptionOffer]? = nil + public var subscriptionPeriodNumberIOS: String? = nil + public var subscriptionPeriodUnitIOS: SubscriptionPeriodIOS? = nil public var title: String public var type: ProductType = .subs public var typeIOS: ProductTypeIOS } public struct PurchaseAndroid: Codable, PurchaseCommon { - public var autoRenewingAndroid: Bool? - public var currentPlanId: String? - public var dataAndroid: String? - public var developerPayloadAndroid: String? + public var autoRenewingAndroid: Bool? = nil + public var currentPlanId: String? = nil + public var dataAndroid: String? = nil + public var developerPayloadAndroid: String? = nil public var id: String - public var ids: [String]? - public var isAcknowledgedAndroid: Bool? + public var ids: [String]? = nil + public var isAcknowledgedAndroid: Bool? = nil public var isAutoRenewing: Bool /// Whether the subscription is suspended (Android) /// A suspended subscription means the user's payment method failed and they need to fix it. /// Users should be directed to the subscription center to resolve the issue. /// Do NOT grant entitlements for suspended subscriptions. /// Available in Google Play Billing Library 8.1.0+ - public var isSuspendedAndroid: Bool? - public var obfuscatedAccountIdAndroid: String? - public var obfuscatedProfileIdAndroid: String? - public var packageNameAndroid: String? + public var isSuspendedAndroid: Bool? = nil + public var obfuscatedAccountIdAndroid: String? = nil + public var obfuscatedProfileIdAndroid: String? = nil + public var packageNameAndroid: String? = nil /// 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 pendingPurchaseUpdateAndroid: PendingPurchaseUpdateAndroid? = nil public var platform: IapPlatform public var productId: String public var purchaseState: PurchaseState - public var purchaseToken: String? + public var purchaseToken: String? = nil public var quantity: Int - public var signatureAndroid: String? + public var signatureAndroid: String? = nil /// Store where purchase was made public var store: IapStore public var transactionDate: Double - public var transactionId: String? + public var transactionId: String? = nil } public struct PurchaseError: Codable { public var code: ErrorCode + public var debugMessage: String? = nil public var message: String - public var productId: String? + public var productId: String? = nil } public struct PurchaseIOS: Codable, PurchaseCommon { - public var appAccountToken: String? - public var appBundleIdIOS: String? - public var countryCodeIOS: String? - public var currencyCodeIOS: String? - public var currencySymbolIOS: String? - public var currentPlanId: String? - public var environmentIOS: String? - public var expirationDateIOS: Double? + public var appAccountToken: String? = nil + public var appBundleIdIOS: String? = nil + public var countryCodeIOS: String? = nil + public var currencyCodeIOS: String? = nil + public var currencySymbolIOS: String? = nil + public var currentPlanId: String? = nil + public var environmentIOS: String? = nil + public var expirationDateIOS: Double? = nil public var id: String - public var ids: [String]? + public var ids: [String]? = nil public var isAutoRenewing: Bool - public var isUpgradedIOS: Bool? - public var offerIOS: PurchaseOfferIOS? - public var originalTransactionDateIOS: Double? - public var originalTransactionIdentifierIOS: String? - public var ownershipTypeIOS: String? + public var isUpgradedIOS: Bool? = nil + public var offerIOS: PurchaseOfferIOS? = nil + public var originalTransactionDateIOS: Double? = nil + public var originalTransactionIdentifierIOS: String? = nil + public var ownershipTypeIOS: String? = nil public var platform: IapPlatform public var productId: String public var purchaseState: PurchaseState - public var purchaseToken: String? + public var purchaseToken: String? = nil public var quantity: Int - public var quantityIOS: Int? - public var reasonIOS: String? - public var reasonStringRepresentationIOS: String? - public var renewalInfoIOS: RenewalInfoIOS? - public var revocationDateIOS: Double? - public var revocationReasonIOS: String? + public var quantityIOS: Int? = nil + public var reasonIOS: String? = nil + public var reasonStringRepresentationIOS: String? = nil + public var renewalInfoIOS: RenewalInfoIOS? = nil + public var revocationDateIOS: Double? = nil + public var revocationReasonIOS: String? = nil /// Store where purchase was made public var store: IapStore - public var storefrontCountryCodeIOS: String? - public var subscriptionGroupIdIOS: String? + public var storefrontCountryCodeIOS: String? = nil + public var subscriptionGroupIdIOS: String? = nil public var transactionDate: Double public var transactionId: String - public var transactionReasonIOS: String? - public var webOrderLineItemIdIOS: String? + public var transactionReasonIOS: String? = nil + public var webOrderLineItemIdIOS: String? = nil } public struct PurchaseOfferIOS: Codable { @@ -1030,38 +1031,38 @@ public struct PurchaseOfferIOS: Codable { } public struct RefundResultIOS: Codable { - public var message: String? + public var message: String? = nil public var status: String } /// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo /// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo public struct RenewalInfoIOS: Codable { - public var autoRenewPreference: String? + public var autoRenewPreference: String? = 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? + public var expirationReason: String? = nil /// Grace period expiration date (milliseconds since epoch) /// When set, subscription is in grace period (billing issue but still has access) - public var gracePeriodExpirationDate: Double? + public var gracePeriodExpirationDate: Double? = nil /// True if subscription failed to renew due to billing issue and is retrying /// Note: Not directly available in RenewalInfo, available in Status - public var isInBillingRetry: Bool? - public var jsonRepresentation: String? + public var isInBillingRetry: Bool? = nil + public var jsonRepresentation: String? = nil /// Product ID that will be used on next renewal (when user upgrades/downgrades) /// If set and different from current productId, subscription will change on expiration - public var pendingUpgradeProductId: String? + public var pendingUpgradeProductId: String? = nil /// User's response to subscription price increase /// Possible values: "AGREED", "PENDING", null (no price increase) - public var priceIncreaseStatus: String? + public var priceIncreaseStatus: String? = nil /// Expected renewal date (milliseconds since epoch) /// For active subscriptions, when the next renewal/charge will occur - public var renewalDate: Double? + public var renewalDate: Double? = nil /// Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) - public var renewalOfferId: String? + public var renewalOfferId: String? = nil /// Type of offer applied to next renewal /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. - public var renewalOfferType: String? + public var renewalOfferType: String? = nil public var willAutoRenew: Bool } @@ -1070,7 +1071,7 @@ public struct RenewalInfoIOS: Codable { public struct RentalDetailsAndroid: Codable { /// Rental expiration period in ISO 8601 format /// Time after rental period ends when user can still extend - public var rentalExpirationPeriod: String? + public var rentalExpirationPeriod: String? = nil /// Rental period in ISO 8601 format (e.g., P7D for 7 days) public var rentalPeriod: String } @@ -1089,8 +1090,8 @@ public struct RequestVerifyPurchaseWithIapkitResult: Codable { } public struct SubscriptionInfoIOS: Codable { - public var introductoryOffer: SubscriptionOfferIOS? - public var promotionalOffers: [SubscriptionOfferIOS]? + public var introductoryOffer: SubscriptionOfferIOS? = nil + public var promotionalOffers: [SubscriptionOfferIOS]? = nil public var subscriptionGroupId: String public var subscriptionPeriod: SubscriptionPeriodValueIOS } @@ -1107,9 +1108,9 @@ public struct SubscriptionInfoIOS: Codable { public struct SubscriptionOffer: Codable { /// [Android] Base plan identifier. /// Identifies which base plan this offer belongs to. - public var basePlanIdAndroid: String? + public var basePlanIdAndroid: String? = nil /// Currency code (ISO 4217, e.g., "USD") - public var currency: String? + public var currency: String? = nil /// Formatted display price string (e.g., "$9.99/month") public var displayPrice: String /// Unique identifier for the offer. @@ -1119,39 +1120,39 @@ public struct SubscriptionOffer: Codable { /// [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? + public var installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = nil /// [iOS] Key identifier for signature validation. /// Used with server-side signature generation for promotional offers. - public var keyIdentifierIOS: String? + public var keyIdentifierIOS: String? = nil /// [iOS] Localized price string. - public var localizedPriceIOS: String? + public var localizedPriceIOS: String? = nil /// [iOS] Cryptographic nonce (UUID) for signature validation. /// Must be generated server-side for each purchase attempt. - public var nonceIOS: String? + public var nonceIOS: String? = nil /// [iOS] Number of billing periods for this discount. - public var numberOfPeriodsIOS: Int? + public var numberOfPeriodsIOS: Int? = nil /// [Android] List of tags associated with this offer. - public var offerTagsAndroid: [String]? + public var offerTagsAndroid: [String]? = nil /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. - public var offerTokenAndroid: String? + public var offerTokenAndroid: String? = nil /// Payment mode during the offer period - public var paymentMode: PaymentMode? + public var paymentMode: PaymentMode? = nil /// Subscription period for this offer - public var period: SubscriptionPeriod? + public var period: SubscriptionPeriod? = nil /// Number of periods the offer applies - public var periodCount: Int? + public var periodCount: Int? = nil /// Numeric price value public var price: Double /// [Android] Pricing phases for this subscription offer. /// Contains detailed pricing information for each phase (trial, intro, regular). - public var pricingPhasesAndroid: PricingPhasesAndroid? + public var pricingPhasesAndroid: PricingPhasesAndroid? = nil /// [iOS] Server-generated signature for promotional offer validation. /// Required when applying promotional offers on iOS. - public var signatureIOS: String? + public var signatureIOS: String? = nil /// [iOS] Timestamp when the signature was generated. /// Used for signature validation. - public var timestampIOS: Double? + public var timestampIOS: Double? = nil /// Type of subscription offer (Introductory or Promotional) public var type: DiscountOfferType } @@ -1183,7 +1184,7 @@ public struct SubscriptionPeriodValueIOS: Codable { } public struct SubscriptionStatusIOS: Codable { - public var renewalInfo: RenewalInfoIOS? + public var renewalInfo: RenewalInfoIOS? = nil public var state: String } @@ -1208,10 +1209,10 @@ public struct ValidTimeWindowAndroid: Codable { public struct VerifyPurchaseResultAndroid: Codable { public var autoRenewing: Bool public var betaProduct: Bool - public var cancelDate: Double? - public var cancelReason: String? - public var deferredDate: Double? - public var deferredSku: String? + public var cancelDate: Double? = nil + public var cancelReason: String? = nil + public var deferredDate: Double? = nil + public var deferredSku: String? = nil public var freeTrialEndDate: Double public var gracePeriodEndDate: Double public var parentProductId: String @@ -1230,7 +1231,7 @@ public struct VerifyPurchaseResultAndroid: Codable { /// Returns verification status and grant time for the entitlement. public struct VerifyPurchaseResultHorizon: Codable { /// Unix timestamp (seconds) when the entitlement was granted. - public var grantTime: Double? + public var grantTime: Double? = nil /// Whether the entitlement verification succeeded. public var success: Bool } @@ -1241,21 +1242,21 @@ public struct VerifyPurchaseResultIOS: Codable { /// JWS representation public var jwsRepresentation: String /// Latest transaction if available - public var latestTransaction: Purchase? + public var latestTransaction: Purchase? = nil /// Receipt data string public var receiptData: String } public struct VerifyPurchaseWithProviderError: Codable { - public var code: String? + public var code: String? = nil public var message: String } public struct VerifyPurchaseWithProviderResult: Codable { /// Error details if verification failed - public var errors: [VerifyPurchaseWithProviderError]? + public var errors: [VerifyPurchaseWithProviderError]? = nil /// IAPKit verification result - public var iapkit: RequestVerifyPurchaseWithIapkitResult? + public var iapkit: RequestVerifyPurchaseWithIapkitResult? = nil public var provider: PurchaseVerificationProvider } diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 08748afb..9b9f7e78 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -107,7 +107,11 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { await productManager.addProduct(product) } } catch { - let purchaseError = makePurchaseError(code: .queryProduct, message: error.localizedDescription) + let purchaseError = makePurchaseError( + code: .queryProduct, + message: error.localizedDescription, + debugMessage: error.localizedDescription + ) emitPurchaseError(purchaseError) throw purchaseError } @@ -202,7 +206,12 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { await state.setPromotedProductId(nil) throw purchaseError } catch { - let wrapped = makePurchaseError(code: .queryProduct, productId: sku, message: error.localizedDescription) + let wrapped = makePurchaseError( + code: .queryProduct, + productId: sku, + message: error.localizedDescription, + debugMessage: error.localizedDescription + ) emitPurchaseError(wrapped) await state.setPromotedProductId(nil) throw wrapped @@ -390,7 +399,8 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { let purchaseError = makePurchaseError( code: .purchaseError, productId: sku, - message: enhancedMessage + message: enhancedMessage, + debugMessage: error.localizedDescription ) emitPurchaseError(purchaseError) throw purchaseError @@ -1493,9 +1503,15 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } } - private func makePurchaseError(code: ErrorCode, productId: String? = nil, message: String? = nil) -> PurchaseError { + private func makePurchaseError( + code: ErrorCode, + productId: String? = nil, + message: String? = nil, + debugMessage: String? = nil + ) -> PurchaseError { PurchaseError( code: code, + debugMessage: debugMessage, message: message ?? defaultMessage(for: code), productId: productId ) diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift index 419da41f..9548fad7 100644 --- a/packages/apple/Tests/OpenIapTests.swift +++ b/packages/apple/Tests/OpenIapTests.swift @@ -23,6 +23,22 @@ final class OpenIapTests: XCTestCase { XCTAssertEqual(error.code, .skuNotFound) XCTAssertEqual(error.message, "Not found") XCTAssertEqual(error.productId, "sku") + XCTAssertNil(error.debugMessage) + } + + func testPurchaseErrorCarriesDebugMessage() throws { + let error = PurchaseError( + code: .purchaseError, + debugMessage: "StoreKitError.notEntitled — product not in entitlements", + message: "Purchase error", + productId: "sku" + ) + XCTAssertEqual(error.debugMessage, "StoreKitError.notEntitled — product not in entitlements") + + let data = try JSONEncoder().encode(error) + let decoded = try JSONDecoder().decode(PurchaseError.self, from: data) + XCTAssertEqual(decoded.debugMessage, error.debugMessage) + XCTAssertEqual(decoded.code, .purchaseError) } func testProductRequestEncoding() throws { diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 221a840d..539e5a98 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -36,8 +36,8 @@ function Releases() { id="subscription-replacement-and-debug-message-2026-04-15" level="h4" > - Subscription Replacement Wiring & Billing Debug Messages - April - 15, 2026 + Subscription Replacement Wiring & Billing Debug Messages - April 15, + 2026

Two connected Android fixes. First, the newer per-product - subscription replacement path now actually reaches the native - layer on flutter_inapp_purchase. Second, Google Play's raw{' '} + subscription replacement path now actually reaches the native layer + on flutter_inapp_purchase. Second, Google Play's raw{' '} BillingResult.debugMessage is now forwarded through{' '} - PurchaseError, so callers can read the specific - reason Play rejected a replacement flow instead of just seeing a - generic "Invalid arguments". + PurchaseError, so callers can read the specific reason + Play rejected a replacement flow instead of just seeing a generic + "Invalid arguments".

@@ -77,17 +77,17 @@ function Releases() { {' '} — both errors are now data classes instead of singletons and accept an optional debugMessage: String?.{' '} - fromBillingResponseCode forwards Google - Play's raw BillingResult.debugMessage into - the error instance, and OpenIapError.toJSON(){' '} - emits a debugMessage key so downstream framework - libraries can surface the reason Play rejected a purchase - (offer token mismatch, subscription group conflict, etc.). - The launchBillingFlow sync-failure path now also + fromBillingResponseCode forwards Google Play's + raw BillingResult.debugMessage into the error + instance, and OpenIapError.toJSON() emits a{' '} + debugMessage key so downstream framework libraries + can surface the reason Play rejected a purchase (offer token + mismatch, subscription group conflict, etc.). The{' '} + launchBillingFlow sync-failure path now also produces DeveloperError (matching the{' '} - onPurchasesUpdated async path) instead of a - generic PurchaseFailed for{' '} - DEVELOPER_ERROR response codes. + onPurchasesUpdated async path) instead of a generic{' '} + PurchaseFailed for DEVELOPER_ERROR{' '} + response codes.

  • - Fix: forward{' '} - subscriptionProductReplacementParams on Android + Fix: forward subscriptionProductReplacementParams{' '} + on Android {' '} — the field was declared on{' '} RequestSubscriptionAndroidProps and parsed correctly by the native plugin, but{' '} flutter_inapp_purchase.dart was dropping it when - building the method-channel payload, so the native side - received null and Google Play applied its default - replacement mode (WITHOUT_PRORATION) regardless - of what callers passed from Dart. The Billing Library 8.1.0+ + building the method-channel payload, so the native side received{' '} + null and Google Play applied its default + replacement mode (WITHOUT_PRORATION) regardless of + what callers passed from Dart. The Billing Library 8.1.0+ per-product replacement path now works end-to-end. ( #97)
  • - Channel test added to assert that{' '} - oldProductId and replacementMode{' '} - reach the native requestPurchase call, so the - wiring can't silently regress again. + Channel test added to assert that oldProductId and{' '} + replacementMode reach the native{' '} + requestPurchase call, so the wiring can't + silently regress again.
  • @@ -168,16 +168,14 @@ function Releases() { >
  • - Fix: surface Google Play's{' '} - debugMessage through{' '} - PurchaseError + Fix: surface Google Play's debugMessage{' '} + through PurchaseError {' '} — convertToPurchaseError was only forwarding{' '} - code and message from the native - error payload, so the raw{' '} - BillingResult.debugMessage and{' '} - responseCode were being dropped. Combined with - the openiap-google 1.3.32 change, Dart callers inspecting{' '} + code and message from the native error + payload, so the raw BillingResult.debugMessage and{' '} + responseCode were being dropped. Combined with the + openiap-google 1.3.32 change, Dart callers inspecting{' '} PurchaseError.debugMessage now see Play's exact rejection reason — useful for diagnosing{' '} DEVELOPER_ERROR surfaces such as{' '} @@ -185,8 +183,8 @@ function Releases() { attach adb.
  • - Picks up openiap-google 1.3.32 (debug message + data class - error types). + Picks up openiap-google 1.3.32 (debug message + data class error + types).
  • diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt index f5269aef..f1e17bec 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt @@ -8,17 +8,17 @@ import com.meta.horizon.billingclient.api.BillingClient @Suppress("DEPRECATION") fun OpenIapError.Companion.fromBillingResponseCode(responseCode: Int, debugMessage: String? = null): OpenIapError { return when (responseCode) { - BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled - BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> OpenIapError.ServiceUnavailable - BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> OpenIapError.BillingUnavailable - BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> OpenIapError.ItemUnavailable + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled(debugMessage) + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> OpenIapError.ServiceUnavailable(debugMessage) + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> OpenIapError.BillingUnavailable(debugMessage) + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> OpenIapError.ItemUnavailable(debugMessage) BillingClient.BillingResponseCode.DEVELOPER_ERROR -> OpenIapError.DeveloperError(debugMessage) - BillingClient.BillingResponseCode.ERROR -> OpenIapError.BillingError - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> OpenIapError.ItemAlreadyOwned - BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> OpenIapError.ItemNotOwned - BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> OpenIapError.ServiceDisconnected - BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> OpenIapError.FeatureNotSupported - BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> OpenIapError.ServiceTimeout - else -> OpenIapError.UnknownError + BillingClient.BillingResponseCode.ERROR -> OpenIapError.BillingError(debugMessage) + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> OpenIapError.ItemAlreadyOwned(debugMessage) + BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> OpenIapError.ItemNotOwned(debugMessage) + BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> OpenIapError.ServiceDisconnected(debugMessage) + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> OpenIapError.FeatureNotSupported(debugMessage) + BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> OpenIapError.ServiceTimeout(debugMessage) + else -> OpenIapError.UnknownError(debugMessage) } } diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index 95957c26..b5183044 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -514,7 +514,7 @@ class OpenIapModule( OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) OpenIapError.DeveloperError(result.debugMessage) } - BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled() else -> OpenIapError.PurchaseFailed(result.debugMessage) } purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } } @@ -715,7 +715,7 @@ class OpenIapModule( override val verifyPurchaseWithProvider: MutationVerifyPurchaseWithProviderHandler = { props -> if (props.provider != PurchaseVerificationProvider.Iapkit) { - throw OpenIapError.FeatureNotSupported + throw OpenIapError.FeatureNotSupported() } val options = props.iapkit ?: throw OpenIapError.DeveloperError() VerifyPurchaseWithProviderResult( diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt index c42fcbd2..336e67f7 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt @@ -33,7 +33,7 @@ sealed class OpenIapError : Exception() { } } - class PurchaseFailed(override val debugMessage: String? = null) : OpenIapError() { + data class PurchaseFailed(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE @@ -67,12 +67,14 @@ sealed class OpenIapError : Exception() { const val MESSAGE = "Payment not allowed" } - object BillingError : OpenIapError() { - val CODE = ErrorCode.ServiceError.rawValue - override val code = CODE - override val message = MESSAGE + data class BillingError(override val debugMessage: String? = null) : OpenIapError() { + override val code: String = CODE + override val message: String = MESSAGE - const val MESSAGE = "Billing error" + companion object { + val CODE = ErrorCode.ServiceError.rawValue + const val MESSAGE = "Billing error" + } } /** @@ -133,12 +135,14 @@ sealed class OpenIapError : Exception() { const val MESSAGE = "Restore failed" } - object UnknownError : OpenIapError() { - val CODE = ErrorCode.Unknown.rawValue - override val code = CODE - override val message = MESSAGE + data class UnknownError(override val debugMessage: String? = null) : OpenIapError() { + override val code: String = CODE + override val message: String = MESSAGE - const val MESSAGE = "Unknown error" + companion object { + val CODE = ErrorCode.Unknown.rawValue + const val MESSAGE = "Unknown error" + } } object NotPrepared : OpenIapError() { @@ -197,53 +201,67 @@ sealed class OpenIapError : Exception() { const val MESSAGE = "Current activity is not available" } - object UserCancelled : OpenIapError() { - val CODE = ErrorCode.UserCancelled.rawValue - const val MESSAGE = "User cancelled the operation" + data class UserCancelled(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE + + companion object { + val CODE = ErrorCode.UserCancelled.rawValue + const val MESSAGE = "User cancelled the operation" + } } - object ItemAlreadyOwned : OpenIapError() { - val CODE = ErrorCode.AlreadyOwned.rawValue + data class ItemAlreadyOwned(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "Item is already owned" + companion object { + val CODE = ErrorCode.AlreadyOwned.rawValue + const val MESSAGE = "Item is already owned" + } } - object ItemNotOwned : OpenIapError() { - val CODE = ErrorCode.ItemNotOwned.rawValue - const val MESSAGE = "Item is not owned" + data class ItemNotOwned(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE + + companion object { + val CODE = ErrorCode.ItemNotOwned.rawValue + const val MESSAGE = "Item is not owned" + } } - object ServiceUnavailable : OpenIapError() { - val CODE = ErrorCode.ServiceError.rawValue + data class ServiceUnavailable(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "Billing service is unavailable" + companion object { + val CODE = ErrorCode.ServiceError.rawValue + const val MESSAGE = "Billing service is unavailable" + } } - object BillingUnavailable : OpenIapError() { - val CODE = ErrorCode.BillingUnavailable.rawValue + data class BillingUnavailable(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "Billing API version is not supported" + companion object { + val CODE = ErrorCode.BillingUnavailable.rawValue + const val MESSAGE = "Billing API version is not supported" + } } - object ItemUnavailable : OpenIapError() { - val CODE = ErrorCode.ItemUnavailable.rawValue + data class ItemUnavailable(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "Requested product is not available for purchase" + companion object { + val CODE = ErrorCode.ItemUnavailable.rawValue + const val MESSAGE = "Requested product is not available for purchase" + } } - class DeveloperError(override val debugMessage: String? = null) : OpenIapError() { + data class DeveloperError(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE @@ -253,28 +271,34 @@ sealed class OpenIapError : Exception() { } } - object FeatureNotSupported : OpenIapError() { - val CODE = ErrorCode.FeatureNotSupported.rawValue + data class FeatureNotSupported(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "Requested feature is not supported by Play Store" + companion object { + val CODE = ErrorCode.FeatureNotSupported.rawValue + const val MESSAGE = "Requested feature is not supported by Play Store" + } } - object ServiceDisconnected : OpenIapError() { - val CODE = ErrorCode.ServiceDisconnected.rawValue + data class ServiceDisconnected(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "Play Store service is not connected" + companion object { + val CODE = ErrorCode.ServiceDisconnected.rawValue + const val MESSAGE = "Play Store service is not connected" + } } - object ServiceTimeout : OpenIapError() { - const val CODE = "service-timeout" + data class ServiceTimeout(override val debugMessage: String? = null) : OpenIapError() { override val code: String = CODE override val message: String = MESSAGE - const val MESSAGE = "The request has reached the maximum timeout before billing service responds" + companion object { + const val CODE = "service-timeout" + const val MESSAGE = "The request has reached the maximum timeout before billing service responds" + } } class AlternativeBillingUnavailable(val details: String) : OpenIapError() { 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 61087f3b..8ea24d80 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 @@ -2527,6 +2527,7 @@ public data class PurchaseAndroid( public data class PurchaseError( val code: ErrorCode, + val debugMessage: String? = null, val message: String, val productId: String? = null ) { @@ -2535,6 +2536,7 @@ public data class PurchaseError( fun fromJson(json: Map): PurchaseError { return PurchaseError( code = (json["code"] as? String)?.let { ErrorCode.fromJson(it) } ?: ErrorCode.Unknown, + debugMessage = json["debugMessage"] as? String, message = json["message"] as? String ?: "", productId = json["productId"] as? String, ) @@ -2544,6 +2546,7 @@ public data class PurchaseError( fun toJson(): Map = mapOf( "__typename" to "PurchaseError", "code" to code.toJson(), + "debugMessage" to debugMessage, "message" to message, "productId" to productId, ) diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt index 35258f9b..fb7ebfb5 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt @@ -376,7 +376,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { try { module.mutationHandlers.requestPurchase?.invoke(props) - ?: throw OpenIapError.FeatureNotSupported + ?: throw OpenIapError.FeatureNotSupported() } finally { if (skuForStatus != null) removePurchasing(skuForStatus) } diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt index 95ff1786..45727f93 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt @@ -8,17 +8,17 @@ import com.android.billingclient.api.BillingClient @Suppress("DEPRECATION") fun OpenIapError.Companion.fromBillingResponseCode(responseCode: Int, debugMessage: String? = null): OpenIapError { return when (responseCode) { - BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled - BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> OpenIapError.ServiceUnavailable - BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> OpenIapError.BillingUnavailable - BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> OpenIapError.ItemUnavailable + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled(debugMessage) + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> OpenIapError.ServiceUnavailable(debugMessage) + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> OpenIapError.BillingUnavailable(debugMessage) + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> OpenIapError.ItemUnavailable(debugMessage) BillingClient.BillingResponseCode.DEVELOPER_ERROR -> OpenIapError.DeveloperError(debugMessage) - BillingClient.BillingResponseCode.ERROR -> OpenIapError.BillingError - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> OpenIapError.ItemAlreadyOwned - BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> OpenIapError.ItemNotOwned - BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> OpenIapError.ServiceDisconnected - BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> OpenIapError.FeatureNotSupported - BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> OpenIapError.ServiceTimeout - else -> OpenIapError.UnknownError + BillingClient.BillingResponseCode.ERROR -> OpenIapError.BillingError(debugMessage) + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> OpenIapError.ItemAlreadyOwned(debugMessage) + BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> OpenIapError.ItemNotOwned(debugMessage) + BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> OpenIapError.ServiceDisconnected(debugMessage) + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> OpenIapError.FeatureNotSupported(debugMessage) + BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> OpenIapError.ServiceTimeout(debugMessage) + else -> OpenIapError.UnknownError(debugMessage) } } diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt index b78a7aa0..2c0c0d50 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt @@ -555,15 +555,21 @@ class OpenIapModule( externalTransactionToken = token )) } else if (continuation.isActive) { - continuation.resumeWithException(OpenIapError.PurchaseFailed()) + continuation.resumeWithException( + OpenIapError.PurchaseFailed("Missing external transaction token") + ) } } catch (e: Exception) { OpenIapLog.e("Failed to extract token: ${e.message}", e, TAG) - if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed()) + if (continuation.isActive) continuation.resumeWithException( + OpenIapError.PurchaseFailed(e.message) + ) } } else { OpenIapLog.e("Reporting details creation failed: ${result?.debugMessage}", tag = TAG) - if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed()) + if (continuation.isActive) continuation.resumeWithException( + OpenIapError.PurchaseFailed(result?.debugMessage) + ) } } null @@ -593,13 +599,13 @@ class OpenIapModule( method.invoke(client, reportingParams, listener) } catch (e: NoSuchMethodException) { OpenIapLog.e("createBillingProgramReportingDetailsAsync not found. Requires Billing Library 8.3.0+", e, TAG) - throw OpenIapError.FeatureNotSupported + throw OpenIapError.FeatureNotSupported() } catch (e: ClassNotFoundException) { OpenIapLog.e("BillingProgramReportingDetailsParams not found. Requires Billing Library 8.3.0+", e, TAG) - throw OpenIapError.FeatureNotSupported + throw OpenIapError.FeatureNotSupported() } catch (e: Exception) { OpenIapLog.e("Failed to create billing program reporting details: ${e.message}", e, TAG) - throw OpenIapError.PurchaseFailed() + throw OpenIapError.PurchaseFailed(e.message) } } } @@ -767,7 +773,7 @@ class OpenIapModule( @Suppress("DEPRECATION") val dialogSuccess = showAlternativeBillingInformationDialog(activity) if (!dialogSuccess) { - val err = OpenIapError.UserCancelled + val err = OpenIapError.UserCancelled() for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() } @@ -831,7 +837,7 @@ class OpenIapModule( } } catch (e: Exception) { OpenIapLog.e("Alternative billing only flow failed: ${e.message}", e, TAG) - val err = OpenIapError.FeatureNotSupported + val err = OpenIapError.FeatureNotSupported() for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() } @@ -1046,7 +1052,7 @@ class OpenIapModule( OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) OpenIapError.DeveloperError(result.debugMessage) } - BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled() else -> OpenIapError.PurchaseFailed(result.debugMessage) } for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } @@ -1113,7 +1119,7 @@ class OpenIapModule( if (!client.isReady) throw OpenIapError.NotPrepared val token = purchase.purchaseToken.orEmpty() if (token.isBlank()) { - throw OpenIapError.PurchaseFailed() + throw OpenIapError.PurchaseFailed("Missing purchase token on purchase") } val result = if (isConsumable == true) { @@ -1133,7 +1139,7 @@ class OpenIapModule( } if (result.responseCode != BillingClient.BillingResponseCode.OK) { - throw OpenIapError.PurchaseFailed() + throw OpenIapError.PurchaseFailed(result.debugMessage) } } } @@ -1201,7 +1207,7 @@ class OpenIapModule( override val verifyPurchaseWithProvider: MutationVerifyPurchaseWithProviderHandler = { props -> if (props.provider != PurchaseVerificationProvider.Iapkit) { - throw OpenIapError.FeatureNotSupported + throw OpenIapError.FeatureNotSupported() } val options = props.iapkit ?: throw OpenIapError.DeveloperError() VerifyPurchaseWithProviderResult( @@ -1359,7 +1365,7 @@ class OpenIapModule( } else { when (billingResult.responseCode) { BillingClient.BillingResponseCode.USER_CANCELED -> { - val err = OpenIapError.UserCancelled + val err = OpenIapError.UserCancelled() for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) } diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt index 7e3fbe51..21b0332f 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt @@ -52,7 +52,7 @@ class OpenIapErrorTest { @Test fun `BillingError has correct code and message`() { - val error = OpenIapError.BillingError + val error = OpenIapError.BillingError() assertEquals(ErrorCode.ServiceError.rawValue, error.code) assertEquals("Billing error", error.message) } @@ -88,7 +88,7 @@ class OpenIapErrorTest { @Test fun `UnknownError has correct code and message`() { - val error = OpenIapError.UnknownError + val error = OpenIapError.UnknownError() assertEquals(ErrorCode.Unknown.rawValue, error.code) assertEquals("Unknown error", error.message) } @@ -145,42 +145,42 @@ class OpenIapErrorTest { @Test fun `UserCancelled has correct code and message`() { - val error = OpenIapError.UserCancelled + val error = OpenIapError.UserCancelled() assertEquals(ErrorCode.UserCancelled.rawValue, error.code) assertEquals("User cancelled the operation", error.message) } @Test fun `ItemAlreadyOwned has correct code and message`() { - val error = OpenIapError.ItemAlreadyOwned + val error = OpenIapError.ItemAlreadyOwned() assertEquals(ErrorCode.AlreadyOwned.rawValue, error.code) assertEquals("Item is already owned", error.message) } @Test fun `ItemNotOwned has correct code and message`() { - val error = OpenIapError.ItemNotOwned + val error = OpenIapError.ItemNotOwned() assertEquals(ErrorCode.ItemNotOwned.rawValue, error.code) assertEquals("Item is not owned", error.message) } @Test fun `ServiceUnavailable has correct code and message`() { - val error = OpenIapError.ServiceUnavailable + val error = OpenIapError.ServiceUnavailable() assertEquals(ErrorCode.ServiceError.rawValue, error.code) assertEquals("Billing service is unavailable", error.message) } @Test fun `BillingUnavailable has correct code and message`() { - val error = OpenIapError.BillingUnavailable + val error = OpenIapError.BillingUnavailable() assertEquals(ErrorCode.BillingUnavailable.rawValue, error.code) assertEquals("Billing API version is not supported", error.message) } @Test fun `ItemUnavailable has correct code and message`() { - val error = OpenIapError.ItemUnavailable + val error = OpenIapError.ItemUnavailable() assertEquals(ErrorCode.ItemUnavailable.rawValue, error.code) assertEquals("Requested product is not available for purchase", error.message) } @@ -202,21 +202,21 @@ class OpenIapErrorTest { @Test fun `FeatureNotSupported has correct code and message`() { - val error = OpenIapError.FeatureNotSupported + val error = OpenIapError.FeatureNotSupported() assertEquals(ErrorCode.FeatureNotSupported.rawValue, error.code) assertEquals("Requested feature is not supported by Play Store", error.message) } @Test fun `ServiceDisconnected has correct code and message`() { - val error = OpenIapError.ServiceDisconnected + val error = OpenIapError.ServiceDisconnected() assertEquals("service-disconnected", error.code) assertEquals("Play Store service is not connected", error.message) } @Test fun `ServiceTimeout has correct code and message`() { - val error = OpenIapError.ServiceTimeout + val error = OpenIapError.ServiceTimeout() assertEquals("service-timeout", error.code) assertEquals("The request has reached the maximum timeout before billing service responds", error.message) } @@ -230,12 +230,12 @@ class OpenIapErrorTest { OpenIapError.PurchaseCancelled, OpenIapError.PurchaseDeferred, OpenIapError.PaymentNotAllowed, - OpenIapError.BillingError, + OpenIapError.BillingError(), OpenIapError.InvalidReceipt, OpenIapError.NetworkError, OpenIapError.VerificationFailed, OpenIapError.RestoreFailed, - OpenIapError.UnknownError, + OpenIapError.UnknownError(), OpenIapError.NotPrepared, OpenIapError.InitConnection, OpenIapError.QueryProduct, @@ -243,16 +243,16 @@ class OpenIapErrorTest { OpenIapError.SkuNotFound("test"), OpenIapError.SkuOfferMismatch, OpenIapError.MissingCurrentActivity, - OpenIapError.UserCancelled, - OpenIapError.ItemAlreadyOwned, - OpenIapError.ItemNotOwned, - OpenIapError.ServiceUnavailable, - OpenIapError.BillingUnavailable, - OpenIapError.ItemUnavailable, + OpenIapError.UserCancelled(), + OpenIapError.ItemAlreadyOwned(), + OpenIapError.ItemNotOwned(), + OpenIapError.ServiceUnavailable(), + OpenIapError.BillingUnavailable(), + OpenIapError.ItemUnavailable(), OpenIapError.DeveloperError(), - OpenIapError.FeatureNotSupported, - OpenIapError.ServiceDisconnected, - OpenIapError.ServiceTimeout + OpenIapError.FeatureNotSupported(), + OpenIapError.ServiceDisconnected(), + OpenIapError.ServiceTimeout() ) errors.forEach { error -> @@ -285,6 +285,30 @@ class OpenIapErrorTest { assertTrue(unknownError is OpenIapError.UnknownError) } + @Test + fun `fromBillingResponseCode forwards debugMessage for every response code`() { + val debug = "offerToken does not match any product details" + val codesToAssert = listOf( + BillingClient.BillingResponseCode.USER_CANCELED, + BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE, + BillingClient.BillingResponseCode.BILLING_UNAVAILABLE, + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE, + BillingClient.BillingResponseCode.DEVELOPER_ERROR, + BillingClient.BillingResponseCode.ERROR, + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED, + BillingClient.BillingResponseCode.ITEM_NOT_OWNED, + BillingClient.BillingResponseCode.SERVICE_DISCONNECTED, + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED, + BillingClient.BillingResponseCode.SERVICE_TIMEOUT, + 999 // else branch → UnknownError + ) + codesToAssert.forEach { code -> + val err = OpenIapError.fromBillingResponseCode(code, debug) + assertEquals("debugMessage missing for response code $code", debug, err.debugMessage) + assertEquals(debug, err.toJSON()["debugMessage"]) + } + } + @Test @Suppress("DEPRECATION") fun `getAllErrorCodes returns all error codes and messages`() { @@ -337,12 +361,12 @@ class OpenIapErrorTest { OpenIapError.PurchaseCancelled to ErrorCode.UserCancelled.rawValue, OpenIapError.PurchaseDeferred to ErrorCode.DeferredPayment.rawValue, OpenIapError.PaymentNotAllowed to ErrorCode.UserError.rawValue, - OpenIapError.BillingError to ErrorCode.ServiceError.rawValue, + OpenIapError.BillingError() to ErrorCode.ServiceError.rawValue, @Suppress("DEPRECATION") OpenIapError.InvalidReceipt to ErrorCode.PurchaseVerificationFailed.rawValue, OpenIapError.NetworkError to ErrorCode.NetworkError.rawValue, OpenIapError.VerificationFailed to ErrorCode.TransactionValidationFailed.rawValue, OpenIapError.RestoreFailed to ErrorCode.SyncError.rawValue, - OpenIapError.UnknownError to ErrorCode.Unknown.rawValue, + OpenIapError.UnknownError() to ErrorCode.Unknown.rawValue, OpenIapError.NotPrepared to ErrorCode.NotPrepared.rawValue, OpenIapError.InitConnection to ErrorCode.InitConnection.rawValue, OpenIapError.QueryProduct to ErrorCode.QueryProduct.rawValue, @@ -350,16 +374,16 @@ class OpenIapErrorTest { OpenIapError.SkuNotFound("test") to ErrorCode.SkuNotFound.rawValue, OpenIapError.SkuOfferMismatch to ErrorCode.SkuOfferMismatch.rawValue, OpenIapError.MissingCurrentActivity to ErrorCode.ActivityUnavailable.rawValue, - OpenIapError.UserCancelled to ErrorCode.UserCancelled.rawValue, - OpenIapError.ItemAlreadyOwned to ErrorCode.AlreadyOwned.rawValue, - OpenIapError.ItemNotOwned to ErrorCode.ItemNotOwned.rawValue, - OpenIapError.ServiceUnavailable to ErrorCode.ServiceError.rawValue, - OpenIapError.BillingUnavailable to ErrorCode.BillingUnavailable.rawValue, - OpenIapError.ItemUnavailable to ErrorCode.ItemUnavailable.rawValue, + OpenIapError.UserCancelled() to ErrorCode.UserCancelled.rawValue, + OpenIapError.ItemAlreadyOwned() to ErrorCode.AlreadyOwned.rawValue, + OpenIapError.ItemNotOwned() to ErrorCode.ItemNotOwned.rawValue, + OpenIapError.ServiceUnavailable() to ErrorCode.ServiceError.rawValue, + OpenIapError.BillingUnavailable() to ErrorCode.BillingUnavailable.rawValue, + OpenIapError.ItemUnavailable() to ErrorCode.ItemUnavailable.rawValue, OpenIapError.DeveloperError() to ErrorCode.DeveloperError.rawValue, - OpenIapError.FeatureNotSupported to ErrorCode.FeatureNotSupported.rawValue, - OpenIapError.ServiceDisconnected to ErrorCode.ServiceDisconnected.rawValue, - OpenIapError.ServiceTimeout to "service-timeout" + OpenIapError.FeatureNotSupported() to ErrorCode.FeatureNotSupported.rawValue, + OpenIapError.ServiceDisconnected() to ErrorCode.ServiceDisconnected.rawValue, + OpenIapError.ServiceTimeout() to "service-timeout" ) errors.forEach { (error, expectedCode) -> diff --git a/packages/gql/codegen/plugins/swift.ts b/packages/gql/codegen/plugins/swift.ts index fb462796..9b39f487 100644 --- a/packages/gql/codegen/plugins/swift.ts +++ b/packages/gql/codegen/plugins/swift.ts @@ -205,6 +205,12 @@ export class SwiftPlugin extends CodegenPlugin { defaultValue = ` = .${defaults.platform}`; } else if (defaults && field.name === 'type') { defaultValue = ` = .${defaults.type === 'in-app' ? 'inApp' : 'subs'}`; + } else if (field.type.nullable) { + // Default nullable properties to nil so the synthesized memberwise + // initializer can omit them — existing call sites that construct + // objects without every optional keep compiling when new nullable + // fields are added. + defaultValue = ' = nil'; } this.emit(` public var ${propertyName}: ${propertyType}${defaultValue}`); diff --git a/packages/gql/src/error.graphql b/packages/gql/src/error.graphql index bcaa0c6c..4c28de52 100644 --- a/packages/gql/src/error.graphql +++ b/packages/gql/src/error.graphql @@ -50,4 +50,8 @@ type PurchaseError { code: ErrorCode! message: String! productId: String + # Raw diagnostic text from the underlying billing layer, when available. + # On Android this mirrors BillingResult.debugMessage; on iOS this is the + # StoreKit error's localizedDescription (or equivalent). May be null. + debugMessage: String } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 3bb33c29..4ebcaf95 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -2614,6 +2614,7 @@ public data class PurchaseAndroid( public data class PurchaseError( val code: ErrorCode, + val debugMessage: String? = null, val message: String, val productId: String? = null ) { @@ -2622,6 +2623,7 @@ public data class PurchaseError( fun fromJson(json: Map): PurchaseError { return PurchaseError( code = (json["code"] as? String)?.let { ErrorCode.fromJson(it) } ?: ErrorCode.Unknown, + debugMessage = json["debugMessage"] as? String, message = json["message"] as? String ?: "", productId = json["productId"] as? String, ) @@ -2631,6 +2633,7 @@ public data class PurchaseError( fun toJson(): Map = mapOf( "__typename" to "PurchaseError", "code" to code.toJson(), + "debugMessage" to debugMessage, "message" to message, "productId" to productId, ) diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 9fa68ab6..d639053f 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -454,35 +454,35 @@ public protocol PurchaseCommon: Codable { // MARK: - Objects public struct ActiveSubscription: Codable { - public var autoRenewingAndroid: Bool? - public var basePlanIdAndroid: String? + public var autoRenewingAndroid: Bool? = nil + public var basePlanIdAndroid: String? = nil /// The current plan identifier. This is: /// - On Android: the basePlanId (e.g., "premium", "premium-year") /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") /// This provides a unified way to identify which specific plan/tier the user is subscribed to. - public var currentPlanId: String? - public var daysUntilExpirationIOS: Double? - public var environmentIOS: String? - public var expirationDateIOS: Double? + public var currentPlanId: String? = nil + public var daysUntilExpirationIOS: Double? = nil + public var environmentIOS: String? = nil + public var expirationDateIOS: Double? = nil public var isActive: Bool public var productId: String - public var purchaseToken: String? + public var purchaseToken: String? = nil /// Required for subscription upgrade/downgrade on Android - public var purchaseTokenAndroid: String? + public var purchaseTokenAndroid: String? = nil /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, /// pending upgrades/downgrades, and auto-renewal preferences. - public var renewalInfoIOS: RenewalInfoIOS? + public var renewalInfoIOS: RenewalInfoIOS? = nil public var transactionDate: Double public var transactionId: String /// @deprecated iOS only - use daysUntilExpirationIOS instead. /// Whether the subscription will expire soon (within 7 days). /// Consider using daysUntilExpirationIOS for more precise control. - public var willExpireSoon: Bool? + public var willExpireSoon: Bool? = nil } public struct AppTransaction: Codable { public var appId: Double - public var appTransactionId: String? + public var appTransactionId: String? = nil public var appVersion: String public var appVersionId: Double public var bundleId: String @@ -490,9 +490,9 @@ public struct AppTransaction: Codable { public var deviceVerificationNonce: String public var environment: String public var originalAppVersion: String - public var originalPlatform: String? + public var originalPlatform: String? = nil public var originalPurchaseDate: Double - public var preorderDate: Double? + public var preorderDate: Double? = nil public var signedDate: Double } @@ -520,12 +520,12 @@ public struct BillingProgramReportingDetailsAndroid: Codable { /// Available in Google Play Billing Library 8.0.0+ public struct BillingResultAndroid: Codable { /// Debug message from the billing library - public var debugMessage: String? + public var debugMessage: String? = nil /// The response code from the billing operation public var responseCode: Int /// Sub-response code for more granular error information (8.0+). /// Provides additional context when responseCode indicates an error. - public var subResponseCode: SubResponseCodeAndroid? + public var subResponseCode: SubResponseCodeAndroid? = nil } /// Details provided when user selects developer billing option (Android) @@ -552,10 +552,10 @@ public struct DiscountAmountAndroid: Codable { public struct DiscountDisplayInfoAndroid: Codable { /// Absolute discount amount details /// Only returned for fixed amount discounts - public var discountAmount: DiscountAmountAndroid? + public var discountAmount: DiscountAmountAndroid? = nil /// Percentage discount (e.g., 33 for 33% off) /// Only returned for percentage-based discounts - public var percentageDiscount: Int? + public var percentageDiscount: Int? = nil } /// Discount information returned from the store. @@ -563,7 +563,7 @@ public struct DiscountDisplayInfoAndroid: Codable { /// @see https://openiap.dev/docs/types#subscription-offer public struct DiscountIOS: Codable { public var identifier: String - public var localizedPrice: String? + public var localizedPrice: String? = nil public var numberOfPeriods: Int public var paymentMode: PaymentModeIOS public var price: String @@ -584,46 +584,46 @@ public struct DiscountOffer: Codable { public var currency: String /// [Android] Fixed discount amount in micro-units. /// Only present for fixed amount discounts. - public var discountAmountMicrosAndroid: String? + public var discountAmountMicrosAndroid: String? = nil /// Formatted display price string (e.g., "$4.99") public var displayPrice: String /// [Android] Formatted discount amount string (e.g., "$5.00 OFF"). - public var formattedDiscountAmountAndroid: String? + public var formattedDiscountAmountAndroid: String? = nil /// [Android] Original full price in micro-units before discount. /// Divide by 1,000,000 to get the actual price. /// Use for displaying strikethrough original price. - public var fullPriceMicrosAndroid: String? + public var fullPriceMicrosAndroid: String? = nil /// Unique identifier for the offer. /// - iOS: Not applicable (one-time discounts not supported) /// - Android: offerId from ProductAndroidOneTimePurchaseOfferDetail - public var id: String? + public var id: String? = nil /// [Android] Limited quantity information. /// Contains maximumQuantity and remainingQuantity. - public var limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? + public var limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? = nil /// [Android] List of tags associated with this offer. - public var offerTagsAndroid: [String]? + public var offerTagsAndroid: [String]? = nil /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. - public var offerTokenAndroid: String? + public var offerTokenAndroid: String? = nil /// [Android] Percentage discount (e.g., 33 for 33% off). /// Only present for percentage-based discounts. - public var percentageDiscountAndroid: Int? + public var percentageDiscountAndroid: Int? = nil /// [Android] Pre-order details if this is a pre-order offer. /// Available in Google Play Billing Library 8.1.0+ - public var preorderDetailsAndroid: PreorderDetailsAndroid? + public var preorderDetailsAndroid: PreorderDetailsAndroid? = nil /// 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? + public var purchaseOptionIdAndroid: String? = nil /// [Android] Rental details if this is a rental offer. - public var rentalDetailsAndroid: RentalDetailsAndroid? + public var rentalDetailsAndroid: RentalDetailsAndroid? = nil /// Type of discount offer public var type: DiscountOfferType /// [Android] Valid time window for the offer. /// Contains startTimeMillis and endTimeMillis. - public var validTimeWindowAndroid: ValidTimeWindowAndroid? + public var validTimeWindowAndroid: ValidTimeWindowAndroid? = nil } /// iOS DiscountOffer (output type). @@ -669,22 +669,22 @@ public struct ExternalPurchaseCustomLinkNoticeResultIOS: Codable { /// Whether the user chose to continue to external purchase public var continued: Bool /// Optional error message if the presentation failed - public var error: String? + public var error: String? = nil } /// Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). public struct ExternalPurchaseCustomLinkTokenResultIOS: Codable { /// Optional error message if token retrieval failed - public var error: String? + public var error: String? = nil /// The external purchase token string. /// Report this token to Apple's External Purchase Server API. - public var token: String? + public var token: String? = nil } /// Result of presenting an external purchase link public struct ExternalPurchaseLinkResultIOS: Codable { /// Optional error message if the presentation failed - public var error: String? + public var error: String? = nil /// Whether the user completed the external purchase flow public var success: Bool } @@ -693,11 +693,11 @@ public struct ExternalPurchaseLinkResultIOS: Codable { /// Returns the token when user continues to external purchase. public struct ExternalPurchaseNoticeResultIOS: Codable { /// Optional error message if the presentation failed - public var error: String? + public var error: String? = nil /// External purchase token returned when user continues (iOS 17.4+). /// This token should be reported to Apple's External Purchase Server API. /// Only present when result is Continue. - public var externalPurchaseToken: String? + public var externalPurchaseToken: String? = nil /// Notice result indicating user action public var result: ExternalPurchaseNoticeAction } @@ -772,34 +772,34 @@ public struct PricingPhasesAndroid: Codable { public struct ProductAndroid: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer - public var discountOffers: [DiscountOffer]? - public var displayName: String? + public var discountOffers: [DiscountOffer]? = nil + public var displayName: String? = nil public var displayPrice: String public var id: String public var nameAndroid: String /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? + public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? = nil public var platform: IapPlatform = .android - public var price: Double? + public var price: Double? = nil /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ - public var productStatusAndroid: ProductStatusAndroid? + public var productStatusAndroid: ProductStatusAndroid? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]? + public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]? = nil /// Standardized subscription offers. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer - public var subscriptionOffers: [SubscriptionOffer]? + public var subscriptionOffers: [SubscriptionOffer]? = nil public var title: String public var type: ProductType = .inApp } @@ -811,53 +811,53 @@ public struct ProductAndroid: Codable, ProductCommon { public struct ProductAndroidOneTimePurchaseOfferDetail: Codable { /// Discount display information /// Only available for discounted offers - public var discountDisplayInfo: DiscountDisplayInfoAndroid? + public var discountDisplayInfo: DiscountDisplayInfoAndroid? = nil public var formattedPrice: String /// Full (non-discounted) price in micro-units /// Only available for discounted offers - public var fullPriceMicros: String? + public var fullPriceMicros: String? = nil /// Limited quantity information - public var limitedQuantityInfo: LimitedQuantityInfoAndroid? + public var limitedQuantityInfo: LimitedQuantityInfoAndroid? = nil /// Offer ID - public var offerId: String? + public var offerId: String? = nil /// List of offer tags public var offerTags: [String] /// Offer token for use in BillingFlowParams when purchasing public var offerToken: String /// Pre-order details for products available for pre-order /// Available in Google Play Billing Library 8.1.0+ - public var preorderDetailsAndroid: PreorderDetailsAndroid? + public var preorderDetailsAndroid: PreorderDetailsAndroid? = nil 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? + public var purchaseOptionId: String? = nil /// Rental details for rental offers - public var rentalDetailsAndroid: RentalDetailsAndroid? + public var rentalDetailsAndroid: RentalDetailsAndroid? = nil /// Valid time window for the offer - public var validTimeWindow: ValidTimeWindowAndroid? + public var validTimeWindow: ValidTimeWindowAndroid? = nil } public struct ProductIOS: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String - public var displayName: String? + public var displayName: String? = nil public var displayNameIOS: String public var displayPrice: String public var id: String public var isFamilyShareableIOS: Bool public var jsonRepresentationIOS: String public var platform: IapPlatform = .ios - public var price: Double? + public var price: Double? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var subscriptionInfoIOS: SubscriptionInfoIOS? + public var subscriptionInfoIOS: SubscriptionInfoIOS? = nil /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// Note: iOS does not support one-time product discounts. /// @see https://openiap.dev/docs/types#subscription-offer - public var subscriptionOffers: [SubscriptionOffer]? + public var subscriptionOffers: [SubscriptionOffer]? = nil public var title: String public var type: ProductType = .inApp public var typeIOS: ProductTypeIOS @@ -865,28 +865,28 @@ public struct ProductIOS: Codable, ProductCommon { public struct ProductSubscriptionAndroid: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer - public var discountOffers: [DiscountOffer]? - public var displayName: String? + public var discountOffers: [DiscountOffer]? = nil + public var displayName: String? = nil public var displayPrice: String public var id: String public var nameAndroid: String /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? + public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]? = nil public var platform: IapPlatform = .android - public var price: Double? + public var price: Double? = nil /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ - public var productStatusAndroid: ProductStatusAndroid? + public var productStatusAndroid: ProductStatusAndroid? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails] /// Standardized subscription offers. @@ -905,8 +905,8 @@ public struct ProductSubscriptionAndroidOfferDetails: Codable { /// 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 installmentPlanDetails: InstallmentPlanDetailsAndroid? = nil + public var offerId: String? = nil public var offerTags: [String] public var offerToken: String public var pricingPhases: PricingPhasesAndroid @@ -914,113 +914,114 @@ public struct ProductSubscriptionAndroidOfferDetails: Codable { public struct ProductSubscriptionIOS: Codable, ProductCommon { public var currency: String - public var debugDescription: String? + public var debugDescription: String? = nil public var description: String /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var discountsIOS: [DiscountIOS]? - public var displayName: String? + public var discountsIOS: [DiscountIOS]? = nil + public var displayName: String? = nil public var displayNameIOS: String public var displayPrice: String public var id: String - public var introductoryPriceAsAmountIOS: String? - public var introductoryPriceIOS: String? - public var introductoryPriceNumberOfPeriodsIOS: String? + public var introductoryPriceAsAmountIOS: String? = nil + public var introductoryPriceIOS: String? = nil + public var introductoryPriceNumberOfPeriodsIOS: String? = nil public var introductoryPricePaymentModeIOS: PaymentModeIOS - public var introductoryPriceSubscriptionPeriodIOS: SubscriptionPeriodIOS? + public var introductoryPriceSubscriptionPeriodIOS: SubscriptionPeriodIOS? = nil public var isFamilyShareableIOS: Bool public var jsonRepresentationIOS: String public var platform: IapPlatform = .ios - public var price: Double? + public var price: Double? = nil /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - public var subscriptionInfoIOS: SubscriptionInfoIOS? + public var subscriptionInfoIOS: SubscriptionInfoIOS? = nil /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer - public var subscriptionOffers: [SubscriptionOffer]? - public var subscriptionPeriodNumberIOS: String? - public var subscriptionPeriodUnitIOS: SubscriptionPeriodIOS? + public var subscriptionOffers: [SubscriptionOffer]? = nil + public var subscriptionPeriodNumberIOS: String? = nil + public var subscriptionPeriodUnitIOS: SubscriptionPeriodIOS? = nil public var title: String public var type: ProductType = .subs public var typeIOS: ProductTypeIOS } public struct PurchaseAndroid: Codable, PurchaseCommon { - public var autoRenewingAndroid: Bool? - public var currentPlanId: String? - public var dataAndroid: String? - public var developerPayloadAndroid: String? + public var autoRenewingAndroid: Bool? = nil + public var currentPlanId: String? = nil + public var dataAndroid: String? = nil + public var developerPayloadAndroid: String? = nil public var id: String - public var ids: [String]? - public var isAcknowledgedAndroid: Bool? + public var ids: [String]? = nil + public var isAcknowledgedAndroid: Bool? = nil public var isAutoRenewing: Bool /// Whether the subscription is suspended (Android) /// A suspended subscription means the user's payment method failed and they need to fix it. /// Users should be directed to the subscription center to resolve the issue. /// Do NOT grant entitlements for suspended subscriptions. /// Available in Google Play Billing Library 8.1.0+ - public var isSuspendedAndroid: Bool? - public var obfuscatedAccountIdAndroid: String? - public var obfuscatedProfileIdAndroid: String? - public var packageNameAndroid: String? + public var isSuspendedAndroid: Bool? = nil + public var obfuscatedAccountIdAndroid: String? = nil + public var obfuscatedProfileIdAndroid: String? = nil + public var packageNameAndroid: String? = nil /// 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 pendingPurchaseUpdateAndroid: PendingPurchaseUpdateAndroid? = nil public var platform: IapPlatform public var productId: String public var purchaseState: PurchaseState - public var purchaseToken: String? + public var purchaseToken: String? = nil public var quantity: Int - public var signatureAndroid: String? + public var signatureAndroid: String? = nil /// Store where purchase was made public var store: IapStore public var transactionDate: Double - public var transactionId: String? + public var transactionId: String? = nil } public struct PurchaseError: Codable { public var code: ErrorCode + public var debugMessage: String? = nil public var message: String - public var productId: String? + public var productId: String? = nil } public struct PurchaseIOS: Codable, PurchaseCommon { - public var appAccountToken: String? - public var appBundleIdIOS: String? - public var countryCodeIOS: String? - public var currencyCodeIOS: String? - public var currencySymbolIOS: String? - public var currentPlanId: String? - public var environmentIOS: String? - public var expirationDateIOS: Double? + public var appAccountToken: String? = nil + public var appBundleIdIOS: String? = nil + public var countryCodeIOS: String? = nil + public var currencyCodeIOS: String? = nil + public var currencySymbolIOS: String? = nil + public var currentPlanId: String? = nil + public var environmentIOS: String? = nil + public var expirationDateIOS: Double? = nil public var id: String - public var ids: [String]? + public var ids: [String]? = nil public var isAutoRenewing: Bool - public var isUpgradedIOS: Bool? - public var offerIOS: PurchaseOfferIOS? - public var originalTransactionDateIOS: Double? - public var originalTransactionIdentifierIOS: String? - public var ownershipTypeIOS: String? + public var isUpgradedIOS: Bool? = nil + public var offerIOS: PurchaseOfferIOS? = nil + public var originalTransactionDateIOS: Double? = nil + public var originalTransactionIdentifierIOS: String? = nil + public var ownershipTypeIOS: String? = nil public var platform: IapPlatform public var productId: String public var purchaseState: PurchaseState - public var purchaseToken: String? + public var purchaseToken: String? = nil public var quantity: Int - public var quantityIOS: Int? - public var reasonIOS: String? - public var reasonStringRepresentationIOS: String? - public var renewalInfoIOS: RenewalInfoIOS? - public var revocationDateIOS: Double? - public var revocationReasonIOS: String? + public var quantityIOS: Int? = nil + public var reasonIOS: String? = nil + public var reasonStringRepresentationIOS: String? = nil + public var renewalInfoIOS: RenewalInfoIOS? = nil + public var revocationDateIOS: Double? = nil + public var revocationReasonIOS: String? = nil /// Store where purchase was made public var store: IapStore - public var storefrontCountryCodeIOS: String? - public var subscriptionGroupIdIOS: String? + public var storefrontCountryCodeIOS: String? = nil + public var subscriptionGroupIdIOS: String? = nil public var transactionDate: Double public var transactionId: String - public var transactionReasonIOS: String? - public var webOrderLineItemIdIOS: String? + public var transactionReasonIOS: String? = nil + public var webOrderLineItemIdIOS: String? = nil } public struct PurchaseOfferIOS: Codable { @@ -1030,38 +1031,38 @@ public struct PurchaseOfferIOS: Codable { } public struct RefundResultIOS: Codable { - public var message: String? + public var message: String? = nil public var status: String } /// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo /// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo public struct RenewalInfoIOS: Codable { - public var autoRenewPreference: String? + public var autoRenewPreference: String? = 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? + public var expirationReason: String? = nil /// Grace period expiration date (milliseconds since epoch) /// When set, subscription is in grace period (billing issue but still has access) - public var gracePeriodExpirationDate: Double? + public var gracePeriodExpirationDate: Double? = nil /// True if subscription failed to renew due to billing issue and is retrying /// Note: Not directly available in RenewalInfo, available in Status - public var isInBillingRetry: Bool? - public var jsonRepresentation: String? + public var isInBillingRetry: Bool? = nil + public var jsonRepresentation: String? = nil /// Product ID that will be used on next renewal (when user upgrades/downgrades) /// If set and different from current productId, subscription will change on expiration - public var pendingUpgradeProductId: String? + public var pendingUpgradeProductId: String? = nil /// User's response to subscription price increase /// Possible values: "AGREED", "PENDING", null (no price increase) - public var priceIncreaseStatus: String? + public var priceIncreaseStatus: String? = nil /// Expected renewal date (milliseconds since epoch) /// For active subscriptions, when the next renewal/charge will occur - public var renewalDate: Double? + public var renewalDate: Double? = nil /// Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) - public var renewalOfferId: String? + public var renewalOfferId: String? = nil /// Type of offer applied to next renewal /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. - public var renewalOfferType: String? + public var renewalOfferType: String? = nil public var willAutoRenew: Bool } @@ -1070,7 +1071,7 @@ public struct RenewalInfoIOS: Codable { public struct RentalDetailsAndroid: Codable { /// Rental expiration period in ISO 8601 format /// Time after rental period ends when user can still extend - public var rentalExpirationPeriod: String? + public var rentalExpirationPeriod: String? = nil /// Rental period in ISO 8601 format (e.g., P7D for 7 days) public var rentalPeriod: String } @@ -1089,8 +1090,8 @@ public struct RequestVerifyPurchaseWithIapkitResult: Codable { } public struct SubscriptionInfoIOS: Codable { - public var introductoryOffer: SubscriptionOfferIOS? - public var promotionalOffers: [SubscriptionOfferIOS]? + public var introductoryOffer: SubscriptionOfferIOS? = nil + public var promotionalOffers: [SubscriptionOfferIOS]? = nil public var subscriptionGroupId: String public var subscriptionPeriod: SubscriptionPeriodValueIOS } @@ -1107,9 +1108,9 @@ public struct SubscriptionInfoIOS: Codable { public struct SubscriptionOffer: Codable { /// [Android] Base plan identifier. /// Identifies which base plan this offer belongs to. - public var basePlanIdAndroid: String? + public var basePlanIdAndroid: String? = nil /// Currency code (ISO 4217, e.g., "USD") - public var currency: String? + public var currency: String? = nil /// Formatted display price string (e.g., "$9.99/month") public var displayPrice: String /// Unique identifier for the offer. @@ -1119,39 +1120,39 @@ public struct SubscriptionOffer: Codable { /// [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? + public var installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = nil /// [iOS] Key identifier for signature validation. /// Used with server-side signature generation for promotional offers. - public var keyIdentifierIOS: String? + public var keyIdentifierIOS: String? = nil /// [iOS] Localized price string. - public var localizedPriceIOS: String? + public var localizedPriceIOS: String? = nil /// [iOS] Cryptographic nonce (UUID) for signature validation. /// Must be generated server-side for each purchase attempt. - public var nonceIOS: String? + public var nonceIOS: String? = nil /// [iOS] Number of billing periods for this discount. - public var numberOfPeriodsIOS: Int? + public var numberOfPeriodsIOS: Int? = nil /// [Android] List of tags associated with this offer. - public var offerTagsAndroid: [String]? + public var offerTagsAndroid: [String]? = nil /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. - public var offerTokenAndroid: String? + public var offerTokenAndroid: String? = nil /// Payment mode during the offer period - public var paymentMode: PaymentMode? + public var paymentMode: PaymentMode? = nil /// Subscription period for this offer - public var period: SubscriptionPeriod? + public var period: SubscriptionPeriod? = nil /// Number of periods the offer applies - public var periodCount: Int? + public var periodCount: Int? = nil /// Numeric price value public var price: Double /// [Android] Pricing phases for this subscription offer. /// Contains detailed pricing information for each phase (trial, intro, regular). - public var pricingPhasesAndroid: PricingPhasesAndroid? + public var pricingPhasesAndroid: PricingPhasesAndroid? = nil /// [iOS] Server-generated signature for promotional offer validation. /// Required when applying promotional offers on iOS. - public var signatureIOS: String? + public var signatureIOS: String? = nil /// [iOS] Timestamp when the signature was generated. /// Used for signature validation. - public var timestampIOS: Double? + public var timestampIOS: Double? = nil /// Type of subscription offer (Introductory or Promotional) public var type: DiscountOfferType } @@ -1183,7 +1184,7 @@ public struct SubscriptionPeriodValueIOS: Codable { } public struct SubscriptionStatusIOS: Codable { - public var renewalInfo: RenewalInfoIOS? + public var renewalInfo: RenewalInfoIOS? = nil public var state: String } @@ -1208,10 +1209,10 @@ public struct ValidTimeWindowAndroid: Codable { public struct VerifyPurchaseResultAndroid: Codable { public var autoRenewing: Bool public var betaProduct: Bool - public var cancelDate: Double? - public var cancelReason: String? - public var deferredDate: Double? - public var deferredSku: String? + public var cancelDate: Double? = nil + public var cancelReason: String? = nil + public var deferredDate: Double? = nil + public var deferredSku: String? = nil public var freeTrialEndDate: Double public var gracePeriodEndDate: Double public var parentProductId: String @@ -1230,7 +1231,7 @@ public struct VerifyPurchaseResultAndroid: Codable { /// Returns verification status and grant time for the entitlement. public struct VerifyPurchaseResultHorizon: Codable { /// Unix timestamp (seconds) when the entitlement was granted. - public var grantTime: Double? + public var grantTime: Double? = nil /// Whether the entitlement verification succeeded. public var success: Bool } @@ -1241,21 +1242,21 @@ public struct VerifyPurchaseResultIOS: Codable { /// JWS representation public var jwsRepresentation: String /// Latest transaction if available - public var latestTransaction: Purchase? + public var latestTransaction: Purchase? = nil /// Receipt data string public var receiptData: String } public struct VerifyPurchaseWithProviderError: Codable { - public var code: String? + public var code: String? = nil public var message: String } public struct VerifyPurchaseWithProviderResult: Codable { /// Error details if verification failed - public var errors: [VerifyPurchaseWithProviderError]? + public var errors: [VerifyPurchaseWithProviderError]? = nil /// IAPKit verification result - public var iapkit: RequestVerifyPurchaseWithIapkitResult? + public var iapkit: RequestVerifyPurchaseWithIapkitResult? = nil public var provider: PurchaseVerificationProvider } diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index a9384c92..eb7f5f85 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -2570,17 +2570,20 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { class PurchaseError { const PurchaseError({ required this.code, + this.debugMessage, required this.message, this.productId, }); final ErrorCode code; + final String? debugMessage; final String message; final String? productId; factory PurchaseError.fromJson(Map json) { return PurchaseError( code: ErrorCode.fromJson(json['code'] as String), + debugMessage: json['debugMessage'] as String?, message: json['message'] as String, productId: json['productId'] as String?, ); @@ -2590,6 +2593,7 @@ class PurchaseError { return { '__typename': 'PurchaseError', 'code': code.toJson(), + 'debugMessage': debugMessage, 'message': message, 'productId': productId, }; diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 9690a8ac..4c7c7a66 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -2000,6 +2000,7 @@ class PurchaseError: var code: ErrorCode var message: String = "" var product_id: String = "" + var debug_message: String = "" static func from_dict(data: Dictionary) -> PurchaseError: var obj = PurchaseError.new() @@ -2013,6 +2014,8 @@ class PurchaseError: obj.message = data["message"] if data.has("productId") and data["productId"] != null: obj.product_id = data["productId"] + if data.has("debugMessage") and data["debugMessage"] != null: + obj.debug_message = data["debugMessage"] return obj func to_dict() -> Dictionary: @@ -2023,6 +2026,7 @@ class PurchaseError: dict["code"] = code dict["message"] = message dict["productId"] = product_id + dict["debugMessage"] = debug_message return dict class PurchaseIOS: diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index 7bb7aec7..dabfb9b6 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1105,6 +1105,7 @@ export interface PurchaseCommon { export interface PurchaseError { code: ErrorCode; + debugMessage?: (string | null); message: string; productId?: (string | null); } From d0f026b7e201bae7ef7f21f75bad7eb147f4c2d8 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 20:00:50 +0900 Subject: [PATCH 3/9] fix(review): address 2nd-round PR #98 feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Schema: add `ErrorCode.ServiceTimeout` to `packages/gql/src/error.graphql` so the code is part of the shared enum instead of a hand-typed `"service-timeout"` string literal. `OpenIapError.ServiceTimeout` now sources its CODE from `ErrorCode.ServiceTimeout.rawValue` like every other entry; KMP and Apple error-message switches gain a matching `.serviceTimeout` / `ErrorCode.ServiceTimeout` branch so the `when`/`switch` stays exhaustive. - GDScript plugin: nullable scalar fields no longer round-trip a sentinel default (e.g. `""`, `0`) through `to_dict()`. Nullable properties keep their non-null declaration-time default so the type-checker stays happy, but `to_dict()` now emits the key only when the value has been explicitly set (not equal to the default). This preserves the schema's `null`/absent signal for `PurchaseError .debugMessage` and every other nullable scalar — Dart/Kotlin/Swift consumers no longer receive `""` where they should see null. - KMP sync automation: `packages/gql/scripts/sync-to-platforms.mjs` now also copies the generated types into `libraries/*/` every time `bun run generate` runs. Flutter, Godot, React Native, and Expo each get the generated file verbatim; KMP additionally gets its package declaration (`io.github.hyochan.kmpiap.openiap`) injected and the same enum-companion semicolon regex the Google post-process uses. This removes the "manual edit to an auto-generated file" smell that the review flagged on `libraries/kmp-iap/.../Types.kt`; the file is now reproducible from `error.graphql` + the codegen + this sync script. - KMP ErrorMapping: add the `ServiceTimeout` branch (and keep the `DuplicatePurchase` branch from the previous round) so the exhaustive `when` stays green against the newly-synced ErrorCode enum. Co-Authored-By: Claude Opus 4.6 (1M context) --- libraries/expo-iap/src/types.ts | 1 + .../flutter_inapp_purchase/lib/types.dart | 3 + libraries/godot-iap/addons/godot-iap/types.gd | 367 ++++++++++++------ .../io/github/hyochan/kmpiap/openiap/Types.kt | 5 + .../hyochan/kmpiap/utils/ErrorMapping.kt | 1 + libraries/react-native-iap/src/types.ts | 1 + .../apple/Sources/Models/OpenIapError.swift | 1 + packages/apple/Sources/Models/Types.swift | 3 + packages/apple/Sources/OpenIapModule.swift | 1 + .../main/java/dev/hyo/openiap/OpenIapError.kt | 2 +- .../src/main/java/dev/hyo/openiap/Types.kt | 3 + packages/gql/codegen/plugins/gdscript.ts | 13 + packages/gql/scripts/sync-to-platforms.mjs | 92 ++++- packages/gql/src/error.graphql | 1 + packages/gql/src/generated/Types.kt | 4 + packages/gql/src/generated/Types.swift | 3 + packages/gql/src/generated/types.dart | 3 + packages/gql/src/generated/types.gd | 367 ++++++++++++------ packages/gql/src/generated/types.ts | 1 + 19 files changed, 622 insertions(+), 250 deletions(-) diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index dabfb9b6..81b1f73b 100644 --- a/libraries/expo-iap/src/types.ts +++ b/libraries/expo-iap/src/types.ts @@ -350,6 +350,7 @@ export enum ErrorCode { RemoteError = 'remote-error', ServiceDisconnected = 'service-disconnected', ServiceError = 'service-error', + ServiceTimeout = 'service-timeout', SkuNotFound = 'sku-not-found', SkuOfferMismatch = 'sku-offer-mismatch', SyncError = 'sync-error', diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 40f02bae..df15cc56 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -193,6 +193,7 @@ enum ErrorCode { ConnectionClosed('connection-closed'), InitConnection('init-connection'), ServiceDisconnected('service-disconnected'), + ServiceTimeout('service-timeout'), QueryProduct('query-product'), SkuNotFound('sku-not-found'), SkuOfferMismatch('sku-offer-mismatch'), @@ -268,6 +269,8 @@ enum ErrorCode { return ErrorCode.InitConnection; case 'service-disconnected': return ErrorCode.ServiceDisconnected; + case 'service-timeout': + return ErrorCode.ServiceTimeout; case 'query-product': return ErrorCode.QueryProduct; case 'sku-not-found': diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 4c7c7a66..615e7ebe 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -86,14 +86,15 @@ enum ErrorCode { CONNECTION_CLOSED = 27, INIT_CONNECTION = 28, SERVICE_DISCONNECTED = 29, - QUERY_PRODUCT = 30, - SKU_NOT_FOUND = 31, - SKU_OFFER_MISMATCH = 32, - ITEM_NOT_OWNED = 33, - BILLING_UNAVAILABLE = 34, - FEATURE_NOT_SUPPORTED = 35, - EMPTY_SKU_LIST = 36, - DUPLICATE_PURCHASE = 37, + SERVICE_TIMEOUT = 30, + QUERY_PRODUCT = 31, + SKU_NOT_FOUND = 32, + SKU_OFFER_MISMATCH = 33, + ITEM_NOT_OWNED = 34, + BILLING_UNAVAILABLE = 35, + FEATURE_NOT_SUPPORTED = 36, + EMPTY_SKU_LIST = 37, + DUPLICATE_PURCHASE = 38, } ## Launch mode for external link flow (Android) Determines how the external URL is launched Available in Google Play Billing Library 8.2.0+ @@ -355,17 +356,26 @@ class ActiveSubscription: var dict = {} dict["productId"] = product_id dict["isActive"] = is_active - dict["expirationDateIOS"] = expiration_date_ios - dict["autoRenewingAndroid"] = auto_renewing_android - dict["environmentIOS"] = environment_ios - dict["willExpireSoon"] = will_expire_soon - dict["daysUntilExpirationIOS"] = days_until_expiration_ios + if expiration_date_ios != 0.0: + dict["expirationDateIOS"] = expiration_date_ios + if auto_renewing_android != false: + dict["autoRenewingAndroid"] = auto_renewing_android + if environment_ios != "": + dict["environmentIOS"] = environment_ios + if will_expire_soon != false: + dict["willExpireSoon"] = will_expire_soon + if days_until_expiration_ios != 0.0: + dict["daysUntilExpirationIOS"] = days_until_expiration_ios dict["transactionId"] = transaction_id - dict["purchaseToken"] = purchase_token + if purchase_token != "": + dict["purchaseToken"] = purchase_token dict["transactionDate"] = transaction_date - dict["basePlanIdAndroid"] = base_plan_id_android - dict["purchaseTokenAndroid"] = purchase_token_android - dict["currentPlanId"] = current_plan_id + if base_plan_id_android != "": + dict["basePlanIdAndroid"] = base_plan_id_android + if purchase_token_android != "": + dict["purchaseTokenAndroid"] = purchase_token_android + if current_plan_id != "": + dict["currentPlanId"] = current_plan_id if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -429,9 +439,12 @@ class AppTransaction: dict["signedDate"] = signed_date dict["appId"] = app_id dict["appVersionId"] = app_version_id - dict["preorderDate"] = preorder_date - dict["appTransactionId"] = app_transaction_id - dict["originalPlatform"] = original_platform + if preorder_date != 0.0: + dict["preorderDate"] = preorder_date + if app_transaction_id != "": + dict["appTransactionId"] = app_transaction_id + if original_platform != "": + dict["originalPlatform"] = original_platform return dict ## Result of checking billing program availability (Android) Available in Google Play Billing Library 8.2.0+ @@ -516,7 +529,8 @@ class BillingResultAndroid: func to_dict() -> Dictionary: var dict = {} dict["responseCode"] = response_code - dict["debugMessage"] = debug_message + if debug_message != "": + dict["debugMessage"] = debug_message if SUB_RESPONSE_CODE_ANDROID_VALUES.has(sub_response_code): dict["subResponseCode"] = SUB_RESPONSE_CODE_ANDROID_VALUES[sub_response_code] else: @@ -580,7 +594,8 @@ class DiscountDisplayInfoAndroid: func to_dict() -> Dictionary: var dict = {} - dict["percentageDiscount"] = percentage_discount + if percentage_discount != 0: + dict["percentageDiscount"] = percentage_discount if discount_amount != null and discount_amount.has_method("to_dict"): dict["discountAmount"] = discount_amount.to_dict() else: @@ -634,7 +649,8 @@ class DiscountIOS: else: dict["paymentMode"] = payment_mode dict["subscriptionPeriod"] = subscription_period - dict["localizedPrice"] = localized_price + if localized_price != "": + dict["localizedPrice"] = localized_price return dict ## Standardized one-time product discount offer. Provides a unified interface for one-time purchase discounts across platforms. Currently supported on Android (Google Play Billing 7.0+). iOS does not support one-time purchase discounts in the same way. @see https://openiap.dev/docs/features/discount @@ -726,7 +742,8 @@ class DiscountOffer: func to_dict() -> Dictionary: var dict = {} - dict["id"] = id + if id != "": + dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price dict["currency"] = currency @@ -734,12 +751,18 @@ class DiscountOffer: dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: dict["type"] = type - dict["offerTokenAndroid"] = offer_token_android - dict["offerTagsAndroid"] = offer_tags_android - dict["fullPriceMicrosAndroid"] = full_price_micros_android - dict["percentageDiscountAndroid"] = percentage_discount_android - dict["discountAmountMicrosAndroid"] = discount_amount_micros_android - dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android + if offer_token_android != "": + dict["offerTokenAndroid"] = offer_token_android + if offer_tags_android != []: + dict["offerTagsAndroid"] = offer_tags_android + if full_price_micros_android != "": + dict["fullPriceMicrosAndroid"] = full_price_micros_android + if percentage_discount_android != 0: + dict["percentageDiscountAndroid"] = percentage_discount_android + if discount_amount_micros_android != "": + dict["discountAmountMicrosAndroid"] = discount_amount_micros_android + if formatted_discount_amount_android != "": + dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android if valid_time_window_android != null and valid_time_window_android.has_method("to_dict"): dict["validTimeWindowAndroid"] = valid_time_window_android.to_dict() else: @@ -756,7 +779,8 @@ class DiscountOffer: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionIdAndroid"] = purchase_option_id_android + if purchase_option_id_android != "": + dict["purchaseOptionIdAndroid"] = purchase_option_id_android return dict ## iOS DiscountOffer (output type). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer @@ -867,7 +891,8 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: func to_dict() -> Dictionary: var dict = {} dict["continued"] = continued - dict["error"] = error + if error != "": + dict["error"] = error return dict ## Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). @@ -887,8 +912,10 @@ class ExternalPurchaseCustomLinkTokenResultIOS: func to_dict() -> Dictionary: var dict = {} - dict["token"] = token - dict["error"] = error + if token != "": + dict["token"] = token + if error != "": + dict["error"] = error return dict ## Result of presenting an external purchase link @@ -909,7 +936,8 @@ class ExternalPurchaseLinkResultIOS: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["error"] = error + if error != "": + dict["error"] = error return dict ## Result of presenting external purchase notice sheet (iOS 17.4+) Returns the token when user continues to external purchase. @@ -941,8 +969,10 @@ class ExternalPurchaseNoticeResultIOS: dict["result"] = EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES[result] else: dict["result"] = result - dict["error"] = error - dict["externalPurchaseToken"] = external_purchase_token + if error != "": + dict["error"] = error + if external_purchase_token != "": + dict["externalPurchaseToken"] = external_purchase_token return dict ## Installment plan details for subscription offers (Android) Contains information about the installment plan commitment. Available in Google Play Billing Library 7.0+ @@ -1196,11 +1226,14 @@ class ProductAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1325,13 +1358,15 @@ class ProductAndroidOneTimePurchaseOfferDetail: func to_dict() -> Dictionary: var dict = {} - dict["offerId"] = offer_id + if offer_id != "": + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags dict["priceCurrencyCode"] = price_currency_code dict["formattedPrice"] = formatted_price dict["priceAmountMicros"] = price_amount_micros - dict["fullPriceMicros"] = full_price_micros + if full_price_micros != "": + dict["fullPriceMicros"] = full_price_micros if discount_display_info != null and discount_display_info.has_method("to_dict"): dict["discountDisplayInfo"] = discount_display_info.to_dict() else: @@ -1352,7 +1387,8 @@ class ProductAndroidOneTimePurchaseOfferDetail: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionId"] = purchase_option_id + if purchase_option_id != "": + dict["purchaseOptionId"] = purchase_option_id return dict class ProductIOS: @@ -1441,11 +1477,14 @@ class ProductIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1577,11 +1616,14 @@ class ProductSubscriptionAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1668,7 +1710,8 @@ class ProductSubscriptionAndroidOfferDetails: func to_dict() -> Dictionary: var dict = {} dict["basePlanId"] = base_plan_id - dict["offerId"] = offer_id + if offer_id != "": + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags if pricing_phases != null and pricing_phases.has_method("to_dict"): @@ -1810,11 +1853,14 @@ class ProductSubscriptionIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1850,18 +1896,22 @@ class ProductSubscriptionIOS: dict["discountsIOS"] = arr else: dict["discountsIOS"] = null - dict["introductoryPriceIOS"] = introductory_price_ios - dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios + if introductory_price_ios != "": + dict["introductoryPriceIOS"] = introductory_price_ios + if introductory_price_as_amount_ios != "": + dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios if PAYMENT_MODE_IOS_VALUES.has(introductory_price_payment_mode_ios): dict["introductoryPricePaymentModeIOS"] = PAYMENT_MODE_IOS_VALUES[introductory_price_payment_mode_ios] else: dict["introductoryPricePaymentModeIOS"] = introductory_price_payment_mode_ios - dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios + if introductory_price_number_of_periods_ios != "": + dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(introductory_price_subscription_period_ios): dict["introductoryPriceSubscriptionPeriodIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[introductory_price_subscription_period_ios] else: dict["introductoryPriceSubscriptionPeriodIOS"] = introductory_price_subscription_period_ios - dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios + if subscription_period_number_ios != "": + dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(subscription_period_unit_ios): dict["subscriptionPeriodUnitIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[subscription_period_unit_ios] else: @@ -1962,10 +2012,13 @@ class PurchaseAndroid: var dict = {} dict["id"] = id dict["productId"] = product_id - dict["ids"] = ids - dict["transactionId"] = transaction_id + if ids != []: + dict["ids"] = ids + if transaction_id != "": + dict["transactionId"] = transaction_id dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != "": + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -1980,16 +2033,26 @@ class PurchaseAndroid: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id - dict["dataAndroid"] = data_android - dict["signatureAndroid"] = signature_android - dict["autoRenewingAndroid"] = auto_renewing_android - dict["isAcknowledgedAndroid"] = is_acknowledged_android - dict["packageNameAndroid"] = package_name_android - dict["developerPayloadAndroid"] = developer_payload_android - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - dict["isSuspendedAndroid"] = is_suspended_android + if current_plan_id != "": + dict["currentPlanId"] = current_plan_id + if data_android != "": + dict["dataAndroid"] = data_android + if signature_android != "": + dict["signatureAndroid"] = signature_android + if auto_renewing_android != false: + dict["autoRenewingAndroid"] = auto_renewing_android + if is_acknowledged_android != false: + dict["isAcknowledgedAndroid"] = is_acknowledged_android + if package_name_android != "": + dict["packageNameAndroid"] = package_name_android + if developer_payload_android != "": + dict["developerPayloadAndroid"] = developer_payload_android + if obfuscated_account_id_android != "": + dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android + if obfuscated_profile_id_android != "": + dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android + if is_suspended_android != false: + dict["isSuspendedAndroid"] = is_suspended_android if pending_purchase_update_android != null and pending_purchase_update_android.has_method("to_dict"): dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android.to_dict() else: @@ -2025,8 +2088,10 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - dict["productId"] = product_id - dict["debugMessage"] = debug_message + if product_id != "": + dict["productId"] = product_id + if debug_message != "": + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2160,9 +2225,11 @@ class PurchaseIOS: var dict = {} dict["id"] = id dict["productId"] = product_id - dict["ids"] = ids + if ids != []: + dict["ids"] = ids dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != "": + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2177,32 +2244,53 @@ class PurchaseIOS: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id + if current_plan_id != "": + dict["currentPlanId"] = current_plan_id dict["transactionId"] = transaction_id - dict["quantityIOS"] = quantity_ios - dict["originalTransactionDateIOS"] = original_transaction_date_ios - dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios - dict["appAccountToken"] = app_account_token - dict["expirationDateIOS"] = expiration_date_ios - dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios - dict["environmentIOS"] = environment_ios - dict["storefrontCountryCodeIOS"] = storefront_country_code_ios - dict["appBundleIdIOS"] = app_bundle_id_ios - dict["subscriptionGroupIdIOS"] = subscription_group_id_ios - dict["isUpgradedIOS"] = is_upgraded_ios - dict["ownershipTypeIOS"] = ownership_type_ios - dict["reasonIOS"] = reason_ios - dict["reasonStringRepresentationIOS"] = reason_string_representation_ios - dict["transactionReasonIOS"] = transaction_reason_ios - dict["revocationDateIOS"] = revocation_date_ios - dict["revocationReasonIOS"] = revocation_reason_ios + if quantity_ios != 0: + dict["quantityIOS"] = quantity_ios + if original_transaction_date_ios != 0.0: + dict["originalTransactionDateIOS"] = original_transaction_date_ios + if original_transaction_identifier_ios != "": + dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios + if app_account_token != "": + dict["appAccountToken"] = app_account_token + if expiration_date_ios != 0.0: + dict["expirationDateIOS"] = expiration_date_ios + if web_order_line_item_id_ios != "": + dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios + if environment_ios != "": + dict["environmentIOS"] = environment_ios + if storefront_country_code_ios != "": + dict["storefrontCountryCodeIOS"] = storefront_country_code_ios + if app_bundle_id_ios != "": + dict["appBundleIdIOS"] = app_bundle_id_ios + if subscription_group_id_ios != "": + dict["subscriptionGroupIdIOS"] = subscription_group_id_ios + if is_upgraded_ios != false: + dict["isUpgradedIOS"] = is_upgraded_ios + if ownership_type_ios != "": + dict["ownershipTypeIOS"] = ownership_type_ios + if reason_ios != "": + dict["reasonIOS"] = reason_ios + if reason_string_representation_ios != "": + dict["reasonStringRepresentationIOS"] = reason_string_representation_ios + if transaction_reason_ios != "": + dict["transactionReasonIOS"] = transaction_reason_ios + if revocation_date_ios != 0.0: + dict["revocationDateIOS"] = revocation_date_ios + if revocation_reason_ios != "": + dict["revocationReasonIOS"] = revocation_reason_ios if offer_ios != null and offer_ios.has_method("to_dict"): dict["offerIOS"] = offer_ios.to_dict() else: dict["offerIOS"] = offer_ios - dict["currencyCodeIOS"] = currency_code_ios - dict["currencySymbolIOS"] = currency_symbol_ios - dict["countryCodeIOS"] = country_code_ios + if currency_code_ios != "": + dict["currencyCodeIOS"] = currency_code_ios + if currency_symbol_ios != "": + dict["currencySymbolIOS"] = currency_symbol_ios + if country_code_ios != "": + dict["countryCodeIOS"] = country_code_ios if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -2246,7 +2334,8 @@ class RefundResultIOS: func to_dict() -> Dictionary: var dict = {} dict["status"] = status - dict["message"] = message + if message != "": + dict["message"] = message return dict ## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo @@ -2299,17 +2388,27 @@ class RenewalInfoIOS: func to_dict() -> Dictionary: var dict = {} - dict["jsonRepresentation"] = json_representation + if json_representation != "": + dict["jsonRepresentation"] = json_representation dict["willAutoRenew"] = will_auto_renew - dict["autoRenewPreference"] = auto_renew_preference - dict["expirationReason"] = expiration_reason - dict["gracePeriodExpirationDate"] = grace_period_expiration_date - dict["isInBillingRetry"] = is_in_billing_retry - dict["pendingUpgradeProductId"] = pending_upgrade_product_id - dict["priceIncreaseStatus"] = price_increase_status - dict["renewalDate"] = renewal_date - dict["renewalOfferId"] = renewal_offer_id - dict["renewalOfferType"] = renewal_offer_type + if auto_renew_preference != "": + dict["autoRenewPreference"] = auto_renew_preference + if expiration_reason != "": + dict["expirationReason"] = expiration_reason + if grace_period_expiration_date != 0.0: + dict["gracePeriodExpirationDate"] = grace_period_expiration_date + if is_in_billing_retry != false: + dict["isInBillingRetry"] = is_in_billing_retry + if pending_upgrade_product_id != "": + dict["pendingUpgradeProductId"] = pending_upgrade_product_id + if price_increase_status != "": + dict["priceIncreaseStatus"] = price_increase_status + if renewal_date != 0.0: + dict["renewalDate"] = renewal_date + if renewal_offer_id != "": + dict["renewalOfferId"] = renewal_offer_id + if renewal_offer_type != "": + dict["renewalOfferType"] = renewal_offer_type return dict ## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+ @@ -2330,7 +2429,8 @@ class RentalDetailsAndroid: func to_dict() -> Dictionary: var dict = {} dict["rentalPeriod"] = rental_period - dict["rentalExpirationPeriod"] = rental_expiration_period + if rental_expiration_period != "": + dict["rentalExpirationPeriod"] = rental_expiration_period return dict class RequestVerifyPurchaseWithIapkitResult: @@ -2529,7 +2629,8 @@ class SubscriptionOffer: dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price - dict["currency"] = currency + if currency != "": + dict["currency"] = currency if DISCOUNT_OFFER_TYPE_VALUES.has(type): dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: @@ -2538,20 +2639,30 @@ class SubscriptionOffer: dict["period"] = period.to_dict() else: dict["period"] = period - dict["periodCount"] = period_count + if period_count != 0: + dict["periodCount"] = period_count if PAYMENT_MODE_VALUES.has(payment_mode): dict["paymentMode"] = PAYMENT_MODE_VALUES[payment_mode] else: dict["paymentMode"] = payment_mode - dict["keyIdentifierIOS"] = key_identifier_ios - dict["nonceIOS"] = nonce_ios - dict["signatureIOS"] = signature_ios - dict["timestampIOS"] = timestamp_ios - dict["numberOfPeriodsIOS"] = number_of_periods_ios - dict["localizedPriceIOS"] = localized_price_ios - dict["basePlanIdAndroid"] = base_plan_id_android - dict["offerTokenAndroid"] = offer_token_android - dict["offerTagsAndroid"] = offer_tags_android + if key_identifier_ios != "": + dict["keyIdentifierIOS"] = key_identifier_ios + if nonce_ios != "": + dict["nonceIOS"] = nonce_ios + if signature_ios != "": + dict["signatureIOS"] = signature_ios + if timestamp_ios != 0.0: + dict["timestampIOS"] = timestamp_ios + if number_of_periods_ios != 0: + dict["numberOfPeriodsIOS"] = number_of_periods_ios + if localized_price_ios != "": + dict["localizedPriceIOS"] = localized_price_ios + if base_plan_id_android != "": + dict["basePlanIdAndroid"] = base_plan_id_android + if offer_token_android != "": + dict["offerTokenAndroid"] = offer_token_android + if offer_tags_android != []: + dict["offerTagsAndroid"] = offer_tags_android if pricing_phases_android != null and pricing_phases_android.has_method("to_dict"): dict["pricingPhasesAndroid"] = pricing_phases_android.to_dict() else: @@ -2804,10 +2915,14 @@ class VerifyPurchaseResultAndroid: var dict = {} dict["autoRenewing"] = auto_renewing dict["betaProduct"] = beta_product - dict["cancelDate"] = cancel_date - dict["cancelReason"] = cancel_reason - dict["deferredDate"] = deferred_date - dict["deferredSku"] = deferred_sku + if cancel_date != 0.0: + dict["cancelDate"] = cancel_date + if cancel_reason != "": + dict["cancelReason"] = cancel_reason + if deferred_date != 0.0: + dict["deferredDate"] = deferred_date + if deferred_sku != "": + dict["deferredSku"] = deferred_sku dict["freeTrialEndDate"] = free_trial_end_date dict["gracePeriodEndDate"] = grace_period_end_date dict["parentProductId"] = parent_product_id @@ -2840,7 +2955,8 @@ class VerifyPurchaseResultHorizon: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["grantTime"] = grant_time + if grant_time != 0.0: + dict["grantTime"] = grant_time return dict class VerifyPurchaseResultIOS: @@ -2888,7 +3004,8 @@ class VerifyPurchaseWithProviderError: func to_dict() -> Dictionary: var dict = {} dict["message"] = message - dict["code"] = code + if code != "": + dict["code"] = code return dict class VerifyPurchaseWithProviderResult: @@ -4131,6 +4248,7 @@ const ERROR_CODE_VALUES = { ErrorCode.CONNECTION_CLOSED: "connection-closed", ErrorCode.INIT_CONNECTION: "init-connection", ErrorCode.SERVICE_DISCONNECTED: "service-disconnected", + ErrorCode.SERVICE_TIMEOUT: "service-timeout", ErrorCode.QUERY_PRODUCT: "query-product", ErrorCode.SKU_NOT_FOUND: "sku-not-found", ErrorCode.SKU_OFFER_MISMATCH: "sku-offer-mismatch", @@ -4347,6 +4465,7 @@ const ERROR_CODE_FROM_STRING = { "connection-closed": ErrorCode.CONNECTION_CLOSED, "init-connection": ErrorCode.INIT_CONNECTION, "service-disconnected": ErrorCode.SERVICE_DISCONNECTED, + "service-timeout": ErrorCode.SERVICE_TIMEOUT, "query-product": ErrorCode.QUERY_PRODUCT, "sku-not-found": ErrorCode.SKU_NOT_FOUND, "sku-offer-mismatch": ErrorCode.SKU_OFFER_MISMATCH, 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 4afc517e..90136860 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 @@ -7,6 +7,7 @@ @file:Suppress("UNCHECKED_CAST") package io.github.hyochan.kmpiap.openiap + // MARK: - Enums /** @@ -205,6 +206,7 @@ public enum class ErrorCode(val rawValue: String) { ConnectionClosed("connection-closed"), InitConnection("init-connection"), ServiceDisconnected("service-disconnected"), + ServiceTimeout("service-timeout"), QueryProduct("query-product"), SkuNotFound("sku-not-found"), SkuOfferMismatch("sku-offer-mismatch"), @@ -306,6 +308,9 @@ public enum class ErrorCode(val rawValue: String) { "service-disconnected" -> ErrorCode.ServiceDisconnected "SERVICE_DISCONNECTED" -> ErrorCode.ServiceDisconnected "ServiceDisconnected" -> ErrorCode.ServiceDisconnected + "service-timeout" -> ErrorCode.ServiceTimeout + "SERVICE_TIMEOUT" -> ErrorCode.ServiceTimeout + "ServiceTimeout" -> ErrorCode.ServiceTimeout "query-product" -> ErrorCode.QueryProduct "QUERY_PRODUCT" -> ErrorCode.QueryProduct "QueryProduct" -> ErrorCode.QueryProduct diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt index 19fb1f48..0a6be119 100644 --- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt +++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt @@ -154,5 +154,6 @@ object ErrorCodeUtils { ErrorCode.PurchaseVerificationFinished -> "Purchase verification completed" ErrorCode.PurchaseVerificationFinishFailed -> "Failed to complete purchase verification" ErrorCode.DuplicatePurchase -> "Duplicate purchase update detected" + ErrorCode.ServiceTimeout -> "Billing service request timed out" } } diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts index dabfb9b6..81b1f73b 100644 --- a/libraries/react-native-iap/src/types.ts +++ b/libraries/react-native-iap/src/types.ts @@ -350,6 +350,7 @@ export enum ErrorCode { RemoteError = 'remote-error', ServiceDisconnected = 'service-disconnected', ServiceError = 'service-error', + ServiceTimeout = 'service-timeout', SkuNotFound = 'sku-not-found', SkuOfferMismatch = 'sku-offer-mismatch', SyncError = 'sync-error', diff --git a/packages/apple/Sources/Models/OpenIapError.swift b/packages/apple/Sources/Models/OpenIapError.swift index 20da1e74..b75b9a90 100644 --- a/packages/apple/Sources/Models/OpenIapError.swift +++ b/packages/apple/Sources/Models/OpenIapError.swift @@ -37,6 +37,7 @@ public extension PurchaseError { case .connectionClosed: return "Connection closed" case .initConnection: return "Failed to initialize billing connection" case .serviceDisconnected: return "Billing service disconnected" + case .serviceTimeout: return "Billing service request timed out" case .queryProduct: return "Failed to query product" case .skuNotFound: return "SKU not found" case .skuOfferMismatch: return "SKU offer mismatch" diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index d639053f..72eb9f43 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -104,6 +104,7 @@ public enum ErrorCode: String, Codable, CaseIterable { case connectionClosed = "connection-closed" case initConnection = "init-connection" case serviceDisconnected = "service-disconnected" + case serviceTimeout = "service-timeout" case queryProduct = "query-product" case skuNotFound = "sku-not-found" case skuOfferMismatch = "sku-offer-mismatch" @@ -178,6 +179,8 @@ public enum ErrorCode: String, Codable, CaseIterable { self = .initConnection case "service-disconnected", "ServiceDisconnected": self = .serviceDisconnected + case "service-timeout", "ServiceTimeout": + self = .serviceTimeout case "query-product", "QueryProduct": self = .queryProduct case "sku-not-found", "SkuNotFound": diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 9b9f7e78..448364fc 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -1550,6 +1550,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { case .connectionClosed: return "Connection closed" case .initConnection: return "Failed to initialize billing connection" case .serviceDisconnected: return "Billing service disconnected" + case .serviceTimeout: return "Billing service request timed out" case .queryProduct: return "Failed to query product" case .skuNotFound: return "SKU not found" case .skuOfferMismatch: return "SKU offer mismatch" diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt index 336e67f7..08d50ba2 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt @@ -296,7 +296,7 @@ sealed class OpenIapError : Exception() { override val message: String = MESSAGE companion object { - const val CODE = "service-timeout" + val CODE = ErrorCode.ServiceTimeout.rawValue const val MESSAGE = "The request has reached the maximum timeout before billing service responds" } } 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 8ea24d80..6479ca6f 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 @@ -202,6 +202,7 @@ public enum class ErrorCode(val rawValue: String) { ConnectionClosed("connection-closed"), InitConnection("init-connection"), ServiceDisconnected("service-disconnected"), + ServiceTimeout("service-timeout"), QueryProduct("query-product"), SkuNotFound("sku-not-found"), SkuOfferMismatch("sku-offer-mismatch"), @@ -273,6 +274,8 @@ public enum class ErrorCode(val rawValue: String) { "InitConnection" -> ErrorCode.InitConnection "service-disconnected" -> ErrorCode.ServiceDisconnected "ServiceDisconnected" -> ErrorCode.ServiceDisconnected + "service-timeout" -> ErrorCode.ServiceTimeout + "ServiceTimeout" -> ErrorCode.ServiceTimeout "query-product" -> ErrorCode.QueryProduct "QueryProduct" -> ErrorCode.QueryProduct "sku-not-found" -> ErrorCode.SkuNotFound diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts index 0ad158a4..cb53b553 100644 --- a/packages/gql/codegen/plugins/gdscript.ts +++ b/packages/gql/codegen/plugins/gdscript.ts @@ -420,6 +420,19 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`\t\t\tdict["${graphqlName}"] = ${enumConstName}[${fieldName}]`); this.emit(`\t\telse:`); this.emit(`\t\t\tdict["${graphqlName}"] = ${fieldName}`); + } else if (type.nullable) { + // Nullable scalars get a non-null default (e.g. "" for String, 0 for + // int) at declaration time so GDScript type checks pass. When that + // default is still in place, treat the field as "not set" and omit it + // from the serialized dict so consumers receive null/absent instead + // of the sentinel default. + const sentinel = this.getDefaultValue(type); + if (sentinel !== null) { + this.emit(`\t\tif ${fieldName} != ${sentinel}:`); + this.emit(`\t\t\tdict["${graphqlName}"] = ${fieldName}`); + } else { + this.emit(`\t\tdict["${graphqlName}"] = ${fieldName}`); + } } else { this.emit(`\t\tdict["${graphqlName}"] = ${fieldName}`); } diff --git a/packages/gql/scripts/sync-to-platforms.mjs b/packages/gql/scripts/sync-to-platforms.mjs index c95ede36..932ecf1d 100755 --- a/packages/gql/scripts/sync-to-platforms.mjs +++ b/packages/gql/scripts/sync-to-platforms.mjs @@ -1,5 +1,11 @@ #!/usr/bin/env bun -import { copyFileSync, existsSync, mkdirSync } from 'node:fs'; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { execSync } from 'node:child_process'; @@ -17,6 +23,32 @@ const kotlinTarget = resolve(monorepoRoot, 'packages/google/openiap/src/main/jav const swiftSource = resolve(gqlRoot, 'src/generated/Types.swift'); const swiftTarget = resolve(monorepoRoot, 'packages/apple/Sources/Models/Types.swift'); +// Library targets — generated types are copied in with per-library +// transformations (package names, file extensions, etc.) so that +// `libraries/*/` stay in lockstep with the gql schema instead of needing +// hand edits after every regeneration. +const dartSource = resolve(gqlRoot, 'src/generated/types.dart'); +const dartTarget = resolve( + monorepoRoot, + 'libraries/flutter_inapp_purchase/lib/types.dart', +); + +const gdSource = resolve(gqlRoot, 'src/generated/types.gd'); +const gdTarget = resolve( + monorepoRoot, + 'libraries/godot-iap/addons/godot-iap/types.gd', +); + +const tsSource = resolve(gqlRoot, 'src/generated/types.ts'); +const rnTsTarget = resolve(monorepoRoot, 'libraries/react-native-iap/src/types.ts'); +const expoTsTarget = resolve(monorepoRoot, 'libraries/expo-iap/src/types.ts'); + +const kmpSource = resolve(gqlRoot, 'src/generated/Types.kt'); +const kmpTarget = resolve( + monorepoRoot, + 'libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt', +); + console.log('📦 Syncing generated types to platforms...\n'); // Sync Kotlin to Google (Android) @@ -51,4 +83,62 @@ if (existsSync(swiftSource)) { console.warn('⚠️ Swift types not found, skipping Apple sync'); } +// Sync Dart to flutter_inapp_purchase +if (existsSync(dartSource)) { + mkdirSync(dirname(dartTarget), { recursive: true }); + copyFileSync(dartSource, dartTarget); + console.log('✅ Dart → flutter_inapp_purchase'); + console.log(` ${dartTarget}\n`); +} + +// Sync GDScript to godot-iap +if (existsSync(gdSource)) { + mkdirSync(dirname(gdTarget), { recursive: true }); + copyFileSync(gdSource, gdTarget); + console.log('✅ GDScript → godot-iap'); + console.log(` ${gdTarget}\n`); +} + +// Sync TypeScript to react-native-iap + expo-iap +if (existsSync(tsSource)) { + for (const target of [rnTsTarget, expoTsTarget]) { + mkdirSync(dirname(target), { recursive: true }); + copyFileSync(tsSource, target); + } + console.log('✅ TypeScript → react-native-iap + expo-iap'); + console.log(` ${rnTsTarget}`); + console.log(` ${expoTsTarget}\n`); +} + +// Sync Kotlin to kmp-iap with the library-specific package declaration and +// the enum-companion semicolon that Kotlin requires. This mirrors the +// post-process that packages/google runs; without it the KMP module would +// not compile against the upstream gql types. +if (existsSync(kmpSource)) { + mkdirSync(dirname(kmpTarget), { recursive: true }); + let text = readFileSync(kmpSource, 'utf8'); + + // Insert package declaration after the leading @file: annotations so the + // resulting file mirrors packages/google/.../Types.kt. + if (!/\bpackage io\.github\.hyochan\.kmpiap\.openiap\b/.test(text)) { + text = text.replace( + /(@file:[^\n]+\n)(?!\s*package\b)/, + '$1\npackage io.github.hyochan.kmpiap.openiap\n', + ); + } + + // Kotlin enums that declare a companion object require a trailing + // semicolon after the last enum entry. Match the same pattern used by + // packages/google/scripts/post-process-types.sh so the files stay in + // lockstep. + text = text.replace( + /(\n\s*\w+\([^)]*\))\n\n(\s+companion object)/g, + '$1;\n\n$2', + ); + + writeFileSync(kmpTarget, text); + console.log('✅ Kotlin → kmp-iap (with package + enum-semicolon post-process)'); + console.log(` ${kmpTarget}\n`); +} + console.log('🎉 Platform sync complete!\n'); diff --git a/packages/gql/src/error.graphql b/packages/gql/src/error.graphql index 4c28de52..5cec9ca2 100644 --- a/packages/gql/src/error.graphql +++ b/packages/gql/src/error.graphql @@ -35,6 +35,7 @@ enum ErrorCode { ConnectionClosed InitConnection ServiceDisconnected + ServiceTimeout QueryProduct SkuNotFound SkuOfferMismatch diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 4ebcaf95..c46f0f95 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -204,6 +204,7 @@ public enum class ErrorCode(val rawValue: String) { ConnectionClosed("connection-closed"), InitConnection("init-connection"), ServiceDisconnected("service-disconnected"), + ServiceTimeout("service-timeout"), QueryProduct("query-product"), SkuNotFound("sku-not-found"), SkuOfferMismatch("sku-offer-mismatch"), @@ -305,6 +306,9 @@ public enum class ErrorCode(val rawValue: String) { "service-disconnected" -> ErrorCode.ServiceDisconnected "SERVICE_DISCONNECTED" -> ErrorCode.ServiceDisconnected "ServiceDisconnected" -> ErrorCode.ServiceDisconnected + "service-timeout" -> ErrorCode.ServiceTimeout + "SERVICE_TIMEOUT" -> ErrorCode.ServiceTimeout + "ServiceTimeout" -> ErrorCode.ServiceTimeout "query-product" -> ErrorCode.QueryProduct "QUERY_PRODUCT" -> ErrorCode.QueryProduct "QueryProduct" -> ErrorCode.QueryProduct diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index d639053f..72eb9f43 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -104,6 +104,7 @@ public enum ErrorCode: String, Codable, CaseIterable { case connectionClosed = "connection-closed" case initConnection = "init-connection" case serviceDisconnected = "service-disconnected" + case serviceTimeout = "service-timeout" case queryProduct = "query-product" case skuNotFound = "sku-not-found" case skuOfferMismatch = "sku-offer-mismatch" @@ -178,6 +179,8 @@ public enum ErrorCode: String, Codable, CaseIterable { self = .initConnection case "service-disconnected", "ServiceDisconnected": self = .serviceDisconnected + case "service-timeout", "ServiceTimeout": + self = .serviceTimeout case "query-product", "QueryProduct": self = .queryProduct case "sku-not-found", "SkuNotFound": diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index eb7f5f85..67d7d3f5 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -182,6 +182,7 @@ enum ErrorCode { ConnectionClosed('connection-closed'), InitConnection('init-connection'), ServiceDisconnected('service-disconnected'), + ServiceTimeout('service-timeout'), QueryProduct('query-product'), SkuNotFound('sku-not-found'), SkuOfferMismatch('sku-offer-mismatch'), @@ -257,6 +258,8 @@ enum ErrorCode { return ErrorCode.InitConnection; case 'service-disconnected': return ErrorCode.ServiceDisconnected; + case 'service-timeout': + return ErrorCode.ServiceTimeout; case 'query-product': return ErrorCode.QueryProduct; case 'sku-not-found': diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 4c7c7a66..615e7ebe 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -86,14 +86,15 @@ enum ErrorCode { CONNECTION_CLOSED = 27, INIT_CONNECTION = 28, SERVICE_DISCONNECTED = 29, - QUERY_PRODUCT = 30, - SKU_NOT_FOUND = 31, - SKU_OFFER_MISMATCH = 32, - ITEM_NOT_OWNED = 33, - BILLING_UNAVAILABLE = 34, - FEATURE_NOT_SUPPORTED = 35, - EMPTY_SKU_LIST = 36, - DUPLICATE_PURCHASE = 37, + SERVICE_TIMEOUT = 30, + QUERY_PRODUCT = 31, + SKU_NOT_FOUND = 32, + SKU_OFFER_MISMATCH = 33, + ITEM_NOT_OWNED = 34, + BILLING_UNAVAILABLE = 35, + FEATURE_NOT_SUPPORTED = 36, + EMPTY_SKU_LIST = 37, + DUPLICATE_PURCHASE = 38, } ## Launch mode for external link flow (Android) Determines how the external URL is launched Available in Google Play Billing Library 8.2.0+ @@ -355,17 +356,26 @@ class ActiveSubscription: var dict = {} dict["productId"] = product_id dict["isActive"] = is_active - dict["expirationDateIOS"] = expiration_date_ios - dict["autoRenewingAndroid"] = auto_renewing_android - dict["environmentIOS"] = environment_ios - dict["willExpireSoon"] = will_expire_soon - dict["daysUntilExpirationIOS"] = days_until_expiration_ios + if expiration_date_ios != 0.0: + dict["expirationDateIOS"] = expiration_date_ios + if auto_renewing_android != false: + dict["autoRenewingAndroid"] = auto_renewing_android + if environment_ios != "": + dict["environmentIOS"] = environment_ios + if will_expire_soon != false: + dict["willExpireSoon"] = will_expire_soon + if days_until_expiration_ios != 0.0: + dict["daysUntilExpirationIOS"] = days_until_expiration_ios dict["transactionId"] = transaction_id - dict["purchaseToken"] = purchase_token + if purchase_token != "": + dict["purchaseToken"] = purchase_token dict["transactionDate"] = transaction_date - dict["basePlanIdAndroid"] = base_plan_id_android - dict["purchaseTokenAndroid"] = purchase_token_android - dict["currentPlanId"] = current_plan_id + if base_plan_id_android != "": + dict["basePlanIdAndroid"] = base_plan_id_android + if purchase_token_android != "": + dict["purchaseTokenAndroid"] = purchase_token_android + if current_plan_id != "": + dict["currentPlanId"] = current_plan_id if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -429,9 +439,12 @@ class AppTransaction: dict["signedDate"] = signed_date dict["appId"] = app_id dict["appVersionId"] = app_version_id - dict["preorderDate"] = preorder_date - dict["appTransactionId"] = app_transaction_id - dict["originalPlatform"] = original_platform + if preorder_date != 0.0: + dict["preorderDate"] = preorder_date + if app_transaction_id != "": + dict["appTransactionId"] = app_transaction_id + if original_platform != "": + dict["originalPlatform"] = original_platform return dict ## Result of checking billing program availability (Android) Available in Google Play Billing Library 8.2.0+ @@ -516,7 +529,8 @@ class BillingResultAndroid: func to_dict() -> Dictionary: var dict = {} dict["responseCode"] = response_code - dict["debugMessage"] = debug_message + if debug_message != "": + dict["debugMessage"] = debug_message if SUB_RESPONSE_CODE_ANDROID_VALUES.has(sub_response_code): dict["subResponseCode"] = SUB_RESPONSE_CODE_ANDROID_VALUES[sub_response_code] else: @@ -580,7 +594,8 @@ class DiscountDisplayInfoAndroid: func to_dict() -> Dictionary: var dict = {} - dict["percentageDiscount"] = percentage_discount + if percentage_discount != 0: + dict["percentageDiscount"] = percentage_discount if discount_amount != null and discount_amount.has_method("to_dict"): dict["discountAmount"] = discount_amount.to_dict() else: @@ -634,7 +649,8 @@ class DiscountIOS: else: dict["paymentMode"] = payment_mode dict["subscriptionPeriod"] = subscription_period - dict["localizedPrice"] = localized_price + if localized_price != "": + dict["localizedPrice"] = localized_price return dict ## Standardized one-time product discount offer. Provides a unified interface for one-time purchase discounts across platforms. Currently supported on Android (Google Play Billing 7.0+). iOS does not support one-time purchase discounts in the same way. @see https://openiap.dev/docs/features/discount @@ -726,7 +742,8 @@ class DiscountOffer: func to_dict() -> Dictionary: var dict = {} - dict["id"] = id + if id != "": + dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price dict["currency"] = currency @@ -734,12 +751,18 @@ class DiscountOffer: dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: dict["type"] = type - dict["offerTokenAndroid"] = offer_token_android - dict["offerTagsAndroid"] = offer_tags_android - dict["fullPriceMicrosAndroid"] = full_price_micros_android - dict["percentageDiscountAndroid"] = percentage_discount_android - dict["discountAmountMicrosAndroid"] = discount_amount_micros_android - dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android + if offer_token_android != "": + dict["offerTokenAndroid"] = offer_token_android + if offer_tags_android != []: + dict["offerTagsAndroid"] = offer_tags_android + if full_price_micros_android != "": + dict["fullPriceMicrosAndroid"] = full_price_micros_android + if percentage_discount_android != 0: + dict["percentageDiscountAndroid"] = percentage_discount_android + if discount_amount_micros_android != "": + dict["discountAmountMicrosAndroid"] = discount_amount_micros_android + if formatted_discount_amount_android != "": + dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android if valid_time_window_android != null and valid_time_window_android.has_method("to_dict"): dict["validTimeWindowAndroid"] = valid_time_window_android.to_dict() else: @@ -756,7 +779,8 @@ class DiscountOffer: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionIdAndroid"] = purchase_option_id_android + if purchase_option_id_android != "": + dict["purchaseOptionIdAndroid"] = purchase_option_id_android return dict ## iOS DiscountOffer (output type). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer @@ -867,7 +891,8 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: func to_dict() -> Dictionary: var dict = {} dict["continued"] = continued - dict["error"] = error + if error != "": + dict["error"] = error return dict ## Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). @@ -887,8 +912,10 @@ class ExternalPurchaseCustomLinkTokenResultIOS: func to_dict() -> Dictionary: var dict = {} - dict["token"] = token - dict["error"] = error + if token != "": + dict["token"] = token + if error != "": + dict["error"] = error return dict ## Result of presenting an external purchase link @@ -909,7 +936,8 @@ class ExternalPurchaseLinkResultIOS: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["error"] = error + if error != "": + dict["error"] = error return dict ## Result of presenting external purchase notice sheet (iOS 17.4+) Returns the token when user continues to external purchase. @@ -941,8 +969,10 @@ class ExternalPurchaseNoticeResultIOS: dict["result"] = EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES[result] else: dict["result"] = result - dict["error"] = error - dict["externalPurchaseToken"] = external_purchase_token + if error != "": + dict["error"] = error + if external_purchase_token != "": + dict["externalPurchaseToken"] = external_purchase_token return dict ## Installment plan details for subscription offers (Android) Contains information about the installment plan commitment. Available in Google Play Billing Library 7.0+ @@ -1196,11 +1226,14 @@ class ProductAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1325,13 +1358,15 @@ class ProductAndroidOneTimePurchaseOfferDetail: func to_dict() -> Dictionary: var dict = {} - dict["offerId"] = offer_id + if offer_id != "": + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags dict["priceCurrencyCode"] = price_currency_code dict["formattedPrice"] = formatted_price dict["priceAmountMicros"] = price_amount_micros - dict["fullPriceMicros"] = full_price_micros + if full_price_micros != "": + dict["fullPriceMicros"] = full_price_micros if discount_display_info != null and discount_display_info.has_method("to_dict"): dict["discountDisplayInfo"] = discount_display_info.to_dict() else: @@ -1352,7 +1387,8 @@ class ProductAndroidOneTimePurchaseOfferDetail: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionId"] = purchase_option_id + if purchase_option_id != "": + dict["purchaseOptionId"] = purchase_option_id return dict class ProductIOS: @@ -1441,11 +1477,14 @@ class ProductIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1577,11 +1616,14 @@ class ProductSubscriptionAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1668,7 +1710,8 @@ class ProductSubscriptionAndroidOfferDetails: func to_dict() -> Dictionary: var dict = {} dict["basePlanId"] = base_plan_id - dict["offerId"] = offer_id + if offer_id != "": + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags if pricing_phases != null and pricing_phases.has_method("to_dict"): @@ -1810,11 +1853,14 @@ class ProductSubscriptionIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != "": + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != 0.0: + dict["price"] = price + if debug_description != "": + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1850,18 +1896,22 @@ class ProductSubscriptionIOS: dict["discountsIOS"] = arr else: dict["discountsIOS"] = null - dict["introductoryPriceIOS"] = introductory_price_ios - dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios + if introductory_price_ios != "": + dict["introductoryPriceIOS"] = introductory_price_ios + if introductory_price_as_amount_ios != "": + dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios if PAYMENT_MODE_IOS_VALUES.has(introductory_price_payment_mode_ios): dict["introductoryPricePaymentModeIOS"] = PAYMENT_MODE_IOS_VALUES[introductory_price_payment_mode_ios] else: dict["introductoryPricePaymentModeIOS"] = introductory_price_payment_mode_ios - dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios + if introductory_price_number_of_periods_ios != "": + dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(introductory_price_subscription_period_ios): dict["introductoryPriceSubscriptionPeriodIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[introductory_price_subscription_period_ios] else: dict["introductoryPriceSubscriptionPeriodIOS"] = introductory_price_subscription_period_ios - dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios + if subscription_period_number_ios != "": + dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(subscription_period_unit_ios): dict["subscriptionPeriodUnitIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[subscription_period_unit_ios] else: @@ -1962,10 +2012,13 @@ class PurchaseAndroid: var dict = {} dict["id"] = id dict["productId"] = product_id - dict["ids"] = ids - dict["transactionId"] = transaction_id + if ids != []: + dict["ids"] = ids + if transaction_id != "": + dict["transactionId"] = transaction_id dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != "": + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -1980,16 +2033,26 @@ class PurchaseAndroid: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id - dict["dataAndroid"] = data_android - dict["signatureAndroid"] = signature_android - dict["autoRenewingAndroid"] = auto_renewing_android - dict["isAcknowledgedAndroid"] = is_acknowledged_android - dict["packageNameAndroid"] = package_name_android - dict["developerPayloadAndroid"] = developer_payload_android - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - dict["isSuspendedAndroid"] = is_suspended_android + if current_plan_id != "": + dict["currentPlanId"] = current_plan_id + if data_android != "": + dict["dataAndroid"] = data_android + if signature_android != "": + dict["signatureAndroid"] = signature_android + if auto_renewing_android != false: + dict["autoRenewingAndroid"] = auto_renewing_android + if is_acknowledged_android != false: + dict["isAcknowledgedAndroid"] = is_acknowledged_android + if package_name_android != "": + dict["packageNameAndroid"] = package_name_android + if developer_payload_android != "": + dict["developerPayloadAndroid"] = developer_payload_android + if obfuscated_account_id_android != "": + dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android + if obfuscated_profile_id_android != "": + dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android + if is_suspended_android != false: + dict["isSuspendedAndroid"] = is_suspended_android if pending_purchase_update_android != null and pending_purchase_update_android.has_method("to_dict"): dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android.to_dict() else: @@ -2025,8 +2088,10 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - dict["productId"] = product_id - dict["debugMessage"] = debug_message + if product_id != "": + dict["productId"] = product_id + if debug_message != "": + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2160,9 +2225,11 @@ class PurchaseIOS: var dict = {} dict["id"] = id dict["productId"] = product_id - dict["ids"] = ids + if ids != []: + dict["ids"] = ids dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != "": + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2177,32 +2244,53 @@ class PurchaseIOS: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id + if current_plan_id != "": + dict["currentPlanId"] = current_plan_id dict["transactionId"] = transaction_id - dict["quantityIOS"] = quantity_ios - dict["originalTransactionDateIOS"] = original_transaction_date_ios - dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios - dict["appAccountToken"] = app_account_token - dict["expirationDateIOS"] = expiration_date_ios - dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios - dict["environmentIOS"] = environment_ios - dict["storefrontCountryCodeIOS"] = storefront_country_code_ios - dict["appBundleIdIOS"] = app_bundle_id_ios - dict["subscriptionGroupIdIOS"] = subscription_group_id_ios - dict["isUpgradedIOS"] = is_upgraded_ios - dict["ownershipTypeIOS"] = ownership_type_ios - dict["reasonIOS"] = reason_ios - dict["reasonStringRepresentationIOS"] = reason_string_representation_ios - dict["transactionReasonIOS"] = transaction_reason_ios - dict["revocationDateIOS"] = revocation_date_ios - dict["revocationReasonIOS"] = revocation_reason_ios + if quantity_ios != 0: + dict["quantityIOS"] = quantity_ios + if original_transaction_date_ios != 0.0: + dict["originalTransactionDateIOS"] = original_transaction_date_ios + if original_transaction_identifier_ios != "": + dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios + if app_account_token != "": + dict["appAccountToken"] = app_account_token + if expiration_date_ios != 0.0: + dict["expirationDateIOS"] = expiration_date_ios + if web_order_line_item_id_ios != "": + dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios + if environment_ios != "": + dict["environmentIOS"] = environment_ios + if storefront_country_code_ios != "": + dict["storefrontCountryCodeIOS"] = storefront_country_code_ios + if app_bundle_id_ios != "": + dict["appBundleIdIOS"] = app_bundle_id_ios + if subscription_group_id_ios != "": + dict["subscriptionGroupIdIOS"] = subscription_group_id_ios + if is_upgraded_ios != false: + dict["isUpgradedIOS"] = is_upgraded_ios + if ownership_type_ios != "": + dict["ownershipTypeIOS"] = ownership_type_ios + if reason_ios != "": + dict["reasonIOS"] = reason_ios + if reason_string_representation_ios != "": + dict["reasonStringRepresentationIOS"] = reason_string_representation_ios + if transaction_reason_ios != "": + dict["transactionReasonIOS"] = transaction_reason_ios + if revocation_date_ios != 0.0: + dict["revocationDateIOS"] = revocation_date_ios + if revocation_reason_ios != "": + dict["revocationReasonIOS"] = revocation_reason_ios if offer_ios != null and offer_ios.has_method("to_dict"): dict["offerIOS"] = offer_ios.to_dict() else: dict["offerIOS"] = offer_ios - dict["currencyCodeIOS"] = currency_code_ios - dict["currencySymbolIOS"] = currency_symbol_ios - dict["countryCodeIOS"] = country_code_ios + if currency_code_ios != "": + dict["currencyCodeIOS"] = currency_code_ios + if currency_symbol_ios != "": + dict["currencySymbolIOS"] = currency_symbol_ios + if country_code_ios != "": + dict["countryCodeIOS"] = country_code_ios if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -2246,7 +2334,8 @@ class RefundResultIOS: func to_dict() -> Dictionary: var dict = {} dict["status"] = status - dict["message"] = message + if message != "": + dict["message"] = message return dict ## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo @@ -2299,17 +2388,27 @@ class RenewalInfoIOS: func to_dict() -> Dictionary: var dict = {} - dict["jsonRepresentation"] = json_representation + if json_representation != "": + dict["jsonRepresentation"] = json_representation dict["willAutoRenew"] = will_auto_renew - dict["autoRenewPreference"] = auto_renew_preference - dict["expirationReason"] = expiration_reason - dict["gracePeriodExpirationDate"] = grace_period_expiration_date - dict["isInBillingRetry"] = is_in_billing_retry - dict["pendingUpgradeProductId"] = pending_upgrade_product_id - dict["priceIncreaseStatus"] = price_increase_status - dict["renewalDate"] = renewal_date - dict["renewalOfferId"] = renewal_offer_id - dict["renewalOfferType"] = renewal_offer_type + if auto_renew_preference != "": + dict["autoRenewPreference"] = auto_renew_preference + if expiration_reason != "": + dict["expirationReason"] = expiration_reason + if grace_period_expiration_date != 0.0: + dict["gracePeriodExpirationDate"] = grace_period_expiration_date + if is_in_billing_retry != false: + dict["isInBillingRetry"] = is_in_billing_retry + if pending_upgrade_product_id != "": + dict["pendingUpgradeProductId"] = pending_upgrade_product_id + if price_increase_status != "": + dict["priceIncreaseStatus"] = price_increase_status + if renewal_date != 0.0: + dict["renewalDate"] = renewal_date + if renewal_offer_id != "": + dict["renewalOfferId"] = renewal_offer_id + if renewal_offer_type != "": + dict["renewalOfferType"] = renewal_offer_type return dict ## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+ @@ -2330,7 +2429,8 @@ class RentalDetailsAndroid: func to_dict() -> Dictionary: var dict = {} dict["rentalPeriod"] = rental_period - dict["rentalExpirationPeriod"] = rental_expiration_period + if rental_expiration_period != "": + dict["rentalExpirationPeriod"] = rental_expiration_period return dict class RequestVerifyPurchaseWithIapkitResult: @@ -2529,7 +2629,8 @@ class SubscriptionOffer: dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price - dict["currency"] = currency + if currency != "": + dict["currency"] = currency if DISCOUNT_OFFER_TYPE_VALUES.has(type): dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: @@ -2538,20 +2639,30 @@ class SubscriptionOffer: dict["period"] = period.to_dict() else: dict["period"] = period - dict["periodCount"] = period_count + if period_count != 0: + dict["periodCount"] = period_count if PAYMENT_MODE_VALUES.has(payment_mode): dict["paymentMode"] = PAYMENT_MODE_VALUES[payment_mode] else: dict["paymentMode"] = payment_mode - dict["keyIdentifierIOS"] = key_identifier_ios - dict["nonceIOS"] = nonce_ios - dict["signatureIOS"] = signature_ios - dict["timestampIOS"] = timestamp_ios - dict["numberOfPeriodsIOS"] = number_of_periods_ios - dict["localizedPriceIOS"] = localized_price_ios - dict["basePlanIdAndroid"] = base_plan_id_android - dict["offerTokenAndroid"] = offer_token_android - dict["offerTagsAndroid"] = offer_tags_android + if key_identifier_ios != "": + dict["keyIdentifierIOS"] = key_identifier_ios + if nonce_ios != "": + dict["nonceIOS"] = nonce_ios + if signature_ios != "": + dict["signatureIOS"] = signature_ios + if timestamp_ios != 0.0: + dict["timestampIOS"] = timestamp_ios + if number_of_periods_ios != 0: + dict["numberOfPeriodsIOS"] = number_of_periods_ios + if localized_price_ios != "": + dict["localizedPriceIOS"] = localized_price_ios + if base_plan_id_android != "": + dict["basePlanIdAndroid"] = base_plan_id_android + if offer_token_android != "": + dict["offerTokenAndroid"] = offer_token_android + if offer_tags_android != []: + dict["offerTagsAndroid"] = offer_tags_android if pricing_phases_android != null and pricing_phases_android.has_method("to_dict"): dict["pricingPhasesAndroid"] = pricing_phases_android.to_dict() else: @@ -2804,10 +2915,14 @@ class VerifyPurchaseResultAndroid: var dict = {} dict["autoRenewing"] = auto_renewing dict["betaProduct"] = beta_product - dict["cancelDate"] = cancel_date - dict["cancelReason"] = cancel_reason - dict["deferredDate"] = deferred_date - dict["deferredSku"] = deferred_sku + if cancel_date != 0.0: + dict["cancelDate"] = cancel_date + if cancel_reason != "": + dict["cancelReason"] = cancel_reason + if deferred_date != 0.0: + dict["deferredDate"] = deferred_date + if deferred_sku != "": + dict["deferredSku"] = deferred_sku dict["freeTrialEndDate"] = free_trial_end_date dict["gracePeriodEndDate"] = grace_period_end_date dict["parentProductId"] = parent_product_id @@ -2840,7 +2955,8 @@ class VerifyPurchaseResultHorizon: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["grantTime"] = grant_time + if grant_time != 0.0: + dict["grantTime"] = grant_time return dict class VerifyPurchaseResultIOS: @@ -2888,7 +3004,8 @@ class VerifyPurchaseWithProviderError: func to_dict() -> Dictionary: var dict = {} dict["message"] = message - dict["code"] = code + if code != "": + dict["code"] = code return dict class VerifyPurchaseWithProviderResult: @@ -4131,6 +4248,7 @@ const ERROR_CODE_VALUES = { ErrorCode.CONNECTION_CLOSED: "connection-closed", ErrorCode.INIT_CONNECTION: "init-connection", ErrorCode.SERVICE_DISCONNECTED: "service-disconnected", + ErrorCode.SERVICE_TIMEOUT: "service-timeout", ErrorCode.QUERY_PRODUCT: "query-product", ErrorCode.SKU_NOT_FOUND: "sku-not-found", ErrorCode.SKU_OFFER_MISMATCH: "sku-offer-mismatch", @@ -4347,6 +4465,7 @@ const ERROR_CODE_FROM_STRING = { "connection-closed": ErrorCode.CONNECTION_CLOSED, "init-connection": ErrorCode.INIT_CONNECTION, "service-disconnected": ErrorCode.SERVICE_DISCONNECTED, + "service-timeout": ErrorCode.SERVICE_TIMEOUT, "query-product": ErrorCode.QUERY_PRODUCT, "sku-not-found": ErrorCode.SKU_NOT_FOUND, "sku-offer-mismatch": ErrorCode.SKU_OFFER_MISMATCH, diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index dabfb9b6..81b1f73b 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -350,6 +350,7 @@ export enum ErrorCode { RemoteError = 'remote-error', ServiceDisconnected = 'service-disconnected', ServiceError = 'service-error', + ServiceTimeout = 'service-timeout', SkuNotFound = 'sku-not-found', SkuOfferMismatch = 'sku-offer-mismatch', SyncError = 'sync-error', From 80798c6c2b10c63778022c3f3ee7fc8e16a4c9b6 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 20:13:54 +0900 Subject: [PATCH 4/9] fix(ci): add ServiceTimeout to rn-iap/expo-iap ErrorCode mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2nd-round review added `ServiceTimeout` to the shared ErrorCode enum but missed the two framework-side `Record` maps that enforce an exhaustive set at compile time, breaking `libraries/react-native-iap` and `libraries/expo-iap` CI on `tsc` (TS2741). Add the missing entry so both maps stay exhaustive. Also revert the incidental ktlint reformatting that `bun install` ran against `libraries/expo-iap/android/**` / example / plugin — those diffs were picked up by `expo-module prepare`'s prepare script and are unrelated to this PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- libraries/expo-iap/src/utils/errorMapping.ts | 1 + libraries/react-native-iap/src/utils/errorMapping.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/libraries/expo-iap/src/utils/errorMapping.ts b/libraries/expo-iap/src/utils/errorMapping.ts index 2dda918e..afc44001 100644 --- a/libraries/expo-iap/src/utils/errorMapping.ts +++ b/libraries/expo-iap/src/utils/errorMapping.ts @@ -80,6 +80,7 @@ const COMMON_ERROR_CODE_MAP: Record = { [ErrorCode.BillingUnavailable]: ErrorCode.BillingUnavailable, [ErrorCode.FeatureNotSupported]: ErrorCode.FeatureNotSupported, [ErrorCode.DuplicatePurchase]: ErrorCode.DuplicatePurchase, + [ErrorCode.ServiceTimeout]: ErrorCode.ServiceTimeout, [ErrorCode.EmptySkuList]: ErrorCode.EmptySkuList, [ErrorCode.PurchaseVerificationFailed]: ErrorCode.PurchaseVerificationFailed, [ErrorCode.PurchaseVerificationFinishFailed]: diff --git a/libraries/react-native-iap/src/utils/errorMapping.ts b/libraries/react-native-iap/src/utils/errorMapping.ts index 00798eba..9a93a6c7 100644 --- a/libraries/react-native-iap/src/utils/errorMapping.ts +++ b/libraries/react-native-iap/src/utils/errorMapping.ts @@ -104,6 +104,7 @@ const COMMON_ERROR_CODE_MAP: Record = { [ErrorCode.BillingUnavailable]: ErrorCode.BillingUnavailable, [ErrorCode.FeatureNotSupported]: ErrorCode.FeatureNotSupported, [ErrorCode.DuplicatePurchase]: ErrorCode.DuplicatePurchase, + [ErrorCode.ServiceTimeout]: ErrorCode.ServiceTimeout, [ErrorCode.EmptySkuList]: ErrorCode.EmptySkuList, [ErrorCode.PurchaseVerificationFailed]: ErrorCode.PurchaseVerificationFailed, [ErrorCode.PurchaseVerificationFinishFailed]: From 4c5011f9a179e7e0bd9bade35ecd0aa30195b846 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 20:40:40 +0900 Subject: [PATCH 5/9] fix(review): 3rd-round PR #98 fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GDScript codegen: nullable scalar fields now declare as \`var x: Variant = null\` instead of typed-with-sentinel-default. The previous "omit when equal to default" approach from the last round was lossy for booleans/ints — \`false\` is a real value, not "unset". Using Variant lets the field actually hold null so from_dict/to_dict round-trips preserve the null-vs-default distinction without dropping legitimate zero/false/empty values. - KMP ErrorMapping: add \`E_SERVICE_TIMEOUT\` / \`SERVICE_TIMEOUT\` and \`E_DUPLICATE_PURCHASE\` / \`DUPLICATE_PURCHASE\` legacy aliases to \`legacyCodeMap\` so the Android path doesn't collapse those codes to \`ErrorCode.Unknown\`. - OpenIapModule (play): the reporting-details catch block and the outer \`createBillingProgramReportingDetails\` exception handler now fall back to \`e.javaClass.simpleName\` when \`e.message\` is null, so \`PurchaseFailed.debugMessage\` always carries something diagnostic instead of just null. - sync-to-platforms.mjs: the KMP package-insertion now walks the file line-by-line and splices the package declaration immediately after the LAST \`@file:\` annotation. The previous single-line regex misplaced the package on files with leading comments (it fell through to prepend-mode, putting the package before the file annotations, which Kotlin rejects). The new approach also handles future additions of multiple \`@file:\` lines without breakage. Co-Authored-By: Claude Opus 4.6 (1M context) --- libraries/godot-iap/addons/godot-iap/types.gd | 626 +++++++----------- .../hyochan/kmpiap/utils/ErrorMapping.kt | 2 + .../java/dev/hyo/openiap/OpenIapModule.kt | 4 +- packages/gql/codegen/plugins/gdscript.ts | 50 +- packages/gql/scripts/sync-to-platforms.mjs | 27 +- packages/gql/src/generated/types.gd | 626 +++++++----------- 6 files changed, 570 insertions(+), 765 deletions(-) diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 615e7ebe..18582a23 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -300,20 +300,20 @@ enum SubscriptionReplacementModeAndroid { class ActiveSubscription: var product_id: String = "" var is_active: bool = false - var expiration_date_ios: float = 0.0 - var auto_renewing_android: bool = false - var environment_ios: String = "" + var expiration_date_ios: Variant = null + var auto_renewing_android: Variant = null + var environment_ios: Variant = null ## @deprecated iOS only - use daysUntilExpirationIOS instead. - var will_expire_soon: bool = false - var days_until_expiration_ios: float = 0.0 + var will_expire_soon: Variant = null + var days_until_expiration_ios: Variant = null var transaction_id: String = "" - var purchase_token: String = "" + var purchase_token: Variant = null var transaction_date: float = 0.0 - var base_plan_id_android: String = "" + var base_plan_id_android: Variant = null ## Required for subscription upgrade/downgrade on Android - var purchase_token_android: String = "" + var purchase_token_android: Variant = null ## The current plan identifier. This is: - var current_plan_id: String = "" + var current_plan_id: Variant = null ## Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, var renewal_info_ios: RenewalInfoIOS @@ -356,26 +356,17 @@ class ActiveSubscription: var dict = {} dict["productId"] = product_id dict["isActive"] = is_active - if expiration_date_ios != 0.0: - dict["expirationDateIOS"] = expiration_date_ios - if auto_renewing_android != false: - dict["autoRenewingAndroid"] = auto_renewing_android - if environment_ios != "": - dict["environmentIOS"] = environment_ios - if will_expire_soon != false: - dict["willExpireSoon"] = will_expire_soon - if days_until_expiration_ios != 0.0: - dict["daysUntilExpirationIOS"] = days_until_expiration_ios + dict["expirationDateIOS"] = expiration_date_ios + dict["autoRenewingAndroid"] = auto_renewing_android + dict["environmentIOS"] = environment_ios + dict["willExpireSoon"] = will_expire_soon + dict["daysUntilExpirationIOS"] = days_until_expiration_ios dict["transactionId"] = transaction_id - if purchase_token != "": - dict["purchaseToken"] = purchase_token + dict["purchaseToken"] = purchase_token dict["transactionDate"] = transaction_date - if base_plan_id_android != "": - dict["basePlanIdAndroid"] = base_plan_id_android - if purchase_token_android != "": - dict["purchaseTokenAndroid"] = purchase_token_android - if current_plan_id != "": - dict["currentPlanId"] = current_plan_id + dict["basePlanIdAndroid"] = base_plan_id_android + dict["purchaseTokenAndroid"] = purchase_token_android + dict["currentPlanId"] = current_plan_id if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -393,9 +384,9 @@ class AppTransaction: var signed_date: float = 0.0 var app_id: float = 0.0 var app_version_id: float = 0.0 - var preorder_date: float = 0.0 - var app_transaction_id: String = "" - var original_platform: String = "" + var preorder_date: Variant = null + var app_transaction_id: Variant = null + var original_platform: Variant = null static func from_dict(data: Dictionary) -> AppTransaction: var obj = AppTransaction.new() @@ -439,12 +430,9 @@ class AppTransaction: dict["signedDate"] = signed_date dict["appId"] = app_id dict["appVersionId"] = app_version_id - if preorder_date != 0.0: - dict["preorderDate"] = preorder_date - if app_transaction_id != "": - dict["appTransactionId"] = app_transaction_id - if original_platform != "": - dict["originalPlatform"] = original_platform + dict["preorderDate"] = preorder_date + dict["appTransactionId"] = app_transaction_id + dict["originalPlatform"] = original_platform return dict ## Result of checking billing program availability (Android) Available in Google Play Billing Library 8.2.0+ @@ -508,7 +496,7 @@ class BillingResultAndroid: ## The response code from the billing operation var response_code: int = 0 ## Debug message from the billing library - var debug_message: String = "" + var debug_message: Variant = null ## Sub-response code for more granular error information (8.0+). var sub_response_code: SubResponseCodeAndroid @@ -529,8 +517,7 @@ class BillingResultAndroid: func to_dict() -> Dictionary: var dict = {} dict["responseCode"] = response_code - if debug_message != "": - dict["debugMessage"] = debug_message + dict["debugMessage"] = debug_message if SUB_RESPONSE_CODE_ANDROID_VALUES.has(sub_response_code): dict["subResponseCode"] = SUB_RESPONSE_CODE_ANDROID_VALUES[sub_response_code] else: @@ -577,7 +564,7 @@ class DiscountAmountAndroid: ## Discount display information for one-time purchase offers (Android) Available in Google Play Billing Library 7.0+ class DiscountDisplayInfoAndroid: ## Percentage discount (e.g., 33 for 33% off) - var percentage_discount: int = 0 + var percentage_discount: Variant = null ## Absolute discount amount details var discount_amount: DiscountAmountAndroid @@ -594,8 +581,7 @@ class DiscountDisplayInfoAndroid: func to_dict() -> Dictionary: var dict = {} - if percentage_discount != 0: - dict["percentageDiscount"] = percentage_discount + dict["percentageDiscount"] = percentage_discount if discount_amount != null and discount_amount.has_method("to_dict"): dict["discountAmount"] = discount_amount.to_dict() else: @@ -611,7 +597,7 @@ class DiscountIOS: var price_amount: float = 0.0 var payment_mode: PaymentModeIOS var subscription_period: String = "" - var localized_price: String = "" + var localized_price: Variant = null static func from_dict(data: Dictionary) -> DiscountIOS: var obj = DiscountIOS.new() @@ -649,14 +635,13 @@ class DiscountIOS: else: dict["paymentMode"] = payment_mode dict["subscriptionPeriod"] = subscription_period - if localized_price != "": - dict["localizedPrice"] = localized_price + dict["localizedPrice"] = localized_price return dict ## Standardized one-time product discount offer. Provides a unified interface for one-time purchase discounts across platforms. Currently supported on Android (Google Play Billing 7.0+). iOS does not support one-time purchase discounts in the same way. @see https://openiap.dev/docs/features/discount class DiscountOffer: ## Unique identifier for the offer. - var id: String = "" + var id: Variant = null ## Formatted display price string (e.g., "$4.99") var display_price: String = "" ## Numeric price value @@ -666,17 +651,17 @@ class DiscountOffer: ## Type of discount offer var type: DiscountOfferType ## [Android] Offer token required for purchase. - var offer_token_android: String = "" + var offer_token_android: Variant = null ## [Android] List of tags associated with this offer. var offer_tags_android: Array[String] = [] ## [Android] Original full price in micro-units before discount. - var full_price_micros_android: String = "" + var full_price_micros_android: Variant = null ## [Android] Percentage discount (e.g., 33 for 33% off). - var percentage_discount_android: int = 0 + var percentage_discount_android: Variant = null ## [Android] Fixed discount amount in micro-units. - var discount_amount_micros_android: String = "" + var discount_amount_micros_android: Variant = null ## [Android] Formatted discount amount string (e.g., "$5.00 OFF"). - var formatted_discount_amount_android: String = "" + var formatted_discount_amount_android: Variant = null ## [Android] Valid time window for the offer. var valid_time_window_android: ValidTimeWindowAndroid ## [Android] Limited quantity information. @@ -686,7 +671,7 @@ class DiscountOffer: ## [Android] Rental details if this is a rental offer. var rental_details_android: RentalDetailsAndroid ## [Android] Purchase option ID for this offer. - var purchase_option_id_android: String = "" + var purchase_option_id_android: Variant = null static func from_dict(data: Dictionary) -> DiscountOffer: var obj = DiscountOffer.new() @@ -742,8 +727,7 @@ class DiscountOffer: func to_dict() -> Dictionary: var dict = {} - if id != "": - dict["id"] = id + dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price dict["currency"] = currency @@ -751,18 +735,12 @@ class DiscountOffer: dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: dict["type"] = type - if offer_token_android != "": - dict["offerTokenAndroid"] = offer_token_android - if offer_tags_android != []: - dict["offerTagsAndroid"] = offer_tags_android - if full_price_micros_android != "": - dict["fullPriceMicrosAndroid"] = full_price_micros_android - if percentage_discount_android != 0: - dict["percentageDiscountAndroid"] = percentage_discount_android - if discount_amount_micros_android != "": - dict["discountAmountMicrosAndroid"] = discount_amount_micros_android - if formatted_discount_amount_android != "": - dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android + dict["offerTokenAndroid"] = offer_token_android + dict["offerTagsAndroid"] = offer_tags_android + dict["fullPriceMicrosAndroid"] = full_price_micros_android + dict["percentageDiscountAndroid"] = percentage_discount_android + dict["discountAmountMicrosAndroid"] = discount_amount_micros_android + dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android if valid_time_window_android != null and valid_time_window_android.has_method("to_dict"): dict["validTimeWindowAndroid"] = valid_time_window_android.to_dict() else: @@ -779,8 +757,7 @@ class DiscountOffer: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - if purchase_option_id_android != "": - dict["purchaseOptionIdAndroid"] = purchase_option_id_android + dict["purchaseOptionIdAndroid"] = purchase_option_id_android return dict ## iOS DiscountOffer (output type). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer @@ -878,7 +855,7 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: ## Whether the user chose to continue to external purchase var continued: bool = false ## Optional error message if the presentation failed - var error: String = "" + var error: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseCustomLinkNoticeResultIOS: var obj = ExternalPurchaseCustomLinkNoticeResultIOS.new() @@ -891,16 +868,15 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: func to_dict() -> Dictionary: var dict = {} dict["continued"] = continued - if error != "": - dict["error"] = error + dict["error"] = error return dict ## Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). class ExternalPurchaseCustomLinkTokenResultIOS: ## The external purchase token string. - var token: String = "" + var token: Variant = null ## Optional error message if token retrieval failed - var error: String = "" + var error: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseCustomLinkTokenResultIOS: var obj = ExternalPurchaseCustomLinkTokenResultIOS.new() @@ -912,10 +888,8 @@ class ExternalPurchaseCustomLinkTokenResultIOS: func to_dict() -> Dictionary: var dict = {} - if token != "": - dict["token"] = token - if error != "": - dict["error"] = error + dict["token"] = token + dict["error"] = error return dict ## Result of presenting an external purchase link @@ -923,7 +897,7 @@ class ExternalPurchaseLinkResultIOS: ## Whether the user completed the external purchase flow var success: bool = false ## Optional error message if the presentation failed - var error: String = "" + var error: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseLinkResultIOS: var obj = ExternalPurchaseLinkResultIOS.new() @@ -936,8 +910,7 @@ class ExternalPurchaseLinkResultIOS: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - if error != "": - dict["error"] = error + dict["error"] = error return dict ## Result of presenting external purchase notice sheet (iOS 17.4+) Returns the token when user continues to external purchase. @@ -945,9 +918,9 @@ class ExternalPurchaseNoticeResultIOS: ## Notice result indicating user action var result: ExternalPurchaseNoticeAction ## Optional error message if the presentation failed - var error: String = "" + var error: Variant = null ## External purchase token returned when user continues (iOS 17.4+). - var external_purchase_token: String = "" + var external_purchase_token: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseNoticeResultIOS: var obj = ExternalPurchaseNoticeResultIOS.new() @@ -969,10 +942,8 @@ class ExternalPurchaseNoticeResultIOS: dict["result"] = EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES[result] else: dict["result"] = result - if error != "": - dict["error"] = error - if external_purchase_token != "": - dict["externalPurchaseToken"] = external_purchase_token + dict["error"] = error + dict["externalPurchaseToken"] = external_purchase_token return dict ## Installment plan details for subscription offers (Android) Contains information about the installment plan commitment. Available in Google Play Billing Library 7.0+ @@ -1127,11 +1098,11 @@ class ProductAndroid: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var name_android: String = "" ## Product-level status code indicating fetch result (Android 8.0+) @@ -1226,14 +1197,11 @@ class ProductAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1288,7 +1256,7 @@ class ProductAndroid: ## One-time purchase offer details (Android). Available in Google Play Billing Library 7.0+ @deprecated Use the standardized DiscountOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#discount-offer class ProductAndroidOneTimePurchaseOfferDetail: ## Offer ID - var offer_id: String = "" + var offer_id: Variant = null ## Offer token for use in BillingFlowParams when purchasing var offer_token: String = "" ## List of offer tags @@ -1297,7 +1265,7 @@ class ProductAndroidOneTimePurchaseOfferDetail: var formatted_price: String = "" var price_amount_micros: String = "" ## Full (non-discounted) price in micro-units - var full_price_micros: String = "" + var full_price_micros: Variant = null ## Discount display information var discount_display_info: DiscountDisplayInfoAndroid ## Valid time window for the offer @@ -1309,7 +1277,7 @@ class ProductAndroidOneTimePurchaseOfferDetail: ## Rental details for rental offers var rental_details_android: RentalDetailsAndroid ## Purchase option ID for this offer (Android) - var purchase_option_id: String = "" + var purchase_option_id: Variant = null static func from_dict(data: Dictionary) -> ProductAndroidOneTimePurchaseOfferDetail: var obj = ProductAndroidOneTimePurchaseOfferDetail.new() @@ -1358,15 +1326,13 @@ class ProductAndroidOneTimePurchaseOfferDetail: func to_dict() -> Dictionary: var dict = {} - if offer_id != "": - dict["offerId"] = offer_id + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags dict["priceCurrencyCode"] = price_currency_code dict["formattedPrice"] = formatted_price dict["priceAmountMicros"] = price_amount_micros - if full_price_micros != "": - dict["fullPriceMicros"] = full_price_micros + dict["fullPriceMicros"] = full_price_micros if discount_display_info != null and discount_display_info.has_method("to_dict"): dict["discountDisplayInfo"] = discount_display_info.to_dict() else: @@ -1387,8 +1353,7 @@ class ProductAndroidOneTimePurchaseOfferDetail: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - if purchase_option_id != "": - dict["purchaseOptionId"] = purchase_option_id + dict["purchaseOptionId"] = purchase_option_id return dict class ProductIOS: @@ -1396,11 +1361,11 @@ class ProductIOS: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var display_name_ios: String = "" var is_family_shareable_ios: bool = false @@ -1477,14 +1442,11 @@ class ProductIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1517,11 +1479,11 @@ class ProductSubscriptionAndroid: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var name_android: String = "" ## Product-level status code indicating fetch result (Android 8.0+) @@ -1616,14 +1578,11 @@ class ProductSubscriptionAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1678,7 +1637,7 @@ class ProductSubscriptionAndroid: ## Subscription offer details (Android). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer class ProductSubscriptionAndroidOfferDetails: var base_plan_id: String = "" - var offer_id: String = "" + var offer_id: Variant = null var offer_token: String = "" var offer_tags: Array[String] = [] var pricing_phases: PricingPhasesAndroid @@ -1710,8 +1669,7 @@ class ProductSubscriptionAndroidOfferDetails: func to_dict() -> Dictionary: var dict = {} dict["basePlanId"] = base_plan_id - if offer_id != "": - dict["offerId"] = offer_id + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags if pricing_phases != null and pricing_phases.has_method("to_dict"): @@ -1729,11 +1687,11 @@ class ProductSubscriptionIOS: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var display_name_ios: String = "" var is_family_shareable_ios: bool = false @@ -1745,12 +1703,12 @@ class ProductSubscriptionIOS: var subscription_info_ios: SubscriptionInfoIOS ## @deprecated Use subscriptionOffers instead for cross-platform compatibility. var discounts_ios: Array[DiscountIOS] = [] - var introductory_price_ios: String = "" - var introductory_price_as_amount_ios: String = "" + var introductory_price_ios: Variant = null + var introductory_price_as_amount_ios: Variant = null var introductory_price_payment_mode_ios: PaymentModeIOS - var introductory_price_number_of_periods_ios: String = "" + var introductory_price_number_of_periods_ios: Variant = null var introductory_price_subscription_period_ios: SubscriptionPeriodIOS - var subscription_period_number_ios: String = "" + var subscription_period_number_ios: Variant = null var subscription_period_unit_ios: SubscriptionPeriodIOS static func from_dict(data: Dictionary) -> ProductSubscriptionIOS: @@ -1853,14 +1811,11 @@ class ProductSubscriptionIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1896,22 +1851,18 @@ class ProductSubscriptionIOS: dict["discountsIOS"] = arr else: dict["discountsIOS"] = null - if introductory_price_ios != "": - dict["introductoryPriceIOS"] = introductory_price_ios - if introductory_price_as_amount_ios != "": - dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios + dict["introductoryPriceIOS"] = introductory_price_ios + dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios if PAYMENT_MODE_IOS_VALUES.has(introductory_price_payment_mode_ios): dict["introductoryPricePaymentModeIOS"] = PAYMENT_MODE_IOS_VALUES[introductory_price_payment_mode_ios] else: dict["introductoryPricePaymentModeIOS"] = introductory_price_payment_mode_ios - if introductory_price_number_of_periods_ios != "": - dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios + dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(introductory_price_subscription_period_ios): dict["introductoryPriceSubscriptionPeriodIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[introductory_price_subscription_period_ios] else: dict["introductoryPriceSubscriptionPeriodIOS"] = introductory_price_subscription_period_ios - if subscription_period_number_ios != "": - dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios + dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(subscription_period_unit_ios): dict["subscriptionPeriodUnitIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[subscription_period_unit_ios] else: @@ -1922,26 +1873,26 @@ class PurchaseAndroid: var id: String = "" var product_id: String = "" var ids: Array[String] = [] - var transaction_id: String = "" + var transaction_id: Variant = null var transaction_date: float = 0.0 - var purchase_token: String = "" + var purchase_token: Variant = null ## Store where purchase was made var store: IapStore var platform: IapPlatform var quantity: int = 0 var purchase_state: PurchaseState var is_auto_renewing: bool = false - var current_plan_id: String = "" - var data_android: String = "" - var signature_android: String = "" - var auto_renewing_android: bool = false - var is_acknowledged_android: bool = false - var package_name_android: String = "" - var developer_payload_android: String = "" - var obfuscated_account_id_android: String = "" - var obfuscated_profile_id_android: String = "" + var current_plan_id: Variant = null + var data_android: Variant = null + var signature_android: Variant = null + var auto_renewing_android: Variant = null + var is_acknowledged_android: Variant = null + var package_name_android: Variant = null + var developer_payload_android: Variant = null + var obfuscated_account_id_android: Variant = null + var obfuscated_profile_id_android: Variant = null ## Whether the subscription is suspended (Android) - var is_suspended_android: bool = false + var is_suspended_android: Variant = null ## Pending purchase update for uncommitted subscription upgrade/downgrade (Android) var pending_purchase_update_android: PendingPurchaseUpdateAndroid @@ -2012,13 +1963,10 @@ class PurchaseAndroid: var dict = {} dict["id"] = id dict["productId"] = product_id - if ids != []: - dict["ids"] = ids - if transaction_id != "": - dict["transactionId"] = transaction_id + dict["ids"] = ids + dict["transactionId"] = transaction_id dict["transactionDate"] = transaction_date - if purchase_token != "": - dict["purchaseToken"] = purchase_token + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2033,26 +1981,16 @@ class PurchaseAndroid: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - if current_plan_id != "": - dict["currentPlanId"] = current_plan_id - if data_android != "": - dict["dataAndroid"] = data_android - if signature_android != "": - dict["signatureAndroid"] = signature_android - if auto_renewing_android != false: - dict["autoRenewingAndroid"] = auto_renewing_android - if is_acknowledged_android != false: - dict["isAcknowledgedAndroid"] = is_acknowledged_android - if package_name_android != "": - dict["packageNameAndroid"] = package_name_android - if developer_payload_android != "": - dict["developerPayloadAndroid"] = developer_payload_android - if obfuscated_account_id_android != "": - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - if obfuscated_profile_id_android != "": - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - if is_suspended_android != false: - dict["isSuspendedAndroid"] = is_suspended_android + dict["currentPlanId"] = current_plan_id + dict["dataAndroid"] = data_android + dict["signatureAndroid"] = signature_android + dict["autoRenewingAndroid"] = auto_renewing_android + dict["isAcknowledgedAndroid"] = is_acknowledged_android + dict["packageNameAndroid"] = package_name_android + dict["developerPayloadAndroid"] = developer_payload_android + dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android + dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android + dict["isSuspendedAndroid"] = is_suspended_android if pending_purchase_update_android != null and pending_purchase_update_android.has_method("to_dict"): dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android.to_dict() else: @@ -2062,8 +2000,8 @@ class PurchaseAndroid: class PurchaseError: var code: ErrorCode var message: String = "" - var product_id: String = "" - var debug_message: String = "" + var product_id: Variant = null + var debug_message: Variant = null static func from_dict(data: Dictionary) -> PurchaseError: var obj = PurchaseError.new() @@ -2088,10 +2026,8 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - if product_id != "": - dict["productId"] = product_id - if debug_message != "": - dict["debugMessage"] = debug_message + dict["productId"] = product_id + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2099,36 +2035,36 @@ class PurchaseIOS: var product_id: String = "" var ids: Array[String] = [] var transaction_date: float = 0.0 - var purchase_token: String = "" + var purchase_token: Variant = null ## Store where purchase was made var store: IapStore var platform: IapPlatform var quantity: int = 0 var purchase_state: PurchaseState var is_auto_renewing: bool = false - var current_plan_id: String = "" + var current_plan_id: Variant = null var transaction_id: String = "" - var quantity_ios: int = 0 - var original_transaction_date_ios: float = 0.0 - var original_transaction_identifier_ios: String = "" - var app_account_token: String = "" - var expiration_date_ios: float = 0.0 - var web_order_line_item_id_ios: String = "" - var environment_ios: String = "" - var storefront_country_code_ios: String = "" - var app_bundle_id_ios: String = "" - var subscription_group_id_ios: String = "" - var is_upgraded_ios: bool = false - var ownership_type_ios: String = "" - var reason_ios: String = "" - var reason_string_representation_ios: String = "" - var transaction_reason_ios: String = "" - var revocation_date_ios: float = 0.0 - var revocation_reason_ios: String = "" + var quantity_ios: Variant = null + var original_transaction_date_ios: Variant = null + var original_transaction_identifier_ios: Variant = null + var app_account_token: Variant = null + var expiration_date_ios: Variant = null + var web_order_line_item_id_ios: Variant = null + var environment_ios: Variant = null + var storefront_country_code_ios: Variant = null + var app_bundle_id_ios: Variant = null + var subscription_group_id_ios: Variant = null + var is_upgraded_ios: Variant = null + var ownership_type_ios: Variant = null + var reason_ios: Variant = null + var reason_string_representation_ios: Variant = null + var transaction_reason_ios: Variant = null + var revocation_date_ios: Variant = null + var revocation_reason_ios: Variant = null var offer_ios: PurchaseOfferIOS - var currency_code_ios: String = "" - var currency_symbol_ios: String = "" - var country_code_ios: String = "" + var currency_code_ios: Variant = null + var currency_symbol_ios: Variant = null + var country_code_ios: Variant = null var renewal_info_ios: RenewalInfoIOS static func from_dict(data: Dictionary) -> PurchaseIOS: @@ -2225,11 +2161,9 @@ class PurchaseIOS: var dict = {} dict["id"] = id dict["productId"] = product_id - if ids != []: - dict["ids"] = ids + dict["ids"] = ids dict["transactionDate"] = transaction_date - if purchase_token != "": - dict["purchaseToken"] = purchase_token + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2244,53 +2178,32 @@ class PurchaseIOS: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - if current_plan_id != "": - dict["currentPlanId"] = current_plan_id + dict["currentPlanId"] = current_plan_id dict["transactionId"] = transaction_id - if quantity_ios != 0: - dict["quantityIOS"] = quantity_ios - if original_transaction_date_ios != 0.0: - dict["originalTransactionDateIOS"] = original_transaction_date_ios - if original_transaction_identifier_ios != "": - dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios - if app_account_token != "": - dict["appAccountToken"] = app_account_token - if expiration_date_ios != 0.0: - dict["expirationDateIOS"] = expiration_date_ios - if web_order_line_item_id_ios != "": - dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios - if environment_ios != "": - dict["environmentIOS"] = environment_ios - if storefront_country_code_ios != "": - dict["storefrontCountryCodeIOS"] = storefront_country_code_ios - if app_bundle_id_ios != "": - dict["appBundleIdIOS"] = app_bundle_id_ios - if subscription_group_id_ios != "": - dict["subscriptionGroupIdIOS"] = subscription_group_id_ios - if is_upgraded_ios != false: - dict["isUpgradedIOS"] = is_upgraded_ios - if ownership_type_ios != "": - dict["ownershipTypeIOS"] = ownership_type_ios - if reason_ios != "": - dict["reasonIOS"] = reason_ios - if reason_string_representation_ios != "": - dict["reasonStringRepresentationIOS"] = reason_string_representation_ios - if transaction_reason_ios != "": - dict["transactionReasonIOS"] = transaction_reason_ios - if revocation_date_ios != 0.0: - dict["revocationDateIOS"] = revocation_date_ios - if revocation_reason_ios != "": - dict["revocationReasonIOS"] = revocation_reason_ios + dict["quantityIOS"] = quantity_ios + dict["originalTransactionDateIOS"] = original_transaction_date_ios + dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios + dict["appAccountToken"] = app_account_token + dict["expirationDateIOS"] = expiration_date_ios + dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios + dict["environmentIOS"] = environment_ios + dict["storefrontCountryCodeIOS"] = storefront_country_code_ios + dict["appBundleIdIOS"] = app_bundle_id_ios + dict["subscriptionGroupIdIOS"] = subscription_group_id_ios + dict["isUpgradedIOS"] = is_upgraded_ios + dict["ownershipTypeIOS"] = ownership_type_ios + dict["reasonIOS"] = reason_ios + dict["reasonStringRepresentationIOS"] = reason_string_representation_ios + dict["transactionReasonIOS"] = transaction_reason_ios + dict["revocationDateIOS"] = revocation_date_ios + dict["revocationReasonIOS"] = revocation_reason_ios if offer_ios != null and offer_ios.has_method("to_dict"): dict["offerIOS"] = offer_ios.to_dict() else: dict["offerIOS"] = offer_ios - if currency_code_ios != "": - dict["currencyCodeIOS"] = currency_code_ios - if currency_symbol_ios != "": - dict["currencySymbolIOS"] = currency_symbol_ios - if country_code_ios != "": - dict["countryCodeIOS"] = country_code_ios + dict["currencyCodeIOS"] = currency_code_ios + dict["currencySymbolIOS"] = currency_symbol_ios + dict["countryCodeIOS"] = country_code_ios if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -2321,7 +2234,7 @@ class PurchaseOfferIOS: class RefundResultIOS: var status: String = "" - var message: String = "" + var message: Variant = null static func from_dict(data: Dictionary) -> RefundResultIOS: var obj = RefundResultIOS.new() @@ -2334,31 +2247,30 @@ class RefundResultIOS: func to_dict() -> Dictionary: var dict = {} dict["status"] = status - if message != "": - dict["message"] = message + dict["message"] = message return dict ## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo class RenewalInfoIOS: - var json_representation: String = "" + var json_representation: Variant = null var will_auto_renew: bool = false - var auto_renew_preference: String = "" + var auto_renew_preference: Variant = null ## When subscription expires due to cancellation/billing issue - var expiration_reason: String = "" + var expiration_reason: Variant = null ## Grace period expiration date (milliseconds since epoch) - var grace_period_expiration_date: float = 0.0 + var grace_period_expiration_date: Variant = null ## True if subscription failed to renew due to billing issue and is retrying - var is_in_billing_retry: bool = false + var is_in_billing_retry: Variant = null ## Product ID that will be used on next renewal (when user upgrades/downgrades) - var pending_upgrade_product_id: String = "" + var pending_upgrade_product_id: Variant = null ## User's response to subscription price increase - var price_increase_status: String = "" + var price_increase_status: Variant = null ## Expected renewal date (milliseconds since epoch) - var renewal_date: float = 0.0 + var renewal_date: Variant = null ## Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) - var renewal_offer_id: String = "" + var renewal_offer_id: Variant = null ## Type of offer applied to next renewal - var renewal_offer_type: String = "" + var renewal_offer_type: Variant = null static func from_dict(data: Dictionary) -> RenewalInfoIOS: var obj = RenewalInfoIOS.new() @@ -2388,27 +2300,17 @@ class RenewalInfoIOS: func to_dict() -> Dictionary: var dict = {} - if json_representation != "": - dict["jsonRepresentation"] = json_representation + dict["jsonRepresentation"] = json_representation dict["willAutoRenew"] = will_auto_renew - if auto_renew_preference != "": - dict["autoRenewPreference"] = auto_renew_preference - if expiration_reason != "": - dict["expirationReason"] = expiration_reason - if grace_period_expiration_date != 0.0: - dict["gracePeriodExpirationDate"] = grace_period_expiration_date - if is_in_billing_retry != false: - dict["isInBillingRetry"] = is_in_billing_retry - if pending_upgrade_product_id != "": - dict["pendingUpgradeProductId"] = pending_upgrade_product_id - if price_increase_status != "": - dict["priceIncreaseStatus"] = price_increase_status - if renewal_date != 0.0: - dict["renewalDate"] = renewal_date - if renewal_offer_id != "": - dict["renewalOfferId"] = renewal_offer_id - if renewal_offer_type != "": - dict["renewalOfferType"] = renewal_offer_type + dict["autoRenewPreference"] = auto_renew_preference + dict["expirationReason"] = expiration_reason + dict["gracePeriodExpirationDate"] = grace_period_expiration_date + dict["isInBillingRetry"] = is_in_billing_retry + dict["pendingUpgradeProductId"] = pending_upgrade_product_id + dict["priceIncreaseStatus"] = price_increase_status + dict["renewalDate"] = renewal_date + dict["renewalOfferId"] = renewal_offer_id + dict["renewalOfferType"] = renewal_offer_type return dict ## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+ @@ -2416,7 +2318,7 @@ class RentalDetailsAndroid: ## Rental period in ISO 8601 format (e.g., P7D for 7 days) var rental_period: String = "" ## Rental expiration period in ISO 8601 format - var rental_expiration_period: String = "" + var rental_expiration_period: Variant = null static func from_dict(data: Dictionary) -> RentalDetailsAndroid: var obj = RentalDetailsAndroid.new() @@ -2429,8 +2331,7 @@ class RentalDetailsAndroid: func to_dict() -> Dictionary: var dict = {} dict["rentalPeriod"] = rental_period - if rental_expiration_period != "": - dict["rentalExpirationPeriod"] = rental_expiration_period + dict["rentalExpirationPeriod"] = rental_expiration_period return dict class RequestVerifyPurchaseWithIapkitResult: @@ -2533,31 +2434,31 @@ class SubscriptionOffer: ## Numeric price value var price: float = 0.0 ## Currency code (ISO 4217, e.g., "USD") - var currency: String = "" + var currency: Variant = null ## Type of subscription offer (Introductory or Promotional) var type: DiscountOfferType ## Subscription period for this offer var period: SubscriptionPeriod ## Number of periods the offer applies - var period_count: int = 0 + var period_count: Variant = null ## Payment mode during the offer period var payment_mode: PaymentMode ## [iOS] Key identifier for signature validation. - var key_identifier_ios: String = "" + var key_identifier_ios: Variant = null ## [iOS] Cryptographic nonce (UUID) for signature validation. - var nonce_ios: String = "" + var nonce_ios: Variant = null ## [iOS] Server-generated signature for promotional offer validation. - var signature_ios: String = "" + var signature_ios: Variant = null ## [iOS] Timestamp when the signature was generated. - var timestamp_ios: float = 0.0 + var timestamp_ios: Variant = null ## [iOS] Number of billing periods for this discount. - var number_of_periods_ios: int = 0 + var number_of_periods_ios: Variant = null ## [iOS] Localized price string. - var localized_price_ios: String = "" + var localized_price_ios: Variant = null ## [Android] Base plan identifier. - var base_plan_id_android: String = "" + var base_plan_id_android: Variant = null ## [Android] Offer token required for purchase. - var offer_token_android: String = "" + var offer_token_android: Variant = null ## [Android] List of tags associated with this offer. var offer_tags_android: Array[String] = [] ## [Android] Pricing phases for this subscription offer. @@ -2629,8 +2530,7 @@ class SubscriptionOffer: dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price - if currency != "": - dict["currency"] = currency + dict["currency"] = currency if DISCOUNT_OFFER_TYPE_VALUES.has(type): dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: @@ -2639,30 +2539,20 @@ class SubscriptionOffer: dict["period"] = period.to_dict() else: dict["period"] = period - if period_count != 0: - dict["periodCount"] = period_count + dict["periodCount"] = period_count if PAYMENT_MODE_VALUES.has(payment_mode): dict["paymentMode"] = PAYMENT_MODE_VALUES[payment_mode] else: dict["paymentMode"] = payment_mode - if key_identifier_ios != "": - dict["keyIdentifierIOS"] = key_identifier_ios - if nonce_ios != "": - dict["nonceIOS"] = nonce_ios - if signature_ios != "": - dict["signatureIOS"] = signature_ios - if timestamp_ios != 0.0: - dict["timestampIOS"] = timestamp_ios - if number_of_periods_ios != 0: - dict["numberOfPeriodsIOS"] = number_of_periods_ios - if localized_price_ios != "": - dict["localizedPriceIOS"] = localized_price_ios - if base_plan_id_android != "": - dict["basePlanIdAndroid"] = base_plan_id_android - if offer_token_android != "": - dict["offerTokenAndroid"] = offer_token_android - if offer_tags_android != []: - dict["offerTagsAndroid"] = offer_tags_android + dict["keyIdentifierIOS"] = key_identifier_ios + dict["nonceIOS"] = nonce_ios + dict["signatureIOS"] = signature_ios + dict["timestampIOS"] = timestamp_ios + dict["numberOfPeriodsIOS"] = number_of_periods_ios + dict["localizedPriceIOS"] = localized_price_ios + dict["basePlanIdAndroid"] = base_plan_id_android + dict["offerTokenAndroid"] = offer_token_android + dict["offerTagsAndroid"] = offer_tags_android if pricing_phases_android != null and pricing_phases_android.has_method("to_dict"): dict["pricingPhasesAndroid"] = pricing_phases_android.to_dict() else: @@ -2854,10 +2744,10 @@ class ValidTimeWindowAndroid: class VerifyPurchaseResultAndroid: var auto_renewing: bool = false var beta_product: bool = false - var cancel_date: float = 0.0 - var cancel_reason: String = "" - var deferred_date: float = 0.0 - var deferred_sku: String = "" + var cancel_date: Variant = null + var cancel_reason: Variant = null + var deferred_date: Variant = null + var deferred_sku: Variant = null var free_trial_end_date: float = 0.0 var grace_period_end_date: float = 0.0 var parent_product_id: String = "" @@ -2915,14 +2805,10 @@ class VerifyPurchaseResultAndroid: var dict = {} dict["autoRenewing"] = auto_renewing dict["betaProduct"] = beta_product - if cancel_date != 0.0: - dict["cancelDate"] = cancel_date - if cancel_reason != "": - dict["cancelReason"] = cancel_reason - if deferred_date != 0.0: - dict["deferredDate"] = deferred_date - if deferred_sku != "": - dict["deferredSku"] = deferred_sku + dict["cancelDate"] = cancel_date + dict["cancelReason"] = cancel_reason + dict["deferredDate"] = deferred_date + dict["deferredSku"] = deferred_sku dict["freeTrialEndDate"] = free_trial_end_date dict["gracePeriodEndDate"] = grace_period_end_date dict["parentProductId"] = parent_product_id @@ -2942,7 +2828,7 @@ class VerifyPurchaseResultHorizon: ## Whether the entitlement verification succeeded. var success: bool = false ## Unix timestamp (seconds) when the entitlement was granted. - var grant_time: float = 0.0 + var grant_time: Variant = null static func from_dict(data: Dictionary) -> VerifyPurchaseResultHorizon: var obj = VerifyPurchaseResultHorizon.new() @@ -2955,8 +2841,7 @@ class VerifyPurchaseResultHorizon: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - if grant_time != 0.0: - dict["grantTime"] = grant_time + dict["grantTime"] = grant_time return dict class VerifyPurchaseResultIOS: @@ -2991,7 +2876,7 @@ class VerifyPurchaseResultIOS: class VerifyPurchaseWithProviderError: var message: String = "" - var code: String = "" + var code: Variant = null static func from_dict(data: Dictionary) -> VerifyPurchaseWithProviderError: var obj = VerifyPurchaseWithProviderError.new() @@ -3004,8 +2889,7 @@ class VerifyPurchaseWithProviderError: func to_dict() -> Dictionary: var dict = {} dict["message"] = message - if code != "": - dict["code"] = code + dict["code"] = code return dict class VerifyPurchaseWithProviderResult: @@ -3102,9 +2986,9 @@ class AndroidSubscriptionOfferInput: class DeepLinkOptions: ## Android SKU to open (required on Android) - var sku_android: String = "" + var sku_android: Variant = null ## Android package name to target (required on Android) - var package_name_android: String = "" + var package_name_android: Variant = null static func from_dict(data: Dictionary) -> DeepLinkOptions: var obj = DeepLinkOptions.new() @@ -3353,7 +3237,7 @@ class PurchaseInput: var product_id: String = "" var ids: Array[String] = [] var transaction_date: float = 0.0 - var purchase_token: String = "" + var purchase_token: Variant = null ## Store where purchase was made var store: IapStore ## @deprecated Use store instead @@ -3433,11 +3317,11 @@ class PurchaseInput: class PurchaseOptions: ## Also emit results through the iOS event listeners - var also_publish_to_event_listener_ios: bool = false + var also_publish_to_event_listener_ios: Variant = null ## Limit to currently active items on iOS - var only_include_active_items_ios: bool = false + var only_include_active_items_ios: Variant = null ## Include suspended subscriptions in the result (Android 8.1+). - var include_suspended_android: bool = false + var include_suspended_android: Variant = null static func from_dict(data: Dictionary) -> PurchaseOptions: var obj = PurchaseOptions.new() @@ -3463,13 +3347,13 @@ class RequestPurchaseAndroidProps: ## List of product SKUs var skus: Array[String] = [] ## Obfuscated account ID - var obfuscated_account_id: String = "" + var obfuscated_account_id: Variant = null ## Obfuscated profile ID - var obfuscated_profile_id: String = "" + var obfuscated_profile_id: Variant = null ## Personalized offer flag. - var is_offer_personalized: bool = false + var is_offer_personalized: Variant = null ## Offer token for one-time purchase discounts (7.0+). - var offer_token: String = "" + var offer_token: Variant = null ## Developer billing option parameters for external payments flow (8.3.0+). var developer_billing_option: DeveloperBillingOptionParamsAndroid @@ -3515,15 +3399,15 @@ class RequestPurchaseIosProps: ## Product SKU var sku: String = "" ## Auto-finish transaction (dangerous) - var and_dangerously_finish_transaction_automatically: bool = false + var and_dangerously_finish_transaction_automatically: Variant = null ## App account token for user tracking - var app_account_token: String = "" + var app_account_token: Variant = null ## Purchase quantity - var quantity: int = 0 + var quantity: Variant = null ## Promotional offer to apply (subscriptions only, ignored for one-time purchases). var with_offer: DiscountOfferInputIOS ## Advanced commerce data token (iOS 15+). - var advanced_commerce_data: String = "" + var advanced_commerce_data: Variant = null static func from_dict(data: Dictionary) -> RequestPurchaseIosProps: var obj = RequestPurchaseIosProps.new() @@ -3571,7 +3455,7 @@ class RequestPurchaseProps: ## Explicit purchase type hint (defaults to in-app) var type: ProductQueryType ## @deprecated Use enableBillingProgramAndroid in InitConnectionConfig instead. - var use_alternative_billing: bool = false + var use_alternative_billing: Variant = null static func from_dict(data: Dictionary) -> RequestPurchaseProps: var obj = RequestPurchaseProps.new() @@ -3679,15 +3563,15 @@ class RequestSubscriptionAndroidProps: ## List of subscription SKUs var skus: Array[String] = [] ## Obfuscated account ID - var obfuscated_account_id: String = "" + var obfuscated_account_id: Variant = null ## Obfuscated profile ID - var obfuscated_profile_id: String = "" + var obfuscated_profile_id: Variant = null ## Personalized offer flag. - var is_offer_personalized: bool = false + var is_offer_personalized: Variant = null ## Purchase token for upgrades/downgrades - var purchase_token: String = "" + var purchase_token: Variant = null ## Replacement mode for subscription changes - var replacement_mode: int = 0 + var replacement_mode: Variant = null ## Subscription offers var subscription_offers: Array[AndroidSubscriptionOfferInput] = [] ## Product-level replacement parameters (8.1.0+) @@ -3765,9 +3649,9 @@ class RequestSubscriptionAndroidProps: class RequestSubscriptionIosProps: var sku: String = "" - var and_dangerously_finish_transaction_automatically: bool = false - var app_account_token: String = "" - var quantity: int = 0 + var and_dangerously_finish_transaction_automatically: Variant = null + var app_account_token: Variant = null + var quantity: Variant = null ## Promotional offer to apply for subscription purchases. var with_offer: DiscountOfferInputIOS ## Win-back offer to apply (iOS 18+) @@ -3775,9 +3659,9 @@ class RequestSubscriptionIosProps: ## 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 = false + var introductory_offer_eligibility: Variant = null ## Advanced commerce data token (iOS 15+). - var advanced_commerce_data: String = "" + var advanced_commerce_data: Variant = null static func from_dict(data: Dictionary) -> RequestSubscriptionIosProps: var obj = RequestSubscriptionIosProps.new() @@ -3935,7 +3819,7 @@ class RequestVerifyPurchaseWithIapkitGoogleProps: ## Platform-specific verification parameters for IAPKit. - apple: Verifies via App Store (JWS token) - google: Verifies via Play Store (purchase token) class RequestVerifyPurchaseWithIapkitProps: ## API key used for the Authorization header (Bearer {apiKey}). - var api_key: String = "" + var api_key: Variant = null ## Apple App Store verification parameters. var apple: RequestVerifyPurchaseWithIapkitAppleProps ## Google Play Store verification parameters. @@ -4031,7 +3915,7 @@ class VerifyPurchaseGoogleOptions: ## Google OAuth2 access token for API authentication. var access_token: String = "" ## Whether this is a subscription purchase (affects API endpoint used) - var is_sub: bool = false + var is_sub: Variant = null static func from_dict(data: Dictionary) -> VerifyPurchaseGoogleOptions: var obj = VerifyPurchaseGoogleOptions.new() diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt index 0a6be119..b6d4845d 100644 --- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt +++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt @@ -71,6 +71,7 @@ object ErrorCodeUtils { alias("E_CONNECTION_CLOSED", "CONNECTION_CLOSED", target = ErrorCode.ConnectionClosed) alias("E_INIT_CONNECTION", "INIT_CONNECTION", target = ErrorCode.InitConnection) alias("E_SERVICE_DISCONNECTED", "SERVICE_DISCONNECTED", target = ErrorCode.ServiceDisconnected) + alias("E_SERVICE_TIMEOUT", "SERVICE_TIMEOUT", target = ErrorCode.ServiceTimeout) alias("E_QUERY_PRODUCT", "QUERY_PRODUCT", target = ErrorCode.QueryProduct) alias("E_SKU_NOT_FOUND", "SKU_NOT_FOUND", target = ErrorCode.SkuNotFound) alias("E_SKU_OFFER_MISMATCH", "SKU_OFFER_MISMATCH", target = ErrorCode.SkuOfferMismatch) @@ -81,6 +82,7 @@ object ErrorCodeUtils { alias("E_PURCHASE_VERIFICATION_FAILED", "PURCHASE_VERIFICATION_FAILED", target = ErrorCode.PurchaseVerificationFailed) alias("E_PURCHASE_VERIFICATION_FINISHED", "PURCHASE_VERIFICATION_FINISHED", target = ErrorCode.PurchaseVerificationFinished) alias("E_PURCHASE_VERIFICATION_FINISH_FAILED", "PURCHASE_VERIFICATION_FINISH_FAILED", target = ErrorCode.PurchaseVerificationFinishFailed) + alias("E_DUPLICATE_PURCHASE", "DUPLICATE_PURCHASE", target = ErrorCode.DuplicatePurchase) } fun fromPlatformCode(platformCode: Any, platform: IapPlatform): ErrorCode { diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt index 2c0c0d50..660b9ab6 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt @@ -562,7 +562,7 @@ class OpenIapModule( } catch (e: Exception) { OpenIapLog.e("Failed to extract token: ${e.message}", e, TAG) if (continuation.isActive) continuation.resumeWithException( - OpenIapError.PurchaseFailed(e.message) + OpenIapError.PurchaseFailed(e.message ?: e.javaClass.simpleName) ) } } else { @@ -605,7 +605,7 @@ class OpenIapModule( throw OpenIapError.FeatureNotSupported() } catch (e: Exception) { OpenIapLog.e("Failed to create billing program reporting details: ${e.message}", e, TAG) - throw OpenIapError.PurchaseFailed(e.message) + throw OpenIapError.PurchaseFailed(e.message ?: e.javaClass.simpleName) } } } diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts index cb53b553..e9b0132f 100644 --- a/packages/gql/codegen/plugins/gdscript.ts +++ b/packages/gql/codegen/plugins/gdscript.ts @@ -330,7 +330,16 @@ export class GDScriptPlugin extends CodegenPlugin { const gdType = this.mapType(field.type); const fieldName = this.getGdscriptFieldName(field.name, irObject.name); const defaultValue = this.getDefaultValue(field.type); - if (defaultValue !== null) { + if (field.type.nullable && this.isNullableScalar(field.type)) { + // Nullable scalars are emitted as untyped Variant so they can + // actually hold `null`. Typed GDScript properties cannot hold + // null, so declaring e.g. `var foo: String = ""` collapses the + // null-vs-empty distinction. Using `Variant` preserves the + // schema's optionality — `null` round-trips through + // from_dict/to_dict without being rewritten to `""` / `0` / + // `false`, and legitimate zero/false/empty values are retained. + this.emit(`\tvar ${fieldName}: Variant = null`); + } else if (defaultValue !== null) { this.emit(`\tvar ${fieldName}: ${gdType} = ${defaultValue}`); } else { this.emit(`\tvar ${fieldName}: ${gdType}`); @@ -420,24 +429,33 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`\t\t\tdict["${graphqlName}"] = ${enumConstName}[${fieldName}]`); this.emit(`\t\telse:`); this.emit(`\t\t\tdict["${graphqlName}"] = ${fieldName}`); - } else if (type.nullable) { - // Nullable scalars get a non-null default (e.g. "" for String, 0 for - // int) at declaration time so GDScript type checks pass. When that - // default is still in place, treat the field as "not set" and omit it - // from the serialized dict so consumers receive null/absent instead - // of the sentinel default. - const sentinel = this.getDefaultValue(type); - if (sentinel !== null) { - this.emit(`\t\tif ${fieldName} != ${sentinel}:`); - this.emit(`\t\t\tdict["${graphqlName}"] = ${fieldName}`); - } else { - this.emit(`\t\tdict["${graphqlName}"] = ${fieldName}`); - } } else { + // Emit the value verbatim for scalars. Non-nullable scalars carry + // their type-appropriate default (e.g. "" for String); nullable + // scalars are declared as Variant/null (see field declarations + // above) so `null` flows through to_dict naturally without having + // to drop legitimate zero/false/empty values. this.emit(`\t\tdict["${graphqlName}"] = ${fieldName}`); } } + /** + * Scalars eligible for the Variant/null nullable representation — + * object/input/enum fields are already nullable via their own + * mechanisms (default null reference, no sentinel collision). + */ + private isNullableScalar(type: IRType): boolean { + if (type.kind === 'list') return false; + if (this.objectNames.has(type.name!) || this.inputNames.has(type.name!)) { + return false; + } + if (type.kind === 'union' || type.kind === 'enum' || this.enumNames.has(type.name!)) { + return false; + } + const gdType = this.mapScalar(type.name!); + return gdType === 'String' || gdType === 'int' || gdType === 'float' || gdType === 'bool'; + } + private isObjectOrInput(type: IRType): boolean { if (type.kind === 'list') { return type.elementType ? this.isObjectOrInput(type.elementType) : false; @@ -465,7 +483,9 @@ export class GDScriptPlugin extends CodegenPlugin { const gdType = this.mapType(field.type); const fieldName = this.getGdscriptFieldName(field.name, irInput.name); const defaultValue = this.getDefaultValue(field.type); - if (defaultValue !== null) { + if (field.type.nullable && this.isNullableScalar(field.type)) { + this.emit(`\tvar ${fieldName}: Variant = null`); + } else if (defaultValue !== null) { this.emit(`\tvar ${fieldName}: ${gdType} = ${defaultValue}`); } else { this.emit(`\tvar ${fieldName}: ${gdType}`); diff --git a/packages/gql/scripts/sync-to-platforms.mjs b/packages/gql/scripts/sync-to-platforms.mjs index 932ecf1d..51445f25 100755 --- a/packages/gql/scripts/sync-to-platforms.mjs +++ b/packages/gql/scripts/sync-to-platforms.mjs @@ -118,13 +118,28 @@ if (existsSync(kmpSource)) { mkdirSync(dirname(kmpTarget), { recursive: true }); let text = readFileSync(kmpSource, 'utf8'); - // Insert package declaration after the leading @file: annotations so the - // resulting file mirrors packages/google/.../Types.kt. + // Insert the package declaration AFTER every leading `@file:` + // annotation. Kotlin requires file annotations to precede the package + // directive, so we walk the file line-by-line, find the last `@file:` + // (the generator can legitimately emit comments/blank lines between + // annotations), and splice the package declaration immediately after + // it. Falling back to a plain prepend is wrong because it would place + // the package before any subsequent `@file:` lines. if (!/\bpackage io\.github\.hyochan\.kmpiap\.openiap\b/.test(text)) { - text = text.replace( - /(@file:[^\n]+\n)(?!\s*package\b)/, - '$1\npackage io.github.hyochan.kmpiap.openiap\n', - ); + const pkg = 'package io.github.hyochan.kmpiap.openiap'; + const lines = text.split('\n'); + let lastFileAnnotation = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('@file:')) { + lastFileAnnotation = i; + } + } + if (lastFileAnnotation >= 0) { + lines.splice(lastFileAnnotation + 1, 0, '', pkg); + } else { + lines.unshift(pkg, ''); + } + text = lines.join('\n'); } // Kotlin enums that declare a companion object require a trailing diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 615e7ebe..18582a23 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -300,20 +300,20 @@ enum SubscriptionReplacementModeAndroid { class ActiveSubscription: var product_id: String = "" var is_active: bool = false - var expiration_date_ios: float = 0.0 - var auto_renewing_android: bool = false - var environment_ios: String = "" + var expiration_date_ios: Variant = null + var auto_renewing_android: Variant = null + var environment_ios: Variant = null ## @deprecated iOS only - use daysUntilExpirationIOS instead. - var will_expire_soon: bool = false - var days_until_expiration_ios: float = 0.0 + var will_expire_soon: Variant = null + var days_until_expiration_ios: Variant = null var transaction_id: String = "" - var purchase_token: String = "" + var purchase_token: Variant = null var transaction_date: float = 0.0 - var base_plan_id_android: String = "" + var base_plan_id_android: Variant = null ## Required for subscription upgrade/downgrade on Android - var purchase_token_android: String = "" + var purchase_token_android: Variant = null ## The current plan identifier. This is: - var current_plan_id: String = "" + var current_plan_id: Variant = null ## Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, var renewal_info_ios: RenewalInfoIOS @@ -356,26 +356,17 @@ class ActiveSubscription: var dict = {} dict["productId"] = product_id dict["isActive"] = is_active - if expiration_date_ios != 0.0: - dict["expirationDateIOS"] = expiration_date_ios - if auto_renewing_android != false: - dict["autoRenewingAndroid"] = auto_renewing_android - if environment_ios != "": - dict["environmentIOS"] = environment_ios - if will_expire_soon != false: - dict["willExpireSoon"] = will_expire_soon - if days_until_expiration_ios != 0.0: - dict["daysUntilExpirationIOS"] = days_until_expiration_ios + dict["expirationDateIOS"] = expiration_date_ios + dict["autoRenewingAndroid"] = auto_renewing_android + dict["environmentIOS"] = environment_ios + dict["willExpireSoon"] = will_expire_soon + dict["daysUntilExpirationIOS"] = days_until_expiration_ios dict["transactionId"] = transaction_id - if purchase_token != "": - dict["purchaseToken"] = purchase_token + dict["purchaseToken"] = purchase_token dict["transactionDate"] = transaction_date - if base_plan_id_android != "": - dict["basePlanIdAndroid"] = base_plan_id_android - if purchase_token_android != "": - dict["purchaseTokenAndroid"] = purchase_token_android - if current_plan_id != "": - dict["currentPlanId"] = current_plan_id + dict["basePlanIdAndroid"] = base_plan_id_android + dict["purchaseTokenAndroid"] = purchase_token_android + dict["currentPlanId"] = current_plan_id if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -393,9 +384,9 @@ class AppTransaction: var signed_date: float = 0.0 var app_id: float = 0.0 var app_version_id: float = 0.0 - var preorder_date: float = 0.0 - var app_transaction_id: String = "" - var original_platform: String = "" + var preorder_date: Variant = null + var app_transaction_id: Variant = null + var original_platform: Variant = null static func from_dict(data: Dictionary) -> AppTransaction: var obj = AppTransaction.new() @@ -439,12 +430,9 @@ class AppTransaction: dict["signedDate"] = signed_date dict["appId"] = app_id dict["appVersionId"] = app_version_id - if preorder_date != 0.0: - dict["preorderDate"] = preorder_date - if app_transaction_id != "": - dict["appTransactionId"] = app_transaction_id - if original_platform != "": - dict["originalPlatform"] = original_platform + dict["preorderDate"] = preorder_date + dict["appTransactionId"] = app_transaction_id + dict["originalPlatform"] = original_platform return dict ## Result of checking billing program availability (Android) Available in Google Play Billing Library 8.2.0+ @@ -508,7 +496,7 @@ class BillingResultAndroid: ## The response code from the billing operation var response_code: int = 0 ## Debug message from the billing library - var debug_message: String = "" + var debug_message: Variant = null ## Sub-response code for more granular error information (8.0+). var sub_response_code: SubResponseCodeAndroid @@ -529,8 +517,7 @@ class BillingResultAndroid: func to_dict() -> Dictionary: var dict = {} dict["responseCode"] = response_code - if debug_message != "": - dict["debugMessage"] = debug_message + dict["debugMessage"] = debug_message if SUB_RESPONSE_CODE_ANDROID_VALUES.has(sub_response_code): dict["subResponseCode"] = SUB_RESPONSE_CODE_ANDROID_VALUES[sub_response_code] else: @@ -577,7 +564,7 @@ class DiscountAmountAndroid: ## Discount display information for one-time purchase offers (Android) Available in Google Play Billing Library 7.0+ class DiscountDisplayInfoAndroid: ## Percentage discount (e.g., 33 for 33% off) - var percentage_discount: int = 0 + var percentage_discount: Variant = null ## Absolute discount amount details var discount_amount: DiscountAmountAndroid @@ -594,8 +581,7 @@ class DiscountDisplayInfoAndroid: func to_dict() -> Dictionary: var dict = {} - if percentage_discount != 0: - dict["percentageDiscount"] = percentage_discount + dict["percentageDiscount"] = percentage_discount if discount_amount != null and discount_amount.has_method("to_dict"): dict["discountAmount"] = discount_amount.to_dict() else: @@ -611,7 +597,7 @@ class DiscountIOS: var price_amount: float = 0.0 var payment_mode: PaymentModeIOS var subscription_period: String = "" - var localized_price: String = "" + var localized_price: Variant = null static func from_dict(data: Dictionary) -> DiscountIOS: var obj = DiscountIOS.new() @@ -649,14 +635,13 @@ class DiscountIOS: else: dict["paymentMode"] = payment_mode dict["subscriptionPeriod"] = subscription_period - if localized_price != "": - dict["localizedPrice"] = localized_price + dict["localizedPrice"] = localized_price return dict ## Standardized one-time product discount offer. Provides a unified interface for one-time purchase discounts across platforms. Currently supported on Android (Google Play Billing 7.0+). iOS does not support one-time purchase discounts in the same way. @see https://openiap.dev/docs/features/discount class DiscountOffer: ## Unique identifier for the offer. - var id: String = "" + var id: Variant = null ## Formatted display price string (e.g., "$4.99") var display_price: String = "" ## Numeric price value @@ -666,17 +651,17 @@ class DiscountOffer: ## Type of discount offer var type: DiscountOfferType ## [Android] Offer token required for purchase. - var offer_token_android: String = "" + var offer_token_android: Variant = null ## [Android] List of tags associated with this offer. var offer_tags_android: Array[String] = [] ## [Android] Original full price in micro-units before discount. - var full_price_micros_android: String = "" + var full_price_micros_android: Variant = null ## [Android] Percentage discount (e.g., 33 for 33% off). - var percentage_discount_android: int = 0 + var percentage_discount_android: Variant = null ## [Android] Fixed discount amount in micro-units. - var discount_amount_micros_android: String = "" + var discount_amount_micros_android: Variant = null ## [Android] Formatted discount amount string (e.g., "$5.00 OFF"). - var formatted_discount_amount_android: String = "" + var formatted_discount_amount_android: Variant = null ## [Android] Valid time window for the offer. var valid_time_window_android: ValidTimeWindowAndroid ## [Android] Limited quantity information. @@ -686,7 +671,7 @@ class DiscountOffer: ## [Android] Rental details if this is a rental offer. var rental_details_android: RentalDetailsAndroid ## [Android] Purchase option ID for this offer. - var purchase_option_id_android: String = "" + var purchase_option_id_android: Variant = null static func from_dict(data: Dictionary) -> DiscountOffer: var obj = DiscountOffer.new() @@ -742,8 +727,7 @@ class DiscountOffer: func to_dict() -> Dictionary: var dict = {} - if id != "": - dict["id"] = id + dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price dict["currency"] = currency @@ -751,18 +735,12 @@ class DiscountOffer: dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: dict["type"] = type - if offer_token_android != "": - dict["offerTokenAndroid"] = offer_token_android - if offer_tags_android != []: - dict["offerTagsAndroid"] = offer_tags_android - if full_price_micros_android != "": - dict["fullPriceMicrosAndroid"] = full_price_micros_android - if percentage_discount_android != 0: - dict["percentageDiscountAndroid"] = percentage_discount_android - if discount_amount_micros_android != "": - dict["discountAmountMicrosAndroid"] = discount_amount_micros_android - if formatted_discount_amount_android != "": - dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android + dict["offerTokenAndroid"] = offer_token_android + dict["offerTagsAndroid"] = offer_tags_android + dict["fullPriceMicrosAndroid"] = full_price_micros_android + dict["percentageDiscountAndroid"] = percentage_discount_android + dict["discountAmountMicrosAndroid"] = discount_amount_micros_android + dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android if valid_time_window_android != null and valid_time_window_android.has_method("to_dict"): dict["validTimeWindowAndroid"] = valid_time_window_android.to_dict() else: @@ -779,8 +757,7 @@ class DiscountOffer: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - if purchase_option_id_android != "": - dict["purchaseOptionIdAndroid"] = purchase_option_id_android + dict["purchaseOptionIdAndroid"] = purchase_option_id_android return dict ## iOS DiscountOffer (output type). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer @@ -878,7 +855,7 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: ## Whether the user chose to continue to external purchase var continued: bool = false ## Optional error message if the presentation failed - var error: String = "" + var error: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseCustomLinkNoticeResultIOS: var obj = ExternalPurchaseCustomLinkNoticeResultIOS.new() @@ -891,16 +868,15 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: func to_dict() -> Dictionary: var dict = {} dict["continued"] = continued - if error != "": - dict["error"] = error + dict["error"] = error return dict ## Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). class ExternalPurchaseCustomLinkTokenResultIOS: ## The external purchase token string. - var token: String = "" + var token: Variant = null ## Optional error message if token retrieval failed - var error: String = "" + var error: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseCustomLinkTokenResultIOS: var obj = ExternalPurchaseCustomLinkTokenResultIOS.new() @@ -912,10 +888,8 @@ class ExternalPurchaseCustomLinkTokenResultIOS: func to_dict() -> Dictionary: var dict = {} - if token != "": - dict["token"] = token - if error != "": - dict["error"] = error + dict["token"] = token + dict["error"] = error return dict ## Result of presenting an external purchase link @@ -923,7 +897,7 @@ class ExternalPurchaseLinkResultIOS: ## Whether the user completed the external purchase flow var success: bool = false ## Optional error message if the presentation failed - var error: String = "" + var error: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseLinkResultIOS: var obj = ExternalPurchaseLinkResultIOS.new() @@ -936,8 +910,7 @@ class ExternalPurchaseLinkResultIOS: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - if error != "": - dict["error"] = error + dict["error"] = error return dict ## Result of presenting external purchase notice sheet (iOS 17.4+) Returns the token when user continues to external purchase. @@ -945,9 +918,9 @@ class ExternalPurchaseNoticeResultIOS: ## Notice result indicating user action var result: ExternalPurchaseNoticeAction ## Optional error message if the presentation failed - var error: String = "" + var error: Variant = null ## External purchase token returned when user continues (iOS 17.4+). - var external_purchase_token: String = "" + var external_purchase_token: Variant = null static func from_dict(data: Dictionary) -> ExternalPurchaseNoticeResultIOS: var obj = ExternalPurchaseNoticeResultIOS.new() @@ -969,10 +942,8 @@ class ExternalPurchaseNoticeResultIOS: dict["result"] = EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES[result] else: dict["result"] = result - if error != "": - dict["error"] = error - if external_purchase_token != "": - dict["externalPurchaseToken"] = external_purchase_token + dict["error"] = error + dict["externalPurchaseToken"] = external_purchase_token return dict ## Installment plan details for subscription offers (Android) Contains information about the installment plan commitment. Available in Google Play Billing Library 7.0+ @@ -1127,11 +1098,11 @@ class ProductAndroid: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var name_android: String = "" ## Product-level status code indicating fetch result (Android 8.0+) @@ -1226,14 +1197,11 @@ class ProductAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1288,7 +1256,7 @@ class ProductAndroid: ## One-time purchase offer details (Android). Available in Google Play Billing Library 7.0+ @deprecated Use the standardized DiscountOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#discount-offer class ProductAndroidOneTimePurchaseOfferDetail: ## Offer ID - var offer_id: String = "" + var offer_id: Variant = null ## Offer token for use in BillingFlowParams when purchasing var offer_token: String = "" ## List of offer tags @@ -1297,7 +1265,7 @@ class ProductAndroidOneTimePurchaseOfferDetail: var formatted_price: String = "" var price_amount_micros: String = "" ## Full (non-discounted) price in micro-units - var full_price_micros: String = "" + var full_price_micros: Variant = null ## Discount display information var discount_display_info: DiscountDisplayInfoAndroid ## Valid time window for the offer @@ -1309,7 +1277,7 @@ class ProductAndroidOneTimePurchaseOfferDetail: ## Rental details for rental offers var rental_details_android: RentalDetailsAndroid ## Purchase option ID for this offer (Android) - var purchase_option_id: String = "" + var purchase_option_id: Variant = null static func from_dict(data: Dictionary) -> ProductAndroidOneTimePurchaseOfferDetail: var obj = ProductAndroidOneTimePurchaseOfferDetail.new() @@ -1358,15 +1326,13 @@ class ProductAndroidOneTimePurchaseOfferDetail: func to_dict() -> Dictionary: var dict = {} - if offer_id != "": - dict["offerId"] = offer_id + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags dict["priceCurrencyCode"] = price_currency_code dict["formattedPrice"] = formatted_price dict["priceAmountMicros"] = price_amount_micros - if full_price_micros != "": - dict["fullPriceMicros"] = full_price_micros + dict["fullPriceMicros"] = full_price_micros if discount_display_info != null and discount_display_info.has_method("to_dict"): dict["discountDisplayInfo"] = discount_display_info.to_dict() else: @@ -1387,8 +1353,7 @@ class ProductAndroidOneTimePurchaseOfferDetail: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - if purchase_option_id != "": - dict["purchaseOptionId"] = purchase_option_id + dict["purchaseOptionId"] = purchase_option_id return dict class ProductIOS: @@ -1396,11 +1361,11 @@ class ProductIOS: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var display_name_ios: String = "" var is_family_shareable_ios: bool = false @@ -1477,14 +1442,11 @@ class ProductIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1517,11 +1479,11 @@ class ProductSubscriptionAndroid: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var name_android: String = "" ## Product-level status code indicating fetch result (Android 8.0+) @@ -1616,14 +1578,11 @@ class ProductSubscriptionAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1678,7 +1637,7 @@ class ProductSubscriptionAndroid: ## Subscription offer details (Android). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer class ProductSubscriptionAndroidOfferDetails: var base_plan_id: String = "" - var offer_id: String = "" + var offer_id: Variant = null var offer_token: String = "" var offer_tags: Array[String] = [] var pricing_phases: PricingPhasesAndroid @@ -1710,8 +1669,7 @@ class ProductSubscriptionAndroidOfferDetails: func to_dict() -> Dictionary: var dict = {} dict["basePlanId"] = base_plan_id - if offer_id != "": - dict["offerId"] = offer_id + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags if pricing_phases != null and pricing_phases.has_method("to_dict"): @@ -1729,11 +1687,11 @@ class ProductSubscriptionIOS: var title: String = "" var description: String = "" var type: ProductType - var display_name: String = "" + var display_name: Variant = null var display_price: String = "" var currency: String = "" - var price: float = 0.0 - var debug_description: String = "" + var price: Variant = null + var debug_description: Variant = null var platform: IapPlatform var display_name_ios: String = "" var is_family_shareable_ios: bool = false @@ -1745,12 +1703,12 @@ class ProductSubscriptionIOS: var subscription_info_ios: SubscriptionInfoIOS ## @deprecated Use subscriptionOffers instead for cross-platform compatibility. var discounts_ios: Array[DiscountIOS] = [] - var introductory_price_ios: String = "" - var introductory_price_as_amount_ios: String = "" + var introductory_price_ios: Variant = null + var introductory_price_as_amount_ios: Variant = null var introductory_price_payment_mode_ios: PaymentModeIOS - var introductory_price_number_of_periods_ios: String = "" + var introductory_price_number_of_periods_ios: Variant = null var introductory_price_subscription_period_ios: SubscriptionPeriodIOS - var subscription_period_number_ios: String = "" + var subscription_period_number_ios: Variant = null var subscription_period_unit_ios: SubscriptionPeriodIOS static func from_dict(data: Dictionary) -> ProductSubscriptionIOS: @@ -1853,14 +1811,11 @@ class ProductSubscriptionIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - if display_name != "": - dict["displayName"] = display_name + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - if price != 0.0: - dict["price"] = price - if debug_description != "": - dict["debugDescription"] = debug_description + dict["price"] = price + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1896,22 +1851,18 @@ class ProductSubscriptionIOS: dict["discountsIOS"] = arr else: dict["discountsIOS"] = null - if introductory_price_ios != "": - dict["introductoryPriceIOS"] = introductory_price_ios - if introductory_price_as_amount_ios != "": - dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios + dict["introductoryPriceIOS"] = introductory_price_ios + dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios if PAYMENT_MODE_IOS_VALUES.has(introductory_price_payment_mode_ios): dict["introductoryPricePaymentModeIOS"] = PAYMENT_MODE_IOS_VALUES[introductory_price_payment_mode_ios] else: dict["introductoryPricePaymentModeIOS"] = introductory_price_payment_mode_ios - if introductory_price_number_of_periods_ios != "": - dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios + dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(introductory_price_subscription_period_ios): dict["introductoryPriceSubscriptionPeriodIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[introductory_price_subscription_period_ios] else: dict["introductoryPriceSubscriptionPeriodIOS"] = introductory_price_subscription_period_ios - if subscription_period_number_ios != "": - dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios + dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(subscription_period_unit_ios): dict["subscriptionPeriodUnitIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[subscription_period_unit_ios] else: @@ -1922,26 +1873,26 @@ class PurchaseAndroid: var id: String = "" var product_id: String = "" var ids: Array[String] = [] - var transaction_id: String = "" + var transaction_id: Variant = null var transaction_date: float = 0.0 - var purchase_token: String = "" + var purchase_token: Variant = null ## Store where purchase was made var store: IapStore var platform: IapPlatform var quantity: int = 0 var purchase_state: PurchaseState var is_auto_renewing: bool = false - var current_plan_id: String = "" - var data_android: String = "" - var signature_android: String = "" - var auto_renewing_android: bool = false - var is_acknowledged_android: bool = false - var package_name_android: String = "" - var developer_payload_android: String = "" - var obfuscated_account_id_android: String = "" - var obfuscated_profile_id_android: String = "" + var current_plan_id: Variant = null + var data_android: Variant = null + var signature_android: Variant = null + var auto_renewing_android: Variant = null + var is_acknowledged_android: Variant = null + var package_name_android: Variant = null + var developer_payload_android: Variant = null + var obfuscated_account_id_android: Variant = null + var obfuscated_profile_id_android: Variant = null ## Whether the subscription is suspended (Android) - var is_suspended_android: bool = false + var is_suspended_android: Variant = null ## Pending purchase update for uncommitted subscription upgrade/downgrade (Android) var pending_purchase_update_android: PendingPurchaseUpdateAndroid @@ -2012,13 +1963,10 @@ class PurchaseAndroid: var dict = {} dict["id"] = id dict["productId"] = product_id - if ids != []: - dict["ids"] = ids - if transaction_id != "": - dict["transactionId"] = transaction_id + dict["ids"] = ids + dict["transactionId"] = transaction_id dict["transactionDate"] = transaction_date - if purchase_token != "": - dict["purchaseToken"] = purchase_token + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2033,26 +1981,16 @@ class PurchaseAndroid: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - if current_plan_id != "": - dict["currentPlanId"] = current_plan_id - if data_android != "": - dict["dataAndroid"] = data_android - if signature_android != "": - dict["signatureAndroid"] = signature_android - if auto_renewing_android != false: - dict["autoRenewingAndroid"] = auto_renewing_android - if is_acknowledged_android != false: - dict["isAcknowledgedAndroid"] = is_acknowledged_android - if package_name_android != "": - dict["packageNameAndroid"] = package_name_android - if developer_payload_android != "": - dict["developerPayloadAndroid"] = developer_payload_android - if obfuscated_account_id_android != "": - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - if obfuscated_profile_id_android != "": - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - if is_suspended_android != false: - dict["isSuspendedAndroid"] = is_suspended_android + dict["currentPlanId"] = current_plan_id + dict["dataAndroid"] = data_android + dict["signatureAndroid"] = signature_android + dict["autoRenewingAndroid"] = auto_renewing_android + dict["isAcknowledgedAndroid"] = is_acknowledged_android + dict["packageNameAndroid"] = package_name_android + dict["developerPayloadAndroid"] = developer_payload_android + dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android + dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android + dict["isSuspendedAndroid"] = is_suspended_android if pending_purchase_update_android != null and pending_purchase_update_android.has_method("to_dict"): dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android.to_dict() else: @@ -2062,8 +2000,8 @@ class PurchaseAndroid: class PurchaseError: var code: ErrorCode var message: String = "" - var product_id: String = "" - var debug_message: String = "" + var product_id: Variant = null + var debug_message: Variant = null static func from_dict(data: Dictionary) -> PurchaseError: var obj = PurchaseError.new() @@ -2088,10 +2026,8 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - if product_id != "": - dict["productId"] = product_id - if debug_message != "": - dict["debugMessage"] = debug_message + dict["productId"] = product_id + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2099,36 +2035,36 @@ class PurchaseIOS: var product_id: String = "" var ids: Array[String] = [] var transaction_date: float = 0.0 - var purchase_token: String = "" + var purchase_token: Variant = null ## Store where purchase was made var store: IapStore var platform: IapPlatform var quantity: int = 0 var purchase_state: PurchaseState var is_auto_renewing: bool = false - var current_plan_id: String = "" + var current_plan_id: Variant = null var transaction_id: String = "" - var quantity_ios: int = 0 - var original_transaction_date_ios: float = 0.0 - var original_transaction_identifier_ios: String = "" - var app_account_token: String = "" - var expiration_date_ios: float = 0.0 - var web_order_line_item_id_ios: String = "" - var environment_ios: String = "" - var storefront_country_code_ios: String = "" - var app_bundle_id_ios: String = "" - var subscription_group_id_ios: String = "" - var is_upgraded_ios: bool = false - var ownership_type_ios: String = "" - var reason_ios: String = "" - var reason_string_representation_ios: String = "" - var transaction_reason_ios: String = "" - var revocation_date_ios: float = 0.0 - var revocation_reason_ios: String = "" + var quantity_ios: Variant = null + var original_transaction_date_ios: Variant = null + var original_transaction_identifier_ios: Variant = null + var app_account_token: Variant = null + var expiration_date_ios: Variant = null + var web_order_line_item_id_ios: Variant = null + var environment_ios: Variant = null + var storefront_country_code_ios: Variant = null + var app_bundle_id_ios: Variant = null + var subscription_group_id_ios: Variant = null + var is_upgraded_ios: Variant = null + var ownership_type_ios: Variant = null + var reason_ios: Variant = null + var reason_string_representation_ios: Variant = null + var transaction_reason_ios: Variant = null + var revocation_date_ios: Variant = null + var revocation_reason_ios: Variant = null var offer_ios: PurchaseOfferIOS - var currency_code_ios: String = "" - var currency_symbol_ios: String = "" - var country_code_ios: String = "" + var currency_code_ios: Variant = null + var currency_symbol_ios: Variant = null + var country_code_ios: Variant = null var renewal_info_ios: RenewalInfoIOS static func from_dict(data: Dictionary) -> PurchaseIOS: @@ -2225,11 +2161,9 @@ class PurchaseIOS: var dict = {} dict["id"] = id dict["productId"] = product_id - if ids != []: - dict["ids"] = ids + dict["ids"] = ids dict["transactionDate"] = transaction_date - if purchase_token != "": - dict["purchaseToken"] = purchase_token + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2244,53 +2178,32 @@ class PurchaseIOS: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - if current_plan_id != "": - dict["currentPlanId"] = current_plan_id + dict["currentPlanId"] = current_plan_id dict["transactionId"] = transaction_id - if quantity_ios != 0: - dict["quantityIOS"] = quantity_ios - if original_transaction_date_ios != 0.0: - dict["originalTransactionDateIOS"] = original_transaction_date_ios - if original_transaction_identifier_ios != "": - dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios - if app_account_token != "": - dict["appAccountToken"] = app_account_token - if expiration_date_ios != 0.0: - dict["expirationDateIOS"] = expiration_date_ios - if web_order_line_item_id_ios != "": - dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios - if environment_ios != "": - dict["environmentIOS"] = environment_ios - if storefront_country_code_ios != "": - dict["storefrontCountryCodeIOS"] = storefront_country_code_ios - if app_bundle_id_ios != "": - dict["appBundleIdIOS"] = app_bundle_id_ios - if subscription_group_id_ios != "": - dict["subscriptionGroupIdIOS"] = subscription_group_id_ios - if is_upgraded_ios != false: - dict["isUpgradedIOS"] = is_upgraded_ios - if ownership_type_ios != "": - dict["ownershipTypeIOS"] = ownership_type_ios - if reason_ios != "": - dict["reasonIOS"] = reason_ios - if reason_string_representation_ios != "": - dict["reasonStringRepresentationIOS"] = reason_string_representation_ios - if transaction_reason_ios != "": - dict["transactionReasonIOS"] = transaction_reason_ios - if revocation_date_ios != 0.0: - dict["revocationDateIOS"] = revocation_date_ios - if revocation_reason_ios != "": - dict["revocationReasonIOS"] = revocation_reason_ios + dict["quantityIOS"] = quantity_ios + dict["originalTransactionDateIOS"] = original_transaction_date_ios + dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios + dict["appAccountToken"] = app_account_token + dict["expirationDateIOS"] = expiration_date_ios + dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios + dict["environmentIOS"] = environment_ios + dict["storefrontCountryCodeIOS"] = storefront_country_code_ios + dict["appBundleIdIOS"] = app_bundle_id_ios + dict["subscriptionGroupIdIOS"] = subscription_group_id_ios + dict["isUpgradedIOS"] = is_upgraded_ios + dict["ownershipTypeIOS"] = ownership_type_ios + dict["reasonIOS"] = reason_ios + dict["reasonStringRepresentationIOS"] = reason_string_representation_ios + dict["transactionReasonIOS"] = transaction_reason_ios + dict["revocationDateIOS"] = revocation_date_ios + dict["revocationReasonIOS"] = revocation_reason_ios if offer_ios != null and offer_ios.has_method("to_dict"): dict["offerIOS"] = offer_ios.to_dict() else: dict["offerIOS"] = offer_ios - if currency_code_ios != "": - dict["currencyCodeIOS"] = currency_code_ios - if currency_symbol_ios != "": - dict["currencySymbolIOS"] = currency_symbol_ios - if country_code_ios != "": - dict["countryCodeIOS"] = country_code_ios + dict["currencyCodeIOS"] = currency_code_ios + dict["currencySymbolIOS"] = currency_symbol_ios + dict["countryCodeIOS"] = country_code_ios if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -2321,7 +2234,7 @@ class PurchaseOfferIOS: class RefundResultIOS: var status: String = "" - var message: String = "" + var message: Variant = null static func from_dict(data: Dictionary) -> RefundResultIOS: var obj = RefundResultIOS.new() @@ -2334,31 +2247,30 @@ class RefundResultIOS: func to_dict() -> Dictionary: var dict = {} dict["status"] = status - if message != "": - dict["message"] = message + dict["message"] = message return dict ## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo class RenewalInfoIOS: - var json_representation: String = "" + var json_representation: Variant = null var will_auto_renew: bool = false - var auto_renew_preference: String = "" + var auto_renew_preference: Variant = null ## When subscription expires due to cancellation/billing issue - var expiration_reason: String = "" + var expiration_reason: Variant = null ## Grace period expiration date (milliseconds since epoch) - var grace_period_expiration_date: float = 0.0 + var grace_period_expiration_date: Variant = null ## True if subscription failed to renew due to billing issue and is retrying - var is_in_billing_retry: bool = false + var is_in_billing_retry: Variant = null ## Product ID that will be used on next renewal (when user upgrades/downgrades) - var pending_upgrade_product_id: String = "" + var pending_upgrade_product_id: Variant = null ## User's response to subscription price increase - var price_increase_status: String = "" + var price_increase_status: Variant = null ## Expected renewal date (milliseconds since epoch) - var renewal_date: float = 0.0 + var renewal_date: Variant = null ## Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) - var renewal_offer_id: String = "" + var renewal_offer_id: Variant = null ## Type of offer applied to next renewal - var renewal_offer_type: String = "" + var renewal_offer_type: Variant = null static func from_dict(data: Dictionary) -> RenewalInfoIOS: var obj = RenewalInfoIOS.new() @@ -2388,27 +2300,17 @@ class RenewalInfoIOS: func to_dict() -> Dictionary: var dict = {} - if json_representation != "": - dict["jsonRepresentation"] = json_representation + dict["jsonRepresentation"] = json_representation dict["willAutoRenew"] = will_auto_renew - if auto_renew_preference != "": - dict["autoRenewPreference"] = auto_renew_preference - if expiration_reason != "": - dict["expirationReason"] = expiration_reason - if grace_period_expiration_date != 0.0: - dict["gracePeriodExpirationDate"] = grace_period_expiration_date - if is_in_billing_retry != false: - dict["isInBillingRetry"] = is_in_billing_retry - if pending_upgrade_product_id != "": - dict["pendingUpgradeProductId"] = pending_upgrade_product_id - if price_increase_status != "": - dict["priceIncreaseStatus"] = price_increase_status - if renewal_date != 0.0: - dict["renewalDate"] = renewal_date - if renewal_offer_id != "": - dict["renewalOfferId"] = renewal_offer_id - if renewal_offer_type != "": - dict["renewalOfferType"] = renewal_offer_type + dict["autoRenewPreference"] = auto_renew_preference + dict["expirationReason"] = expiration_reason + dict["gracePeriodExpirationDate"] = grace_period_expiration_date + dict["isInBillingRetry"] = is_in_billing_retry + dict["pendingUpgradeProductId"] = pending_upgrade_product_id + dict["priceIncreaseStatus"] = price_increase_status + dict["renewalDate"] = renewal_date + dict["renewalOfferId"] = renewal_offer_id + dict["renewalOfferType"] = renewal_offer_type return dict ## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+ @@ -2416,7 +2318,7 @@ class RentalDetailsAndroid: ## Rental period in ISO 8601 format (e.g., P7D for 7 days) var rental_period: String = "" ## Rental expiration period in ISO 8601 format - var rental_expiration_period: String = "" + var rental_expiration_period: Variant = null static func from_dict(data: Dictionary) -> RentalDetailsAndroid: var obj = RentalDetailsAndroid.new() @@ -2429,8 +2331,7 @@ class RentalDetailsAndroid: func to_dict() -> Dictionary: var dict = {} dict["rentalPeriod"] = rental_period - if rental_expiration_period != "": - dict["rentalExpirationPeriod"] = rental_expiration_period + dict["rentalExpirationPeriod"] = rental_expiration_period return dict class RequestVerifyPurchaseWithIapkitResult: @@ -2533,31 +2434,31 @@ class SubscriptionOffer: ## Numeric price value var price: float = 0.0 ## Currency code (ISO 4217, e.g., "USD") - var currency: String = "" + var currency: Variant = null ## Type of subscription offer (Introductory or Promotional) var type: DiscountOfferType ## Subscription period for this offer var period: SubscriptionPeriod ## Number of periods the offer applies - var period_count: int = 0 + var period_count: Variant = null ## Payment mode during the offer period var payment_mode: PaymentMode ## [iOS] Key identifier for signature validation. - var key_identifier_ios: String = "" + var key_identifier_ios: Variant = null ## [iOS] Cryptographic nonce (UUID) for signature validation. - var nonce_ios: String = "" + var nonce_ios: Variant = null ## [iOS] Server-generated signature for promotional offer validation. - var signature_ios: String = "" + var signature_ios: Variant = null ## [iOS] Timestamp when the signature was generated. - var timestamp_ios: float = 0.0 + var timestamp_ios: Variant = null ## [iOS] Number of billing periods for this discount. - var number_of_periods_ios: int = 0 + var number_of_periods_ios: Variant = null ## [iOS] Localized price string. - var localized_price_ios: String = "" + var localized_price_ios: Variant = null ## [Android] Base plan identifier. - var base_plan_id_android: String = "" + var base_plan_id_android: Variant = null ## [Android] Offer token required for purchase. - var offer_token_android: String = "" + var offer_token_android: Variant = null ## [Android] List of tags associated with this offer. var offer_tags_android: Array[String] = [] ## [Android] Pricing phases for this subscription offer. @@ -2629,8 +2530,7 @@ class SubscriptionOffer: dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price - if currency != "": - dict["currency"] = currency + dict["currency"] = currency if DISCOUNT_OFFER_TYPE_VALUES.has(type): dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: @@ -2639,30 +2539,20 @@ class SubscriptionOffer: dict["period"] = period.to_dict() else: dict["period"] = period - if period_count != 0: - dict["periodCount"] = period_count + dict["periodCount"] = period_count if PAYMENT_MODE_VALUES.has(payment_mode): dict["paymentMode"] = PAYMENT_MODE_VALUES[payment_mode] else: dict["paymentMode"] = payment_mode - if key_identifier_ios != "": - dict["keyIdentifierIOS"] = key_identifier_ios - if nonce_ios != "": - dict["nonceIOS"] = nonce_ios - if signature_ios != "": - dict["signatureIOS"] = signature_ios - if timestamp_ios != 0.0: - dict["timestampIOS"] = timestamp_ios - if number_of_periods_ios != 0: - dict["numberOfPeriodsIOS"] = number_of_periods_ios - if localized_price_ios != "": - dict["localizedPriceIOS"] = localized_price_ios - if base_plan_id_android != "": - dict["basePlanIdAndroid"] = base_plan_id_android - if offer_token_android != "": - dict["offerTokenAndroid"] = offer_token_android - if offer_tags_android != []: - dict["offerTagsAndroid"] = offer_tags_android + dict["keyIdentifierIOS"] = key_identifier_ios + dict["nonceIOS"] = nonce_ios + dict["signatureIOS"] = signature_ios + dict["timestampIOS"] = timestamp_ios + dict["numberOfPeriodsIOS"] = number_of_periods_ios + dict["localizedPriceIOS"] = localized_price_ios + dict["basePlanIdAndroid"] = base_plan_id_android + dict["offerTokenAndroid"] = offer_token_android + dict["offerTagsAndroid"] = offer_tags_android if pricing_phases_android != null and pricing_phases_android.has_method("to_dict"): dict["pricingPhasesAndroid"] = pricing_phases_android.to_dict() else: @@ -2854,10 +2744,10 @@ class ValidTimeWindowAndroid: class VerifyPurchaseResultAndroid: var auto_renewing: bool = false var beta_product: bool = false - var cancel_date: float = 0.0 - var cancel_reason: String = "" - var deferred_date: float = 0.0 - var deferred_sku: String = "" + var cancel_date: Variant = null + var cancel_reason: Variant = null + var deferred_date: Variant = null + var deferred_sku: Variant = null var free_trial_end_date: float = 0.0 var grace_period_end_date: float = 0.0 var parent_product_id: String = "" @@ -2915,14 +2805,10 @@ class VerifyPurchaseResultAndroid: var dict = {} dict["autoRenewing"] = auto_renewing dict["betaProduct"] = beta_product - if cancel_date != 0.0: - dict["cancelDate"] = cancel_date - if cancel_reason != "": - dict["cancelReason"] = cancel_reason - if deferred_date != 0.0: - dict["deferredDate"] = deferred_date - if deferred_sku != "": - dict["deferredSku"] = deferred_sku + dict["cancelDate"] = cancel_date + dict["cancelReason"] = cancel_reason + dict["deferredDate"] = deferred_date + dict["deferredSku"] = deferred_sku dict["freeTrialEndDate"] = free_trial_end_date dict["gracePeriodEndDate"] = grace_period_end_date dict["parentProductId"] = parent_product_id @@ -2942,7 +2828,7 @@ class VerifyPurchaseResultHorizon: ## Whether the entitlement verification succeeded. var success: bool = false ## Unix timestamp (seconds) when the entitlement was granted. - var grant_time: float = 0.0 + var grant_time: Variant = null static func from_dict(data: Dictionary) -> VerifyPurchaseResultHorizon: var obj = VerifyPurchaseResultHorizon.new() @@ -2955,8 +2841,7 @@ class VerifyPurchaseResultHorizon: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - if grant_time != 0.0: - dict["grantTime"] = grant_time + dict["grantTime"] = grant_time return dict class VerifyPurchaseResultIOS: @@ -2991,7 +2876,7 @@ class VerifyPurchaseResultIOS: class VerifyPurchaseWithProviderError: var message: String = "" - var code: String = "" + var code: Variant = null static func from_dict(data: Dictionary) -> VerifyPurchaseWithProviderError: var obj = VerifyPurchaseWithProviderError.new() @@ -3004,8 +2889,7 @@ class VerifyPurchaseWithProviderError: func to_dict() -> Dictionary: var dict = {} dict["message"] = message - if code != "": - dict["code"] = code + dict["code"] = code return dict class VerifyPurchaseWithProviderResult: @@ -3102,9 +2986,9 @@ class AndroidSubscriptionOfferInput: class DeepLinkOptions: ## Android SKU to open (required on Android) - var sku_android: String = "" + var sku_android: Variant = null ## Android package name to target (required on Android) - var package_name_android: String = "" + var package_name_android: Variant = null static func from_dict(data: Dictionary) -> DeepLinkOptions: var obj = DeepLinkOptions.new() @@ -3353,7 +3237,7 @@ class PurchaseInput: var product_id: String = "" var ids: Array[String] = [] var transaction_date: float = 0.0 - var purchase_token: String = "" + var purchase_token: Variant = null ## Store where purchase was made var store: IapStore ## @deprecated Use store instead @@ -3433,11 +3317,11 @@ class PurchaseInput: class PurchaseOptions: ## Also emit results through the iOS event listeners - var also_publish_to_event_listener_ios: bool = false + var also_publish_to_event_listener_ios: Variant = null ## Limit to currently active items on iOS - var only_include_active_items_ios: bool = false + var only_include_active_items_ios: Variant = null ## Include suspended subscriptions in the result (Android 8.1+). - var include_suspended_android: bool = false + var include_suspended_android: Variant = null static func from_dict(data: Dictionary) -> PurchaseOptions: var obj = PurchaseOptions.new() @@ -3463,13 +3347,13 @@ class RequestPurchaseAndroidProps: ## List of product SKUs var skus: Array[String] = [] ## Obfuscated account ID - var obfuscated_account_id: String = "" + var obfuscated_account_id: Variant = null ## Obfuscated profile ID - var obfuscated_profile_id: String = "" + var obfuscated_profile_id: Variant = null ## Personalized offer flag. - var is_offer_personalized: bool = false + var is_offer_personalized: Variant = null ## Offer token for one-time purchase discounts (7.0+). - var offer_token: String = "" + var offer_token: Variant = null ## Developer billing option parameters for external payments flow (8.3.0+). var developer_billing_option: DeveloperBillingOptionParamsAndroid @@ -3515,15 +3399,15 @@ class RequestPurchaseIosProps: ## Product SKU var sku: String = "" ## Auto-finish transaction (dangerous) - var and_dangerously_finish_transaction_automatically: bool = false + var and_dangerously_finish_transaction_automatically: Variant = null ## App account token for user tracking - var app_account_token: String = "" + var app_account_token: Variant = null ## Purchase quantity - var quantity: int = 0 + var quantity: Variant = null ## Promotional offer to apply (subscriptions only, ignored for one-time purchases). var with_offer: DiscountOfferInputIOS ## Advanced commerce data token (iOS 15+). - var advanced_commerce_data: String = "" + var advanced_commerce_data: Variant = null static func from_dict(data: Dictionary) -> RequestPurchaseIosProps: var obj = RequestPurchaseIosProps.new() @@ -3571,7 +3455,7 @@ class RequestPurchaseProps: ## Explicit purchase type hint (defaults to in-app) var type: ProductQueryType ## @deprecated Use enableBillingProgramAndroid in InitConnectionConfig instead. - var use_alternative_billing: bool = false + var use_alternative_billing: Variant = null static func from_dict(data: Dictionary) -> RequestPurchaseProps: var obj = RequestPurchaseProps.new() @@ -3679,15 +3563,15 @@ class RequestSubscriptionAndroidProps: ## List of subscription SKUs var skus: Array[String] = [] ## Obfuscated account ID - var obfuscated_account_id: String = "" + var obfuscated_account_id: Variant = null ## Obfuscated profile ID - var obfuscated_profile_id: String = "" + var obfuscated_profile_id: Variant = null ## Personalized offer flag. - var is_offer_personalized: bool = false + var is_offer_personalized: Variant = null ## Purchase token for upgrades/downgrades - var purchase_token: String = "" + var purchase_token: Variant = null ## Replacement mode for subscription changes - var replacement_mode: int = 0 + var replacement_mode: Variant = null ## Subscription offers var subscription_offers: Array[AndroidSubscriptionOfferInput] = [] ## Product-level replacement parameters (8.1.0+) @@ -3765,9 +3649,9 @@ class RequestSubscriptionAndroidProps: class RequestSubscriptionIosProps: var sku: String = "" - var and_dangerously_finish_transaction_automatically: bool = false - var app_account_token: String = "" - var quantity: int = 0 + var and_dangerously_finish_transaction_automatically: Variant = null + var app_account_token: Variant = null + var quantity: Variant = null ## Promotional offer to apply for subscription purchases. var with_offer: DiscountOfferInputIOS ## Win-back offer to apply (iOS 18+) @@ -3775,9 +3659,9 @@ class RequestSubscriptionIosProps: ## 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 = false + var introductory_offer_eligibility: Variant = null ## Advanced commerce data token (iOS 15+). - var advanced_commerce_data: String = "" + var advanced_commerce_data: Variant = null static func from_dict(data: Dictionary) -> RequestSubscriptionIosProps: var obj = RequestSubscriptionIosProps.new() @@ -3935,7 +3819,7 @@ class RequestVerifyPurchaseWithIapkitGoogleProps: ## Platform-specific verification parameters for IAPKit. - apple: Verifies via App Store (JWS token) - google: Verifies via Play Store (purchase token) class RequestVerifyPurchaseWithIapkitProps: ## API key used for the Authorization header (Bearer {apiKey}). - var api_key: String = "" + var api_key: Variant = null ## Apple App Store verification parameters. var apple: RequestVerifyPurchaseWithIapkitAppleProps ## Google Play Store verification parameters. @@ -4031,7 +3915,7 @@ class VerifyPurchaseGoogleOptions: ## Google OAuth2 access token for API authentication. var access_token: String = "" ## Whether this is a subscription purchase (affects API endpoint used) - var is_sub: bool = false + var is_sub: Variant = null static func from_dict(data: Dictionary) -> VerifyPurchaseGoogleOptions: var obj = VerifyPurchaseGoogleOptions.new() From 821db895ae610fee2e45607b33545bd48e43729d Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 21:39:29 +0900 Subject: [PATCH 6/9] fix(review): 4th-round PR #98 fixes (SemVer + GDScript null) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump the planned release version for openiap-google and openiap-google-horizon from 1.3.32 to 2.0.0. The error-type refactor (`OpenIapError.DeveloperError`/`PurchaseFailed`/UserCancelled/… singletons → data classes) is a source-breaking change for direct Kotlin consumers, so SemVer requires a major bump. Updated the 2026-04-15 release note entry and the PR description to reflect the bump and include the migration notes (the 11 affected names, the `()` migration pattern, companion access staying unchanged). - GDScript codegen: `generateToDictField` now omits nullable scalar keys from `to_dict()` when the value is `null`, instead of emitting an explicit `null`. Legitimate `0` / `false` / `""` / any explicitly-set value still serialize; only the "unset" (`null`) state is dropped. That restores the absent-vs-null distinction without reintroducing the sentinel-based-omission bug that drops legitimate `false`/`0`/`""`. Regenerated `types.gd`. Co-Authored-By: Claude Opus 4.6 (1M context) --- libraries/godot-iap/addons/godot-iap/types.gd | 336 ++++++++++++------ .../docs/src/pages/docs/updates/releases.tsx | 55 ++- packages/gql/codegen/plugins/gdscript.ts | 15 +- packages/gql/src/generated/types.gd | 336 ++++++++++++------ 4 files changed, 499 insertions(+), 243 deletions(-) diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 18582a23..fed9bd9b 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -356,17 +356,26 @@ class ActiveSubscription: var dict = {} dict["productId"] = product_id dict["isActive"] = is_active - dict["expirationDateIOS"] = expiration_date_ios - dict["autoRenewingAndroid"] = auto_renewing_android - dict["environmentIOS"] = environment_ios - dict["willExpireSoon"] = will_expire_soon - dict["daysUntilExpirationIOS"] = days_until_expiration_ios + if expiration_date_ios != null: + dict["expirationDateIOS"] = expiration_date_ios + if auto_renewing_android != null: + dict["autoRenewingAndroid"] = auto_renewing_android + if environment_ios != null: + dict["environmentIOS"] = environment_ios + if will_expire_soon != null: + dict["willExpireSoon"] = will_expire_soon + if days_until_expiration_ios != null: + dict["daysUntilExpirationIOS"] = days_until_expiration_ios dict["transactionId"] = transaction_id - dict["purchaseToken"] = purchase_token + if purchase_token != null: + dict["purchaseToken"] = purchase_token dict["transactionDate"] = transaction_date - dict["basePlanIdAndroid"] = base_plan_id_android - dict["purchaseTokenAndroid"] = purchase_token_android - dict["currentPlanId"] = current_plan_id + if base_plan_id_android != null: + dict["basePlanIdAndroid"] = base_plan_id_android + if purchase_token_android != null: + dict["purchaseTokenAndroid"] = purchase_token_android + if current_plan_id != null: + dict["currentPlanId"] = current_plan_id if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -430,9 +439,12 @@ class AppTransaction: dict["signedDate"] = signed_date dict["appId"] = app_id dict["appVersionId"] = app_version_id - dict["preorderDate"] = preorder_date - dict["appTransactionId"] = app_transaction_id - dict["originalPlatform"] = original_platform + if preorder_date != null: + dict["preorderDate"] = preorder_date + if app_transaction_id != null: + dict["appTransactionId"] = app_transaction_id + if original_platform != null: + dict["originalPlatform"] = original_platform return dict ## Result of checking billing program availability (Android) Available in Google Play Billing Library 8.2.0+ @@ -517,7 +529,8 @@ class BillingResultAndroid: func to_dict() -> Dictionary: var dict = {} dict["responseCode"] = response_code - dict["debugMessage"] = debug_message + if debug_message != null: + dict["debugMessage"] = debug_message if SUB_RESPONSE_CODE_ANDROID_VALUES.has(sub_response_code): dict["subResponseCode"] = SUB_RESPONSE_CODE_ANDROID_VALUES[sub_response_code] else: @@ -581,7 +594,8 @@ class DiscountDisplayInfoAndroid: func to_dict() -> Dictionary: var dict = {} - dict["percentageDiscount"] = percentage_discount + if percentage_discount != null: + dict["percentageDiscount"] = percentage_discount if discount_amount != null and discount_amount.has_method("to_dict"): dict["discountAmount"] = discount_amount.to_dict() else: @@ -635,7 +649,8 @@ class DiscountIOS: else: dict["paymentMode"] = payment_mode dict["subscriptionPeriod"] = subscription_period - dict["localizedPrice"] = localized_price + if localized_price != null: + dict["localizedPrice"] = localized_price return dict ## Standardized one-time product discount offer. Provides a unified interface for one-time purchase discounts across platforms. Currently supported on Android (Google Play Billing 7.0+). iOS does not support one-time purchase discounts in the same way. @see https://openiap.dev/docs/features/discount @@ -727,7 +742,8 @@ class DiscountOffer: func to_dict() -> Dictionary: var dict = {} - dict["id"] = id + if id != null: + dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price dict["currency"] = currency @@ -735,12 +751,17 @@ class DiscountOffer: dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: dict["type"] = type - dict["offerTokenAndroid"] = offer_token_android + if offer_token_android != null: + dict["offerTokenAndroid"] = offer_token_android dict["offerTagsAndroid"] = offer_tags_android - dict["fullPriceMicrosAndroid"] = full_price_micros_android - dict["percentageDiscountAndroid"] = percentage_discount_android - dict["discountAmountMicrosAndroid"] = discount_amount_micros_android - dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android + if full_price_micros_android != null: + dict["fullPriceMicrosAndroid"] = full_price_micros_android + if percentage_discount_android != null: + dict["percentageDiscountAndroid"] = percentage_discount_android + if discount_amount_micros_android != null: + dict["discountAmountMicrosAndroid"] = discount_amount_micros_android + if formatted_discount_amount_android != null: + dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android if valid_time_window_android != null and valid_time_window_android.has_method("to_dict"): dict["validTimeWindowAndroid"] = valid_time_window_android.to_dict() else: @@ -757,7 +778,8 @@ class DiscountOffer: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionIdAndroid"] = purchase_option_id_android + if purchase_option_id_android != null: + dict["purchaseOptionIdAndroid"] = purchase_option_id_android return dict ## iOS DiscountOffer (output type). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer @@ -868,7 +890,8 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: func to_dict() -> Dictionary: var dict = {} dict["continued"] = continued - dict["error"] = error + if error != null: + dict["error"] = error return dict ## Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). @@ -888,8 +911,10 @@ class ExternalPurchaseCustomLinkTokenResultIOS: func to_dict() -> Dictionary: var dict = {} - dict["token"] = token - dict["error"] = error + if token != null: + dict["token"] = token + if error != null: + dict["error"] = error return dict ## Result of presenting an external purchase link @@ -910,7 +935,8 @@ class ExternalPurchaseLinkResultIOS: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["error"] = error + if error != null: + dict["error"] = error return dict ## Result of presenting external purchase notice sheet (iOS 17.4+) Returns the token when user continues to external purchase. @@ -942,8 +968,10 @@ class ExternalPurchaseNoticeResultIOS: dict["result"] = EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES[result] else: dict["result"] = result - dict["error"] = error - dict["externalPurchaseToken"] = external_purchase_token + if error != null: + dict["error"] = error + if external_purchase_token != null: + dict["externalPurchaseToken"] = external_purchase_token return dict ## Installment plan details for subscription offers (Android) Contains information about the installment plan commitment. Available in Google Play Billing Library 7.0+ @@ -1197,11 +1225,14 @@ class ProductAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1326,13 +1357,15 @@ class ProductAndroidOneTimePurchaseOfferDetail: func to_dict() -> Dictionary: var dict = {} - dict["offerId"] = offer_id + if offer_id != null: + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags dict["priceCurrencyCode"] = price_currency_code dict["formattedPrice"] = formatted_price dict["priceAmountMicros"] = price_amount_micros - dict["fullPriceMicros"] = full_price_micros + if full_price_micros != null: + dict["fullPriceMicros"] = full_price_micros if discount_display_info != null and discount_display_info.has_method("to_dict"): dict["discountDisplayInfo"] = discount_display_info.to_dict() else: @@ -1353,7 +1386,8 @@ class ProductAndroidOneTimePurchaseOfferDetail: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionId"] = purchase_option_id + if purchase_option_id != null: + dict["purchaseOptionId"] = purchase_option_id return dict class ProductIOS: @@ -1442,11 +1476,14 @@ class ProductIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1578,11 +1615,14 @@ class ProductSubscriptionAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1669,7 +1709,8 @@ class ProductSubscriptionAndroidOfferDetails: func to_dict() -> Dictionary: var dict = {} dict["basePlanId"] = base_plan_id - dict["offerId"] = offer_id + if offer_id != null: + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags if pricing_phases != null and pricing_phases.has_method("to_dict"): @@ -1811,11 +1852,14 @@ class ProductSubscriptionIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1851,18 +1895,22 @@ class ProductSubscriptionIOS: dict["discountsIOS"] = arr else: dict["discountsIOS"] = null - dict["introductoryPriceIOS"] = introductory_price_ios - dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios + if introductory_price_ios != null: + dict["introductoryPriceIOS"] = introductory_price_ios + if introductory_price_as_amount_ios != null: + dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios if PAYMENT_MODE_IOS_VALUES.has(introductory_price_payment_mode_ios): dict["introductoryPricePaymentModeIOS"] = PAYMENT_MODE_IOS_VALUES[introductory_price_payment_mode_ios] else: dict["introductoryPricePaymentModeIOS"] = introductory_price_payment_mode_ios - dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios + if introductory_price_number_of_periods_ios != null: + dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(introductory_price_subscription_period_ios): dict["introductoryPriceSubscriptionPeriodIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[introductory_price_subscription_period_ios] else: dict["introductoryPriceSubscriptionPeriodIOS"] = introductory_price_subscription_period_ios - dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios + if subscription_period_number_ios != null: + dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(subscription_period_unit_ios): dict["subscriptionPeriodUnitIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[subscription_period_unit_ios] else: @@ -1964,9 +2012,11 @@ class PurchaseAndroid: dict["id"] = id dict["productId"] = product_id dict["ids"] = ids - dict["transactionId"] = transaction_id + if transaction_id != null: + dict["transactionId"] = transaction_id dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != null: + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -1981,16 +2031,26 @@ class PurchaseAndroid: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id - dict["dataAndroid"] = data_android - dict["signatureAndroid"] = signature_android - dict["autoRenewingAndroid"] = auto_renewing_android - dict["isAcknowledgedAndroid"] = is_acknowledged_android - dict["packageNameAndroid"] = package_name_android - dict["developerPayloadAndroid"] = developer_payload_android - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - dict["isSuspendedAndroid"] = is_suspended_android + if current_plan_id != null: + dict["currentPlanId"] = current_plan_id + if data_android != null: + dict["dataAndroid"] = data_android + if signature_android != null: + dict["signatureAndroid"] = signature_android + if auto_renewing_android != null: + dict["autoRenewingAndroid"] = auto_renewing_android + if is_acknowledged_android != null: + dict["isAcknowledgedAndroid"] = is_acknowledged_android + if package_name_android != null: + dict["packageNameAndroid"] = package_name_android + if developer_payload_android != null: + dict["developerPayloadAndroid"] = developer_payload_android + if obfuscated_account_id_android != null: + dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android + if obfuscated_profile_id_android != null: + dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android + if is_suspended_android != null: + dict["isSuspendedAndroid"] = is_suspended_android if pending_purchase_update_android != null and pending_purchase_update_android.has_method("to_dict"): dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android.to_dict() else: @@ -2026,8 +2086,10 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - dict["productId"] = product_id - dict["debugMessage"] = debug_message + if product_id != null: + dict["productId"] = product_id + if debug_message != null: + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2163,7 +2225,8 @@ class PurchaseIOS: dict["productId"] = product_id dict["ids"] = ids dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != null: + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2178,32 +2241,53 @@ class PurchaseIOS: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id + if current_plan_id != null: + dict["currentPlanId"] = current_plan_id dict["transactionId"] = transaction_id - dict["quantityIOS"] = quantity_ios - dict["originalTransactionDateIOS"] = original_transaction_date_ios - dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios - dict["appAccountToken"] = app_account_token - dict["expirationDateIOS"] = expiration_date_ios - dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios - dict["environmentIOS"] = environment_ios - dict["storefrontCountryCodeIOS"] = storefront_country_code_ios - dict["appBundleIdIOS"] = app_bundle_id_ios - dict["subscriptionGroupIdIOS"] = subscription_group_id_ios - dict["isUpgradedIOS"] = is_upgraded_ios - dict["ownershipTypeIOS"] = ownership_type_ios - dict["reasonIOS"] = reason_ios - dict["reasonStringRepresentationIOS"] = reason_string_representation_ios - dict["transactionReasonIOS"] = transaction_reason_ios - dict["revocationDateIOS"] = revocation_date_ios - dict["revocationReasonIOS"] = revocation_reason_ios + if quantity_ios != null: + dict["quantityIOS"] = quantity_ios + if original_transaction_date_ios != null: + dict["originalTransactionDateIOS"] = original_transaction_date_ios + if original_transaction_identifier_ios != null: + dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios + if app_account_token != null: + dict["appAccountToken"] = app_account_token + if expiration_date_ios != null: + dict["expirationDateIOS"] = expiration_date_ios + if web_order_line_item_id_ios != null: + dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios + if environment_ios != null: + dict["environmentIOS"] = environment_ios + if storefront_country_code_ios != null: + dict["storefrontCountryCodeIOS"] = storefront_country_code_ios + if app_bundle_id_ios != null: + dict["appBundleIdIOS"] = app_bundle_id_ios + if subscription_group_id_ios != null: + dict["subscriptionGroupIdIOS"] = subscription_group_id_ios + if is_upgraded_ios != null: + dict["isUpgradedIOS"] = is_upgraded_ios + if ownership_type_ios != null: + dict["ownershipTypeIOS"] = ownership_type_ios + if reason_ios != null: + dict["reasonIOS"] = reason_ios + if reason_string_representation_ios != null: + dict["reasonStringRepresentationIOS"] = reason_string_representation_ios + if transaction_reason_ios != null: + dict["transactionReasonIOS"] = transaction_reason_ios + if revocation_date_ios != null: + dict["revocationDateIOS"] = revocation_date_ios + if revocation_reason_ios != null: + dict["revocationReasonIOS"] = revocation_reason_ios if offer_ios != null and offer_ios.has_method("to_dict"): dict["offerIOS"] = offer_ios.to_dict() else: dict["offerIOS"] = offer_ios - dict["currencyCodeIOS"] = currency_code_ios - dict["currencySymbolIOS"] = currency_symbol_ios - dict["countryCodeIOS"] = country_code_ios + if currency_code_ios != null: + dict["currencyCodeIOS"] = currency_code_ios + if currency_symbol_ios != null: + dict["currencySymbolIOS"] = currency_symbol_ios + if country_code_ios != null: + dict["countryCodeIOS"] = country_code_ios if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -2247,7 +2331,8 @@ class RefundResultIOS: func to_dict() -> Dictionary: var dict = {} dict["status"] = status - dict["message"] = message + if message != null: + dict["message"] = message return dict ## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo @@ -2300,17 +2385,27 @@ class RenewalInfoIOS: func to_dict() -> Dictionary: var dict = {} - dict["jsonRepresentation"] = json_representation + if json_representation != null: + dict["jsonRepresentation"] = json_representation dict["willAutoRenew"] = will_auto_renew - dict["autoRenewPreference"] = auto_renew_preference - dict["expirationReason"] = expiration_reason - dict["gracePeriodExpirationDate"] = grace_period_expiration_date - dict["isInBillingRetry"] = is_in_billing_retry - dict["pendingUpgradeProductId"] = pending_upgrade_product_id - dict["priceIncreaseStatus"] = price_increase_status - dict["renewalDate"] = renewal_date - dict["renewalOfferId"] = renewal_offer_id - dict["renewalOfferType"] = renewal_offer_type + if auto_renew_preference != null: + dict["autoRenewPreference"] = auto_renew_preference + if expiration_reason != null: + dict["expirationReason"] = expiration_reason + if grace_period_expiration_date != null: + dict["gracePeriodExpirationDate"] = grace_period_expiration_date + if is_in_billing_retry != null: + dict["isInBillingRetry"] = is_in_billing_retry + if pending_upgrade_product_id != null: + dict["pendingUpgradeProductId"] = pending_upgrade_product_id + if price_increase_status != null: + dict["priceIncreaseStatus"] = price_increase_status + if renewal_date != null: + dict["renewalDate"] = renewal_date + if renewal_offer_id != null: + dict["renewalOfferId"] = renewal_offer_id + if renewal_offer_type != null: + dict["renewalOfferType"] = renewal_offer_type return dict ## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+ @@ -2331,7 +2426,8 @@ class RentalDetailsAndroid: func to_dict() -> Dictionary: var dict = {} dict["rentalPeriod"] = rental_period - dict["rentalExpirationPeriod"] = rental_expiration_period + if rental_expiration_period != null: + dict["rentalExpirationPeriod"] = rental_expiration_period return dict class RequestVerifyPurchaseWithIapkitResult: @@ -2530,7 +2626,8 @@ class SubscriptionOffer: dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price - dict["currency"] = currency + if currency != null: + dict["currency"] = currency if DISCOUNT_OFFER_TYPE_VALUES.has(type): dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: @@ -2539,19 +2636,28 @@ class SubscriptionOffer: dict["period"] = period.to_dict() else: dict["period"] = period - dict["periodCount"] = period_count + if period_count != null: + dict["periodCount"] = period_count if PAYMENT_MODE_VALUES.has(payment_mode): dict["paymentMode"] = PAYMENT_MODE_VALUES[payment_mode] else: dict["paymentMode"] = payment_mode - dict["keyIdentifierIOS"] = key_identifier_ios - dict["nonceIOS"] = nonce_ios - dict["signatureIOS"] = signature_ios - dict["timestampIOS"] = timestamp_ios - dict["numberOfPeriodsIOS"] = number_of_periods_ios - dict["localizedPriceIOS"] = localized_price_ios - dict["basePlanIdAndroid"] = base_plan_id_android - dict["offerTokenAndroid"] = offer_token_android + if key_identifier_ios != null: + dict["keyIdentifierIOS"] = key_identifier_ios + if nonce_ios != null: + dict["nonceIOS"] = nonce_ios + if signature_ios != null: + dict["signatureIOS"] = signature_ios + if timestamp_ios != null: + dict["timestampIOS"] = timestamp_ios + if number_of_periods_ios != null: + dict["numberOfPeriodsIOS"] = number_of_periods_ios + if localized_price_ios != null: + dict["localizedPriceIOS"] = localized_price_ios + if base_plan_id_android != null: + dict["basePlanIdAndroid"] = base_plan_id_android + if offer_token_android != null: + dict["offerTokenAndroid"] = offer_token_android dict["offerTagsAndroid"] = offer_tags_android if pricing_phases_android != null and pricing_phases_android.has_method("to_dict"): dict["pricingPhasesAndroid"] = pricing_phases_android.to_dict() @@ -2805,10 +2911,14 @@ class VerifyPurchaseResultAndroid: var dict = {} dict["autoRenewing"] = auto_renewing dict["betaProduct"] = beta_product - dict["cancelDate"] = cancel_date - dict["cancelReason"] = cancel_reason - dict["deferredDate"] = deferred_date - dict["deferredSku"] = deferred_sku + if cancel_date != null: + dict["cancelDate"] = cancel_date + if cancel_reason != null: + dict["cancelReason"] = cancel_reason + if deferred_date != null: + dict["deferredDate"] = deferred_date + if deferred_sku != null: + dict["deferredSku"] = deferred_sku dict["freeTrialEndDate"] = free_trial_end_date dict["gracePeriodEndDate"] = grace_period_end_date dict["parentProductId"] = parent_product_id @@ -2841,7 +2951,8 @@ class VerifyPurchaseResultHorizon: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["grantTime"] = grant_time + if grant_time != null: + dict["grantTime"] = grant_time return dict class VerifyPurchaseResultIOS: @@ -2889,7 +3000,8 @@ class VerifyPurchaseWithProviderError: func to_dict() -> Dictionary: var dict = {} dict["message"] = message - dict["code"] = code + if code != null: + dict["code"] = code return dict class VerifyPurchaseWithProviderResult: diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 539e5a98..702b8792 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -59,11 +59,11 @@ function Releases() {
    - openiap-google 1.3.32 + openiap-google 2.0.0
    • - Feat: DeveloperError and{' '} - PurchaseFailed now carry{' '} - debugMessage + Breaking: OpenIapError error types are now data + classes {' '} - — both errors are now data classes instead of singletons and - accept an optional debugMessage: String?.{' '} - fromBillingResponseCode forwards Google Play's - raw BillingResult.debugMessage into the error - instance, and OpenIapError.toJSON() emits a{' '} + — every error returned by fromBillingResponseCode ( + DeveloperError, PurchaseFailed,{' '} + UserCancelled, ServiceUnavailable,{' '} + BillingUnavailable, ItemUnavailable,{' '} + BillingError, ItemAlreadyOwned,{' '} + ItemNotOwned, ServiceDisconnected,{' '} + FeatureNotSupported, ServiceTimeout,{' '} + UnknownError) now accepts an optional{' '} + debugMessage: String?. This is a source-breaking + change for direct Kotlin consumers: references like{' '} + throw OpenIapError.DeveloperError must become{' '} + throw OpenIapError.DeveloperError(). Companion{' '} + .CODE / .MESSAGE accesses and{' '} + is OpenIapError.X type checks are unchanged. Hence + the major version bump. +
    • +
    • + + Feat: surface Google Play's{' '} + BillingResult.debugMessage + {' '} + — fromBillingResponseCode forwards Play's raw + debug text into the error instance for every response code, and{' '} + OpenIapError.toJSON() emits a{' '} debugMessage key so downstream framework libraries can surface the reason Play rejected a purchase (offer token mismatch, subscription group conflict, etc.). The{' '} @@ -89,6 +107,15 @@ function Releases() { PurchaseFailed for DEVELOPER_ERROR{' '} response codes.
    • +
    • + + Schema: add ServiceTimeout to the shared{' '} + ErrorCode enum + {' '} + so the code comes from{' '} + ErrorCode.ServiceTimeout.rawValue like every other + entry instead of a hand-typed string literal. +

    @@ -106,7 +133,7 @@ function Releases() { {' '} ·{' '} @@ -175,7 +202,7 @@ function Releases() { code and message from the native error payload, so the raw BillingResult.debugMessage and{' '} responseCode were being dropped. Combined with the - openiap-google 1.3.32 change, Dart callers inspecting{' '} + openiap-google 2.0.0 change, Dart callers inspecting{' '} PurchaseError.debugMessage now see Play's exact rejection reason — useful for diagnosing{' '} DEVELOPER_ERROR surfaces such as{' '} @@ -183,7 +210,7 @@ function Releases() { attach adb.

  • - Picks up openiap-google 1.3.32 (debug message + data class error + Picks up openiap-google 2.0.0 (debug message + data class error types).
  • diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts index e9b0132f..75e42415 100644 --- a/packages/gql/codegen/plugins/gdscript.ts +++ b/packages/gql/codegen/plugins/gdscript.ts @@ -429,12 +429,17 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`\t\t\tdict["${graphqlName}"] = ${enumConstName}[${fieldName}]`); this.emit(`\t\telse:`); this.emit(`\t\t\tdict["${graphqlName}"] = ${fieldName}`); + } else if (type.nullable && this.isNullableScalar(type)) { + // Nullable scalars are declared as Variant/null (see field + // declarations above). Emit the key only when the value is + // actually set — including legitimate `0`, `false`, and `""` — + // so the absent-vs-null distinction survives the round-trip. A + // `null` value means "unset" and is omitted from the dict. + this.emit(`\t\tif ${fieldName} != null:`); + this.emit(`\t\t\tdict["${graphqlName}"] = ${fieldName}`); } else { - // Emit the value verbatim for scalars. Non-nullable scalars carry - // their type-appropriate default (e.g. "" for String); nullable - // scalars are declared as Variant/null (see field declarations - // above) so `null` flows through to_dict naturally without having - // to drop legitimate zero/false/empty values. + // Non-nullable scalars carry their type-appropriate default + // (e.g. "" for String) and are always emitted. this.emit(`\t\tdict["${graphqlName}"] = ${fieldName}`); } } diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 18582a23..fed9bd9b 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -356,17 +356,26 @@ class ActiveSubscription: var dict = {} dict["productId"] = product_id dict["isActive"] = is_active - dict["expirationDateIOS"] = expiration_date_ios - dict["autoRenewingAndroid"] = auto_renewing_android - dict["environmentIOS"] = environment_ios - dict["willExpireSoon"] = will_expire_soon - dict["daysUntilExpirationIOS"] = days_until_expiration_ios + if expiration_date_ios != null: + dict["expirationDateIOS"] = expiration_date_ios + if auto_renewing_android != null: + dict["autoRenewingAndroid"] = auto_renewing_android + if environment_ios != null: + dict["environmentIOS"] = environment_ios + if will_expire_soon != null: + dict["willExpireSoon"] = will_expire_soon + if days_until_expiration_ios != null: + dict["daysUntilExpirationIOS"] = days_until_expiration_ios dict["transactionId"] = transaction_id - dict["purchaseToken"] = purchase_token + if purchase_token != null: + dict["purchaseToken"] = purchase_token dict["transactionDate"] = transaction_date - dict["basePlanIdAndroid"] = base_plan_id_android - dict["purchaseTokenAndroid"] = purchase_token_android - dict["currentPlanId"] = current_plan_id + if base_plan_id_android != null: + dict["basePlanIdAndroid"] = base_plan_id_android + if purchase_token_android != null: + dict["purchaseTokenAndroid"] = purchase_token_android + if current_plan_id != null: + dict["currentPlanId"] = current_plan_id if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -430,9 +439,12 @@ class AppTransaction: dict["signedDate"] = signed_date dict["appId"] = app_id dict["appVersionId"] = app_version_id - dict["preorderDate"] = preorder_date - dict["appTransactionId"] = app_transaction_id - dict["originalPlatform"] = original_platform + if preorder_date != null: + dict["preorderDate"] = preorder_date + if app_transaction_id != null: + dict["appTransactionId"] = app_transaction_id + if original_platform != null: + dict["originalPlatform"] = original_platform return dict ## Result of checking billing program availability (Android) Available in Google Play Billing Library 8.2.0+ @@ -517,7 +529,8 @@ class BillingResultAndroid: func to_dict() -> Dictionary: var dict = {} dict["responseCode"] = response_code - dict["debugMessage"] = debug_message + if debug_message != null: + dict["debugMessage"] = debug_message if SUB_RESPONSE_CODE_ANDROID_VALUES.has(sub_response_code): dict["subResponseCode"] = SUB_RESPONSE_CODE_ANDROID_VALUES[sub_response_code] else: @@ -581,7 +594,8 @@ class DiscountDisplayInfoAndroid: func to_dict() -> Dictionary: var dict = {} - dict["percentageDiscount"] = percentage_discount + if percentage_discount != null: + dict["percentageDiscount"] = percentage_discount if discount_amount != null and discount_amount.has_method("to_dict"): dict["discountAmount"] = discount_amount.to_dict() else: @@ -635,7 +649,8 @@ class DiscountIOS: else: dict["paymentMode"] = payment_mode dict["subscriptionPeriod"] = subscription_period - dict["localizedPrice"] = localized_price + if localized_price != null: + dict["localizedPrice"] = localized_price return dict ## Standardized one-time product discount offer. Provides a unified interface for one-time purchase discounts across platforms. Currently supported on Android (Google Play Billing 7.0+). iOS does not support one-time purchase discounts in the same way. @see https://openiap.dev/docs/features/discount @@ -727,7 +742,8 @@ class DiscountOffer: func to_dict() -> Dictionary: var dict = {} - dict["id"] = id + if id != null: + dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price dict["currency"] = currency @@ -735,12 +751,17 @@ class DiscountOffer: dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: dict["type"] = type - dict["offerTokenAndroid"] = offer_token_android + if offer_token_android != null: + dict["offerTokenAndroid"] = offer_token_android dict["offerTagsAndroid"] = offer_tags_android - dict["fullPriceMicrosAndroid"] = full_price_micros_android - dict["percentageDiscountAndroid"] = percentage_discount_android - dict["discountAmountMicrosAndroid"] = discount_amount_micros_android - dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android + if full_price_micros_android != null: + dict["fullPriceMicrosAndroid"] = full_price_micros_android + if percentage_discount_android != null: + dict["percentageDiscountAndroid"] = percentage_discount_android + if discount_amount_micros_android != null: + dict["discountAmountMicrosAndroid"] = discount_amount_micros_android + if formatted_discount_amount_android != null: + dict["formattedDiscountAmountAndroid"] = formatted_discount_amount_android if valid_time_window_android != null and valid_time_window_android.has_method("to_dict"): dict["validTimeWindowAndroid"] = valid_time_window_android.to_dict() else: @@ -757,7 +778,8 @@ class DiscountOffer: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionIdAndroid"] = purchase_option_id_android + if purchase_option_id_android != null: + dict["purchaseOptionIdAndroid"] = purchase_option_id_android return dict ## iOS DiscountOffer (output type). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer @@ -868,7 +890,8 @@ class ExternalPurchaseCustomLinkNoticeResultIOS: func to_dict() -> Dictionary: var dict = {} dict["continued"] = continued - dict["error"] = error + if error != null: + dict["error"] = error return dict ## Result of requesting an ExternalPurchaseCustomLink token (iOS 18.1+). @@ -888,8 +911,10 @@ class ExternalPurchaseCustomLinkTokenResultIOS: func to_dict() -> Dictionary: var dict = {} - dict["token"] = token - dict["error"] = error + if token != null: + dict["token"] = token + if error != null: + dict["error"] = error return dict ## Result of presenting an external purchase link @@ -910,7 +935,8 @@ class ExternalPurchaseLinkResultIOS: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["error"] = error + if error != null: + dict["error"] = error return dict ## Result of presenting external purchase notice sheet (iOS 17.4+) Returns the token when user continues to external purchase. @@ -942,8 +968,10 @@ class ExternalPurchaseNoticeResultIOS: dict["result"] = EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES[result] else: dict["result"] = result - dict["error"] = error - dict["externalPurchaseToken"] = external_purchase_token + if error != null: + dict["error"] = error + if external_purchase_token != null: + dict["externalPurchaseToken"] = external_purchase_token return dict ## Installment plan details for subscription offers (Android) Contains information about the installment plan commitment. Available in Google Play Billing Library 7.0+ @@ -1197,11 +1225,14 @@ class ProductAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1326,13 +1357,15 @@ class ProductAndroidOneTimePurchaseOfferDetail: func to_dict() -> Dictionary: var dict = {} - dict["offerId"] = offer_id + if offer_id != null: + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags dict["priceCurrencyCode"] = price_currency_code dict["formattedPrice"] = formatted_price dict["priceAmountMicros"] = price_amount_micros - dict["fullPriceMicros"] = full_price_micros + if full_price_micros != null: + dict["fullPriceMicros"] = full_price_micros if discount_display_info != null and discount_display_info.has_method("to_dict"): dict["discountDisplayInfo"] = discount_display_info.to_dict() else: @@ -1353,7 +1386,8 @@ class ProductAndroidOneTimePurchaseOfferDetail: dict["rentalDetailsAndroid"] = rental_details_android.to_dict() else: dict["rentalDetailsAndroid"] = rental_details_android - dict["purchaseOptionId"] = purchase_option_id + if purchase_option_id != null: + dict["purchaseOptionId"] = purchase_option_id return dict class ProductIOS: @@ -1442,11 +1476,14 @@ class ProductIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1578,11 +1615,14 @@ class ProductSubscriptionAndroid: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1669,7 +1709,8 @@ class ProductSubscriptionAndroidOfferDetails: func to_dict() -> Dictionary: var dict = {} dict["basePlanId"] = base_plan_id - dict["offerId"] = offer_id + if offer_id != null: + dict["offerId"] = offer_id dict["offerToken"] = offer_token dict["offerTags"] = offer_tags if pricing_phases != null and pricing_phases.has_method("to_dict"): @@ -1811,11 +1852,14 @@ class ProductSubscriptionIOS: dict["type"] = PRODUCT_TYPE_VALUES[type] else: dict["type"] = type - dict["displayName"] = display_name + if display_name != null: + dict["displayName"] = display_name dict["displayPrice"] = display_price dict["currency"] = currency - dict["price"] = price - dict["debugDescription"] = debug_description + if price != null: + dict["price"] = price + if debug_description != null: + dict["debugDescription"] = debug_description if IAP_PLATFORM_VALUES.has(platform): dict["platform"] = IAP_PLATFORM_VALUES[platform] else: @@ -1851,18 +1895,22 @@ class ProductSubscriptionIOS: dict["discountsIOS"] = arr else: dict["discountsIOS"] = null - dict["introductoryPriceIOS"] = introductory_price_ios - dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios + if introductory_price_ios != null: + dict["introductoryPriceIOS"] = introductory_price_ios + if introductory_price_as_amount_ios != null: + dict["introductoryPriceAsAmountIOS"] = introductory_price_as_amount_ios if PAYMENT_MODE_IOS_VALUES.has(introductory_price_payment_mode_ios): dict["introductoryPricePaymentModeIOS"] = PAYMENT_MODE_IOS_VALUES[introductory_price_payment_mode_ios] else: dict["introductoryPricePaymentModeIOS"] = introductory_price_payment_mode_ios - dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios + if introductory_price_number_of_periods_ios != null: + dict["introductoryPriceNumberOfPeriodsIOS"] = introductory_price_number_of_periods_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(introductory_price_subscription_period_ios): dict["introductoryPriceSubscriptionPeriodIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[introductory_price_subscription_period_ios] else: dict["introductoryPriceSubscriptionPeriodIOS"] = introductory_price_subscription_period_ios - dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios + if subscription_period_number_ios != null: + dict["subscriptionPeriodNumberIOS"] = subscription_period_number_ios if SUBSCRIPTION_PERIOD_IOS_VALUES.has(subscription_period_unit_ios): dict["subscriptionPeriodUnitIOS"] = SUBSCRIPTION_PERIOD_IOS_VALUES[subscription_period_unit_ios] else: @@ -1964,9 +2012,11 @@ class PurchaseAndroid: dict["id"] = id dict["productId"] = product_id dict["ids"] = ids - dict["transactionId"] = transaction_id + if transaction_id != null: + dict["transactionId"] = transaction_id dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != null: + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -1981,16 +2031,26 @@ class PurchaseAndroid: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id - dict["dataAndroid"] = data_android - dict["signatureAndroid"] = signature_android - dict["autoRenewingAndroid"] = auto_renewing_android - dict["isAcknowledgedAndroid"] = is_acknowledged_android - dict["packageNameAndroid"] = package_name_android - dict["developerPayloadAndroid"] = developer_payload_android - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - dict["isSuspendedAndroid"] = is_suspended_android + if current_plan_id != null: + dict["currentPlanId"] = current_plan_id + if data_android != null: + dict["dataAndroid"] = data_android + if signature_android != null: + dict["signatureAndroid"] = signature_android + if auto_renewing_android != null: + dict["autoRenewingAndroid"] = auto_renewing_android + if is_acknowledged_android != null: + dict["isAcknowledgedAndroid"] = is_acknowledged_android + if package_name_android != null: + dict["packageNameAndroid"] = package_name_android + if developer_payload_android != null: + dict["developerPayloadAndroid"] = developer_payload_android + if obfuscated_account_id_android != null: + dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android + if obfuscated_profile_id_android != null: + dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android + if is_suspended_android != null: + dict["isSuspendedAndroid"] = is_suspended_android if pending_purchase_update_android != null and pending_purchase_update_android.has_method("to_dict"): dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android.to_dict() else: @@ -2026,8 +2086,10 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - dict["productId"] = product_id - dict["debugMessage"] = debug_message + if product_id != null: + dict["productId"] = product_id + if debug_message != null: + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2163,7 +2225,8 @@ class PurchaseIOS: dict["productId"] = product_id dict["ids"] = ids dict["transactionDate"] = transaction_date - dict["purchaseToken"] = purchase_token + if purchase_token != null: + dict["purchaseToken"] = purchase_token if IAP_STORE_VALUES.has(store): dict["store"] = IAP_STORE_VALUES[store] else: @@ -2178,32 +2241,53 @@ class PurchaseIOS: else: dict["purchaseState"] = purchase_state dict["isAutoRenewing"] = is_auto_renewing - dict["currentPlanId"] = current_plan_id + if current_plan_id != null: + dict["currentPlanId"] = current_plan_id dict["transactionId"] = transaction_id - dict["quantityIOS"] = quantity_ios - dict["originalTransactionDateIOS"] = original_transaction_date_ios - dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios - dict["appAccountToken"] = app_account_token - dict["expirationDateIOS"] = expiration_date_ios - dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios - dict["environmentIOS"] = environment_ios - dict["storefrontCountryCodeIOS"] = storefront_country_code_ios - dict["appBundleIdIOS"] = app_bundle_id_ios - dict["subscriptionGroupIdIOS"] = subscription_group_id_ios - dict["isUpgradedIOS"] = is_upgraded_ios - dict["ownershipTypeIOS"] = ownership_type_ios - dict["reasonIOS"] = reason_ios - dict["reasonStringRepresentationIOS"] = reason_string_representation_ios - dict["transactionReasonIOS"] = transaction_reason_ios - dict["revocationDateIOS"] = revocation_date_ios - dict["revocationReasonIOS"] = revocation_reason_ios + if quantity_ios != null: + dict["quantityIOS"] = quantity_ios + if original_transaction_date_ios != null: + dict["originalTransactionDateIOS"] = original_transaction_date_ios + if original_transaction_identifier_ios != null: + dict["originalTransactionIdentifierIOS"] = original_transaction_identifier_ios + if app_account_token != null: + dict["appAccountToken"] = app_account_token + if expiration_date_ios != null: + dict["expirationDateIOS"] = expiration_date_ios + if web_order_line_item_id_ios != null: + dict["webOrderLineItemIdIOS"] = web_order_line_item_id_ios + if environment_ios != null: + dict["environmentIOS"] = environment_ios + if storefront_country_code_ios != null: + dict["storefrontCountryCodeIOS"] = storefront_country_code_ios + if app_bundle_id_ios != null: + dict["appBundleIdIOS"] = app_bundle_id_ios + if subscription_group_id_ios != null: + dict["subscriptionGroupIdIOS"] = subscription_group_id_ios + if is_upgraded_ios != null: + dict["isUpgradedIOS"] = is_upgraded_ios + if ownership_type_ios != null: + dict["ownershipTypeIOS"] = ownership_type_ios + if reason_ios != null: + dict["reasonIOS"] = reason_ios + if reason_string_representation_ios != null: + dict["reasonStringRepresentationIOS"] = reason_string_representation_ios + if transaction_reason_ios != null: + dict["transactionReasonIOS"] = transaction_reason_ios + if revocation_date_ios != null: + dict["revocationDateIOS"] = revocation_date_ios + if revocation_reason_ios != null: + dict["revocationReasonIOS"] = revocation_reason_ios if offer_ios != null and offer_ios.has_method("to_dict"): dict["offerIOS"] = offer_ios.to_dict() else: dict["offerIOS"] = offer_ios - dict["currencyCodeIOS"] = currency_code_ios - dict["currencySymbolIOS"] = currency_symbol_ios - dict["countryCodeIOS"] = country_code_ios + if currency_code_ios != null: + dict["currencyCodeIOS"] = currency_code_ios + if currency_symbol_ios != null: + dict["currencySymbolIOS"] = currency_symbol_ios + if country_code_ios != null: + dict["countryCodeIOS"] = country_code_ios if renewal_info_ios != null and renewal_info_ios.has_method("to_dict"): dict["renewalInfoIOS"] = renewal_info_ios.to_dict() else: @@ -2247,7 +2331,8 @@ class RefundResultIOS: func to_dict() -> Dictionary: var dict = {} dict["status"] = status - dict["message"] = message + if message != null: + dict["message"] = message return dict ## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo @@ -2300,17 +2385,27 @@ class RenewalInfoIOS: func to_dict() -> Dictionary: var dict = {} - dict["jsonRepresentation"] = json_representation + if json_representation != null: + dict["jsonRepresentation"] = json_representation dict["willAutoRenew"] = will_auto_renew - dict["autoRenewPreference"] = auto_renew_preference - dict["expirationReason"] = expiration_reason - dict["gracePeriodExpirationDate"] = grace_period_expiration_date - dict["isInBillingRetry"] = is_in_billing_retry - dict["pendingUpgradeProductId"] = pending_upgrade_product_id - dict["priceIncreaseStatus"] = price_increase_status - dict["renewalDate"] = renewal_date - dict["renewalOfferId"] = renewal_offer_id - dict["renewalOfferType"] = renewal_offer_type + if auto_renew_preference != null: + dict["autoRenewPreference"] = auto_renew_preference + if expiration_reason != null: + dict["expirationReason"] = expiration_reason + if grace_period_expiration_date != null: + dict["gracePeriodExpirationDate"] = grace_period_expiration_date + if is_in_billing_retry != null: + dict["isInBillingRetry"] = is_in_billing_retry + if pending_upgrade_product_id != null: + dict["pendingUpgradeProductId"] = pending_upgrade_product_id + if price_increase_status != null: + dict["priceIncreaseStatus"] = price_increase_status + if renewal_date != null: + dict["renewalDate"] = renewal_date + if renewal_offer_id != null: + dict["renewalOfferId"] = renewal_offer_id + if renewal_offer_type != null: + dict["renewalOfferType"] = renewal_offer_type return dict ## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+ @@ -2331,7 +2426,8 @@ class RentalDetailsAndroid: func to_dict() -> Dictionary: var dict = {} dict["rentalPeriod"] = rental_period - dict["rentalExpirationPeriod"] = rental_expiration_period + if rental_expiration_period != null: + dict["rentalExpirationPeriod"] = rental_expiration_period return dict class RequestVerifyPurchaseWithIapkitResult: @@ -2530,7 +2626,8 @@ class SubscriptionOffer: dict["id"] = id dict["displayPrice"] = display_price dict["price"] = price - dict["currency"] = currency + if currency != null: + dict["currency"] = currency if DISCOUNT_OFFER_TYPE_VALUES.has(type): dict["type"] = DISCOUNT_OFFER_TYPE_VALUES[type] else: @@ -2539,19 +2636,28 @@ class SubscriptionOffer: dict["period"] = period.to_dict() else: dict["period"] = period - dict["periodCount"] = period_count + if period_count != null: + dict["periodCount"] = period_count if PAYMENT_MODE_VALUES.has(payment_mode): dict["paymentMode"] = PAYMENT_MODE_VALUES[payment_mode] else: dict["paymentMode"] = payment_mode - dict["keyIdentifierIOS"] = key_identifier_ios - dict["nonceIOS"] = nonce_ios - dict["signatureIOS"] = signature_ios - dict["timestampIOS"] = timestamp_ios - dict["numberOfPeriodsIOS"] = number_of_periods_ios - dict["localizedPriceIOS"] = localized_price_ios - dict["basePlanIdAndroid"] = base_plan_id_android - dict["offerTokenAndroid"] = offer_token_android + if key_identifier_ios != null: + dict["keyIdentifierIOS"] = key_identifier_ios + if nonce_ios != null: + dict["nonceIOS"] = nonce_ios + if signature_ios != null: + dict["signatureIOS"] = signature_ios + if timestamp_ios != null: + dict["timestampIOS"] = timestamp_ios + if number_of_periods_ios != null: + dict["numberOfPeriodsIOS"] = number_of_periods_ios + if localized_price_ios != null: + dict["localizedPriceIOS"] = localized_price_ios + if base_plan_id_android != null: + dict["basePlanIdAndroid"] = base_plan_id_android + if offer_token_android != null: + dict["offerTokenAndroid"] = offer_token_android dict["offerTagsAndroid"] = offer_tags_android if pricing_phases_android != null and pricing_phases_android.has_method("to_dict"): dict["pricingPhasesAndroid"] = pricing_phases_android.to_dict() @@ -2805,10 +2911,14 @@ class VerifyPurchaseResultAndroid: var dict = {} dict["autoRenewing"] = auto_renewing dict["betaProduct"] = beta_product - dict["cancelDate"] = cancel_date - dict["cancelReason"] = cancel_reason - dict["deferredDate"] = deferred_date - dict["deferredSku"] = deferred_sku + if cancel_date != null: + dict["cancelDate"] = cancel_date + if cancel_reason != null: + dict["cancelReason"] = cancel_reason + if deferred_date != null: + dict["deferredDate"] = deferred_date + if deferred_sku != null: + dict["deferredSku"] = deferred_sku dict["freeTrialEndDate"] = free_trial_end_date dict["gracePeriodEndDate"] = grace_period_end_date dict["parentProductId"] = parent_product_id @@ -2841,7 +2951,8 @@ class VerifyPurchaseResultHorizon: func to_dict() -> Dictionary: var dict = {} dict["success"] = success - dict["grantTime"] = grant_time + if grant_time != null: + dict["grantTime"] = grant_time return dict class VerifyPurchaseResultIOS: @@ -2889,7 +3000,8 @@ class VerifyPurchaseWithProviderError: func to_dict() -> Dictionary: var dict = {} dict["message"] = message - dict["code"] = code + if code != null: + dict["code"] = code return dict class VerifyPurchaseWithProviderResult: From e5d23f41e21885112b5e4cae4af22a1ab0ce2a3e Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 21:46:59 +0900 Subject: [PATCH 7/9] ci(gql): verify generated types are drift-clean after bun run generate Previously the "Test GQL Types" job only checked that the generated files *exist* after `bun run generate`, not that they *match* what's committed. That allowed plugin changes (or missed regenerations) to ship with stale copies in the framework libraries. - Add a `git diff --exit-code` step after `bun run generate` so CI fails with a clear message if the codegen output diverges from the committed files. - Revert the in-flight attempt to run `dart format` inside sync-to-platforms.mjs. The flutter_inapp_purchase CLAUDE.md explicitly excludes `lib/types.dart` from the Dart format check, and running the formatter inside the sync would have required a Dart SDK on every CI runner that hits this job (plus hidden reformat churn every time the formatter version shifts). Instead the sync now copies the raw generator output verbatim, matching every other library. - Re-commit `libraries/flutter_inapp_purchase/lib/types.dart` in the raw generator-output shape so `bun run generate` is idempotent. An earlier local `dart format` pass had introduced blank lines between enum entries that the codegen doesn't emit; the checked-in file now matches `packages/gql/src/generated/types.dart` byte-for-byte. Verified locally: `bun run generate` run twice in succession produces no working-tree diff. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 14 + .../flutter_inapp_purchase/lib/types.dart | 1123 ++++------------- packages/gql/scripts/sync-to-platforms.mjs | 4 + 3 files changed, 255 insertions(+), 886 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88fb7e7a..5698b5c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,20 @@ jobs: test -f src/generated/Types.swift || exit 1 test -f src/generated/types.dart || exit 1 + - name: Verify generated types are committed (no drift) + run: | + # `bun run generate` is expected to be idempotent: running it + # against the checked-in schema must produce the same bytes + # as the files checked into the repo. If this step fails, + # somebody changed a codegen plugin or the schema without + # re-running `bun run generate` + committing the output. + if ! git diff --exit-code; then + echo "" + echo "::error::Generated types differ from checked-in copies." + echo "Run 'cd packages/gql && bun run generate' locally and commit the result." + exit 1 + fi + test-android: name: Test Android runs-on: ubuntu-latest diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index df15cc56..67d7d3f5 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -16,12 +16,10 @@ import 'dart:async'; enum AlternativeBillingModeAndroid { /// Standard Google Play billing (default) None('none'), - /// User choice billing - user can select between Google Play or alternative /// Requires Google Play Billing Library 7.0+ /// @deprecated Use BillingProgramAndroid.USER_CHOICE_BILLING instead UserChoice('user-choice'), - /// Alternative billing only - no Google Play billing option /// Requires Google Play Billing Library 6.2+ /// @deprecated Use BillingProgramAndroid.EXTERNAL_OFFER instead @@ -51,22 +49,18 @@ enum AlternativeBillingModeAndroid { enum BillingProgramAndroid { /// Unspecified billing program. Do not use. Unspecified('unspecified'), - /// User Choice Billing program. /// User can select between Google Play Billing or alternative billing. /// Available in Google Play Billing Library 7.0+ UserChoiceBilling('user-choice-billing'), - /// External Content Links program. /// Allows linking to external content outside the app. /// Available in Google Play Billing Library 8.2.0+ ExternalContentLink('external-content-link'), - /// External Offers program. /// Allows offering digital content purchases outside the app. /// Available in Google Play Billing Library 8.2.0+ ExternalOffer('external-offer'), - /// External Payments program (Japan only). /// Allows presenting a side-by-side choice between Google Play Billing and developer's external payment option. /// Users can choose to complete the purchase on the developer's website. @@ -102,11 +96,9 @@ enum BillingProgramAndroid { enum DeveloperBillingLaunchModeAndroid { /// Unspecified launch mode. Do not use. Unspecified('unspecified'), - /// Google Play will launch the link in an external browser or eligible app. /// Use this when you want Play to handle launching the external payment URL. LaunchInExternalBrowserOrApp('launch-in-external-browser-or-app'), - /// The caller app will launch the link after Play returns control. /// Use this when you want to handle launching the external payment URL yourself. CallerWillLaunchLink('caller-will-launch-link'); @@ -124,8 +116,7 @@ enum DeveloperBillingLaunchModeAndroid { case 'caller-will-launch-link': return DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink; } - throw ArgumentError( - 'Unknown DeveloperBillingLaunchModeAndroid value: $value'); + throw ArgumentError('Unknown DeveloperBillingLaunchModeAndroid value: $value'); } String toJson() => value; @@ -136,10 +127,8 @@ enum DeveloperBillingLaunchModeAndroid { enum DiscountOfferType { /// Introductory offer for new subscribers (first-time purchase discount) Introductory('introductory'), - /// Promotional offer for existing or returning subscribers Promotional('promotional'), - /// One-time product discount (Android only, Google Play Billing 7.0+) OneTime('one-time'); @@ -300,10 +289,8 @@ enum ErrorCode { enum ExternalLinkLaunchModeAndroid { /// Unspecified launch mode. Do not use. Unspecified('unspecified'), - /// Play will launch the URL in an external browser or eligible app LaunchInExternalBrowserOrApp('launch-in-external-browser-or-app'), - /// Play will not launch the URL. The app handles launching the URL after Play returns control. CallerWillLaunchLink('caller-will-launch-link'); @@ -332,10 +319,8 @@ enum ExternalLinkLaunchModeAndroid { enum ExternalLinkTypeAndroid { /// Unspecified link type. Do not use. Unspecified('unspecified'), - /// The link will direct users to a digital content offer LinkToDigitalContentOffer('link-to-digital-content-offer'), - /// The link will direct users to download an app LinkToAppDownload('link-to-app-download'); @@ -375,8 +360,7 @@ enum ExternalPurchaseCustomLinkNoticeTypeIOS { case 'browser': return ExternalPurchaseCustomLinkNoticeTypeIOS.Browser; } - throw ArgumentError( - 'Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value'); + throw ArgumentError('Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value'); } String toJson() => value; @@ -389,7 +373,6 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { /// Token for customer acquisition tracking. /// Use this when a new customer makes their first purchase through external link. Acquisition('acquisition'), - /// Token for ongoing services tracking. /// Use this for existing customers making additional purchases. Services('services'); @@ -405,8 +388,7 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { case 'services': return ExternalPurchaseCustomLinkTokenTypeIOS.Services; } - throw ArgumentError( - 'Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value'); + throw ArgumentError('Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value'); } String toJson() => value; @@ -416,7 +398,6 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { enum ExternalPurchaseNoticeAction { /// User chose to continue to external purchase Continue('continue'), - /// User dismissed the notice sheet Dismissed('dismissed'); @@ -442,7 +423,6 @@ enum IapEvent { PurchaseError('purchase-error'), PromotedProductIOS('promoted-product-ios'), UserChoiceBillingAndroid('user-choice-billing-android'), - /// Fired when user selects developer-provided billing option in external payments flow. /// Available on Android with Google Play Billing Library 8.3.0+ DeveloperProvidedBillingAndroid('developer-provided-billing-android'); @@ -474,28 +454,20 @@ enum IapEvent { enum IapkitPurchaseState { /// User is entitled to the product (purchase is complete and active). Entitled('entitled'), - /// Receipt is valid but still needs server acknowledgment. PendingAcknowledgment('pending-acknowledgment'), - /// Purchase is in progress or awaiting confirmation. Pending('pending'), - /// Purchase was cancelled or refunded. Canceled('canceled'), - /// Subscription or entitlement has expired. Expired('expired'), - /// Consumable purchase is ready to be fulfilled. ReadyToConsume('ready-to-consume'), - /// Consumable item has been fulfilled/consumed. Consumed('consumed'), - /// Purchase state could not be determined. Unknown('unknown'), - /// Purchase receipt is not authentic (fraudulent or tampered). Inauthentic('inauthentic'); @@ -583,13 +555,10 @@ enum IapStore { enum PaymentMode { /// Free trial period - no charge during offer FreeTrial('free-trial'), - /// Pay each period at reduced price PayAsYouGo('pay-as-you-go'), - /// Pay full discounted amount upfront PayUpFront('pay-up-front'), - /// Unknown or unspecified payment mode Unknown('unknown'); @@ -672,13 +641,10 @@ enum ProductQueryType { enum ProductStatusAndroid { /// Product was successfully fetched Ok('ok'), - /// Product not found - the SKU doesn't exist in the Play Console NotFound('not-found'), - /// No offers available for the user - product exists but user is not eligible for any offers NoOffersAvailable('no-offers-available'), - /// Unknown error occurred while fetching the product Unknown('unknown'); @@ -798,11 +764,8 @@ enum PurchaseVerificationProvider { enum SubResponseCodeAndroid { /// No specific sub-response code applies NoApplicableSubResponseCode('no-applicable-sub-response-code'), - /// User's payment method has insufficient funds - PaymentDeclinedDueToInsufficientFunds( - 'payment-declined-due-to-insufficient-funds'), - + PaymentDeclinedDueToInsufficientFunds('payment-declined-due-to-insufficient-funds'), /// User doesn't meet subscription offer eligibility requirements UserIneligible('user-ineligible'); @@ -828,7 +791,6 @@ enum SubResponseCodeAndroid { enum SubscriptionOfferTypeIOS { Introductory('introductory'), Promotional('promotional'), - /// Win-back offer type (iOS 18+) /// Used to re-engage churned subscribers with a discount or free trial. WinBack('win-back'); @@ -919,22 +881,16 @@ enum SubscriptionPeriodUnit { enum SubscriptionReplacementModeAndroid { /// Unknown replacement mode. Do not use. UnknownReplacementMode('unknown-replacement-mode'), - /// Replacement takes effect immediately, and the new expiration time will be prorated. WithTimeProration('with-time-proration'), - /// Replacement takes effect immediately, and the billing cycle remains the same. ChargeProratedPrice('charge-prorated-price'), - /// Replacement takes effect immediately, and the user is charged full price immediately. ChargeFullPrice('charge-full-price'), - /// Replacement takes effect when the old plan expires. WithoutProration('without-proration'), - /// Replacement takes effect when the old plan expires, and the user is not charged. Deferred('deferred'), - /// Keep the existing payment schedule unchanged for the item (8.1.0+) KeepExisting('keep-existing'); @@ -959,8 +915,7 @@ enum SubscriptionReplacementModeAndroid { case 'keep-existing': return SubscriptionReplacementModeAndroid.KeepExisting; } - throw ArgumentError( - 'Unknown SubscriptionReplacementModeAndroid value: $value'); + throw ArgumentError('Unknown SubscriptionReplacementModeAndroid value: $value'); } String toJson() => value; @@ -993,11 +948,9 @@ abstract class PurchaseCommon { IapPlatform get platform; String get productId; PurchaseState get purchaseState; - /// Unified purchase token (iOS JWS, Android purchaseToken) String? get purchaseToken; int get quantity; - /// Store where purchase was made IapStore get store; double get transactionDate; @@ -1025,7 +978,6 @@ class ActiveSubscription { final bool? autoRenewingAndroid; final String? basePlanIdAndroid; - /// The current plan identifier. This is: /// - On Android: the basePlanId (e.g., "premium", "premium-year") /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") @@ -1037,16 +989,13 @@ class ActiveSubscription { final bool isActive; final String productId; final String? purchaseToken; - /// Required for subscription upgrade/downgrade on Android final String? purchaseTokenAndroid; - /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, /// pending upgrades/downgrades, and auto-renewal preferences. final RenewalInfoIOS? renewalInfoIOS; final double transactionDate; final String transactionId; - /// @deprecated iOS only - use daysUntilExpirationIOS instead. /// Whether the subscription will expire soon (within 7 days). /// Consider using daysUntilExpirationIOS for more precise control. @@ -1057,18 +1006,14 @@ class ActiveSubscription { autoRenewingAndroid: json['autoRenewingAndroid'] as bool?, basePlanIdAndroid: json['basePlanIdAndroid'] as String?, currentPlanId: json['currentPlanId'] as String?, - daysUntilExpirationIOS: - (json['daysUntilExpirationIOS'] as num?)?.toDouble(), + daysUntilExpirationIOS: (json['daysUntilExpirationIOS'] as num?)?.toDouble(), environmentIOS: json['environmentIOS'] as String?, expirationDateIOS: (json['expirationDateIOS'] as num?)?.toDouble(), isActive: json['isActive'] as bool, productId: json['productId'] as String, purchaseToken: json['purchaseToken'] as String?, purchaseTokenAndroid: json['purchaseTokenAndroid'] as String?, - renewalInfoIOS: json['renewalInfoIOS'] != null - ? RenewalInfoIOS.fromJson( - json['renewalInfoIOS'] as Map) - : null, + renewalInfoIOS: json['renewalInfoIOS'] != null ? RenewalInfoIOS.fromJson(json['renewalInfoIOS'] as Map) : null, transactionDate: (json['transactionDate'] as num).toDouble(), transactionId: json['transactionId'] as String, willExpireSoon: json['willExpireSoon'] as bool?, @@ -1175,15 +1120,12 @@ class BillingProgramAvailabilityResultAndroid { /// The billing program that was checked final BillingProgramAndroid billingProgram; - /// Whether the billing program is available for the user final bool isAvailable; - factory BillingProgramAvailabilityResultAndroid.fromJson( - Map json) { + factory BillingProgramAvailabilityResultAndroid.fromJson(Map json) { return BillingProgramAvailabilityResultAndroid( - billingProgram: - BillingProgramAndroid.fromJson(json['billingProgram'] as String), + billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), isAvailable: json['isAvailable'] as bool, ); } @@ -1208,16 +1150,13 @@ class BillingProgramReportingDetailsAndroid { /// The billing program that the reporting details are associated with final BillingProgramAndroid billingProgram; - /// External transaction token used to report transactions made outside of Google Play Billing. /// This token must be used when reporting the external transaction to Google. final String externalTransactionToken; - factory BillingProgramReportingDetailsAndroid.fromJson( - Map json) { + factory BillingProgramReportingDetailsAndroid.fromJson(Map json) { return BillingProgramReportingDetailsAndroid( - billingProgram: - BillingProgramAndroid.fromJson(json['billingProgram'] as String), + billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), externalTransactionToken: json['externalTransactionToken'] as String, ); } @@ -1242,10 +1181,8 @@ class BillingResultAndroid { /// Debug message from the billing library final String? debugMessage; - /// The response code from the billing operation final int responseCode; - /// Sub-response code for more granular error information (8.0+). /// Provides additional context when responseCode indicates an error. final SubResponseCodeAndroid? subResponseCode; @@ -1254,9 +1191,7 @@ class BillingResultAndroid { return BillingResultAndroid( debugMessage: json['debugMessage'] as String?, responseCode: json['responseCode'] as int, - subResponseCode: json['subResponseCode'] != null - ? SubResponseCodeAndroid.fromJson(json['subResponseCode'] as String) - : null, + subResponseCode: json['subResponseCode'] != null ? SubResponseCodeAndroid.fromJson(json['subResponseCode'] as String) : null, ); } @@ -1283,8 +1218,7 @@ class DeveloperProvidedBillingDetailsAndroid { /// Must be reported within 24 hours of the transaction. final String externalTransactionToken; - factory DeveloperProvidedBillingDetailsAndroid.fromJson( - Map json) { + factory DeveloperProvidedBillingDetailsAndroid.fromJson(Map json) { return DeveloperProvidedBillingDetailsAndroid( externalTransactionToken: json['externalTransactionToken'] as String, ); @@ -1308,7 +1242,6 @@ class DiscountAmountAndroid { /// Discount amount in micro-units (1,000,000 = 1 unit of currency) final String discountAmountMicros; - /// Formatted discount amount with currency sign (e.g., "$4.99") final String formattedDiscountAmount; @@ -1339,17 +1272,13 @@ class DiscountDisplayInfoAndroid { /// Absolute discount amount details /// Only returned for fixed amount discounts final DiscountAmountAndroid? discountAmount; - /// Percentage discount (e.g., 33 for 33% off) /// Only returned for percentage-based discounts final int? percentageDiscount; factory DiscountDisplayInfoAndroid.fromJson(Map json) { return DiscountDisplayInfoAndroid( - discountAmount: json['discountAmount'] != null - ? DiscountAmountAndroid.fromJson( - json['discountAmount'] as Map) - : null, + discountAmount: json['discountAmount'] != null ? DiscountAmountAndroid.fromJson(json['discountAmount'] as Map) : null, percentageDiscount: json['percentageDiscount'] as int?, ); } @@ -1417,10 +1346,10 @@ class DiscountIOS { /// Standardized one-time product discount offer. /// Provides a unified interface for one-time purchase discounts across platforms. -/// +/// /// Currently supported on Android (Google Play Billing 7.0+). /// iOS does not support one-time purchase discounts in the same way. -/// +/// /// @see https://openiap.dev/docs/features/discount class DiscountOffer { const DiscountOffer({ @@ -1444,60 +1373,45 @@ class DiscountOffer { /// Currency code (ISO 4217, e.g., "USD") final String currency; - /// [Android] Fixed discount amount in micro-units. /// Only present for fixed amount discounts. final String? discountAmountMicrosAndroid; - /// Formatted display price string (e.g., "$4.99") final String displayPrice; - /// [Android] Formatted discount amount string (e.g., "$5.00 OFF"). final String? formattedDiscountAmountAndroid; - /// [Android] Original full price in micro-units before discount. /// Divide by 1,000,000 to get the actual price. /// Use for displaying strikethrough original price. final String? fullPriceMicrosAndroid; - /// Unique identifier for the offer. /// - iOS: Not applicable (one-time discounts not supported) /// - Android: offerId from ProductAndroidOneTimePurchaseOfferDetail final String? id; - /// [Android] Limited quantity information. /// Contains maximumQuantity and remainingQuantity. final LimitedQuantityInfoAndroid? limitedQuantityInfoAndroid; - /// [Android] List of tags associated with this offer. final List? offerTagsAndroid; - /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. final String? offerTokenAndroid; - /// [Android] Percentage discount (e.g., 33 for 33% off). /// Only present for percentage-based discounts. final int? percentageDiscountAndroid; - /// [Android] Pre-order details if this is a pre-order offer. /// Available in Google Play Billing Library 8.1.0+ final PreorderDetailsAndroid? preorderDetailsAndroid; - /// Numeric price value final double price; - /// [Android] Purchase option ID for this offer. /// Used to identify which purchase option the user selected. /// Available in Google Play Billing Library 7.0+ final String? purchaseOptionIdAndroid; - /// [Android] Rental details if this is a rental offer. final RentalDetailsAndroid? rentalDetailsAndroid; - /// Type of discount offer final DiscountOfferType type; - /// [Android] Valid time window for the offer. /// Contains startTimeMillis and endTimeMillis. final ValidTimeWindowAndroid? validTimeWindowAndroid; @@ -1505,39 +1419,21 @@ class DiscountOffer { factory DiscountOffer.fromJson(Map json) { return DiscountOffer( currency: json['currency'] as String, - discountAmountMicrosAndroid: - json['discountAmountMicrosAndroid'] as String?, + discountAmountMicrosAndroid: json['discountAmountMicrosAndroid'] as String?, displayPrice: json['displayPrice'] as String, - formattedDiscountAmountAndroid: - json['formattedDiscountAmountAndroid'] as String?, + formattedDiscountAmountAndroid: json['formattedDiscountAmountAndroid'] as String?, fullPriceMicrosAndroid: json['fullPriceMicrosAndroid'] as String?, id: json['id'] as String?, - limitedQuantityInfoAndroid: json['limitedQuantityInfoAndroid'] != null - ? LimitedQuantityInfoAndroid.fromJson( - json['limitedQuantityInfoAndroid'] as Map) - : null, - offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null - ? null - : (json['offerTagsAndroid'] as List?)! - .map((e) => e as String) - .toList(), + limitedQuantityInfoAndroid: json['limitedQuantityInfoAndroid'] != null ? LimitedQuantityInfoAndroid.fromJson(json['limitedQuantityInfoAndroid'] as Map) : null, + offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null ? null : (json['offerTagsAndroid'] as List?)!.map((e) => e as String).toList(), offerTokenAndroid: json['offerTokenAndroid'] as String?, percentageDiscountAndroid: json['percentageDiscountAndroid'] as int?, - preorderDetailsAndroid: json['preorderDetailsAndroid'] != null - ? PreorderDetailsAndroid.fromJson( - json['preorderDetailsAndroid'] as Map) - : null, + preorderDetailsAndroid: json['preorderDetailsAndroid'] != null ? PreorderDetailsAndroid.fromJson(json['preorderDetailsAndroid'] as Map) : null, price: (json['price'] as num).toDouble(), purchaseOptionIdAndroid: json['purchaseOptionIdAndroid'] as String?, - rentalDetailsAndroid: json['rentalDetailsAndroid'] != null - ? RentalDetailsAndroid.fromJson( - json['rentalDetailsAndroid'] as Map) - : null, + rentalDetailsAndroid: json['rentalDetailsAndroid'] != null ? RentalDetailsAndroid.fromJson(json['rentalDetailsAndroid'] as Map) : null, type: DiscountOfferType.fromJson(json['type'] as String), - validTimeWindowAndroid: json['validTimeWindowAndroid'] != null - ? ValidTimeWindowAndroid.fromJson( - json['validTimeWindowAndroid'] as Map) - : null, + validTimeWindowAndroid: json['validTimeWindowAndroid'] != null ? ValidTimeWindowAndroid.fromJson(json['validTimeWindowAndroid'] as Map) : null, ); } @@ -1578,16 +1474,12 @@ class DiscountOfferIOS { /// Discount identifier final String identifier; - /// Key identifier for validation final String keyIdentifier; - /// Cryptographic nonce final String nonce; - /// Signature for validation final String signature; - /// Timestamp of discount offer final double timestamp; @@ -1653,8 +1545,7 @@ class ExternalOfferAvailabilityResultAndroid { /// Whether external offers are available for the user final bool isAvailable; - factory ExternalOfferAvailabilityResultAndroid.fromJson( - Map json) { + factory ExternalOfferAvailabilityResultAndroid.fromJson(Map json) { return ExternalOfferAvailabilityResultAndroid( isAvailable: json['isAvailable'] as bool, ); @@ -1679,8 +1570,7 @@ class ExternalOfferReportingDetailsAndroid { /// External transaction token for reporting external offer transactions final String externalTransactionToken; - factory ExternalOfferReportingDetailsAndroid.fromJson( - Map json) { + factory ExternalOfferReportingDetailsAndroid.fromJson(Map json) { return ExternalOfferReportingDetailsAndroid( externalTransactionToken: json['externalTransactionToken'] as String, ); @@ -1703,12 +1593,10 @@ class ExternalPurchaseCustomLinkNoticeResultIOS { /// Whether the user chose to continue to external purchase final bool continued; - /// Optional error message if the presentation failed final String? error; - factory ExternalPurchaseCustomLinkNoticeResultIOS.fromJson( - Map json) { + factory ExternalPurchaseCustomLinkNoticeResultIOS.fromJson(Map json) { return ExternalPurchaseCustomLinkNoticeResultIOS( continued: json['continued'] as bool, error: json['error'] as String?, @@ -1733,13 +1621,11 @@ class ExternalPurchaseCustomLinkTokenResultIOS { /// Optional error message if token retrieval failed final String? error; - /// The external purchase token string. /// Report this token to Apple's External Purchase Server API. final String? token; - factory ExternalPurchaseCustomLinkTokenResultIOS.fromJson( - Map json) { + factory ExternalPurchaseCustomLinkTokenResultIOS.fromJson(Map json) { return ExternalPurchaseCustomLinkTokenResultIOS( error: json['error'] as String?, token: json['token'] as String?, @@ -1764,7 +1650,6 @@ class ExternalPurchaseLinkResultIOS { /// Optional error message if the presentation failed final String? error; - /// Whether the user completed the external purchase flow final bool success; @@ -1795,12 +1680,10 @@ class ExternalPurchaseNoticeResultIOS { /// Optional error message if the presentation failed final String? error; - /// External purchase token returned when user continues (iOS 17.4+). /// This token should be reported to Apple's External Purchase Server API. /// Only present when result is Continue. final String? externalPurchaseToken; - /// Notice result indicating user action final ExternalPurchaseNoticeAction result; @@ -1854,7 +1737,6 @@ class InstallmentPlanDetailsAndroid { /// For example, for a monthly subscription with commitmentPaymentsCount of 12, /// users will be charged monthly for 12 months after signup. final int commitmentPaymentsCount; - /// 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. @@ -1864,8 +1746,7 @@ class InstallmentPlanDetailsAndroid { factory InstallmentPlanDetailsAndroid.fromJson(Map json) { return InstallmentPlanDetailsAndroid( commitmentPaymentsCount: json['commitmentPaymentsCount'] as int, - subsequentCommitmentPaymentsCount: - json['subsequentCommitmentPaymentsCount'] as int, + subsequentCommitmentPaymentsCount: json['subsequentCommitmentPaymentsCount'] as int, ); } @@ -1888,7 +1769,6 @@ class LimitedQuantityInfoAndroid { /// Maximum quantity a user can purchase final int maximumQuantity; - /// Remaining quantity the user can still purchase final int remainingQuantity; @@ -1922,15 +1802,13 @@ class PendingPurchaseUpdateAndroid { /// Product IDs for the pending purchase update. /// These are the new products the user is switching to. final List products; - /// Purchase token for the pending transaction. /// Use this token to track or manage the pending purchase update. final String purchaseToken; factory PendingPurchaseUpdateAndroid.fromJson(Map json) { return PendingPurchaseUpdateAndroid( - products: - (json['products'] as List).map((e) => e as String).toList(), + products: (json['products'] as List).map((e) => e as String).toList(), purchaseToken: json['purchaseToken'] as String, ); } @@ -1955,15 +1833,13 @@ class PreorderDetailsAndroid { /// Pre-order presale end time in milliseconds since epoch. /// This is when the presale period ends and the product will be released. final String preorderPresaleEndTimeMillis; - /// Pre-order release time in milliseconds since epoch. /// This is when the product will be available to users who pre-ordered. final String preorderReleaseTimeMillis; factory PreorderDetailsAndroid.fromJson(Map json) { return PreorderDetailsAndroid( - preorderPresaleEndTimeMillis: - json['preorderPresaleEndTimeMillis'] as String, + preorderPresaleEndTimeMillis: json['preorderPresaleEndTimeMillis'] as String, preorderReleaseTimeMillis: json['preorderReleaseTimeMillis'] as String, ); } @@ -2027,9 +1903,7 @@ class PricingPhasesAndroid { factory PricingPhasesAndroid.fromJson(Map json) { return PricingPhasesAndroid( - pricingPhaseList: (json['pricingPhaseList'] as List) - .map((e) => PricingPhaseAndroid.fromJson(e as Map)) - .toList(), + pricingPhaseList: (json['pricingPhaseList'] as List).map((e) => PricingPhaseAndroid.fromJson(e as Map)).toList(), ); } @@ -2064,7 +1938,6 @@ class ProductAndroid extends Product implements ProductCommon { final String currency; final String? debugDescription; final String description; - /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer @@ -2073,26 +1946,20 @@ class ProductAndroid extends Product implements ProductCommon { final String displayPrice; final String id; final String nameAndroid; - /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - final List? - oneTimePurchaseOfferDetailsAndroid; + final List? oneTimePurchaseOfferDetailsAndroid; final IapPlatform platform; final double? price; - /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ final ProductStatusAndroid? productStatusAndroid; - /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - final List? - subscriptionOfferDetailsAndroid; - + final List? subscriptionOfferDetailsAndroid; /// Standardized subscription offers. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer @@ -2105,40 +1972,17 @@ class ProductAndroid extends Product implements ProductCommon { currency: json['currency'] as String, debugDescription: json['debugDescription'] as String?, description: json['description'] as String, - discountOffers: (json['discountOffers'] as List?) == null - ? null - : (json['discountOffers'] as List?)! - .map((e) => DiscountOffer.fromJson(e as Map)) - .toList(), + discountOffers: (json['discountOffers'] as List?) == null ? null : (json['discountOffers'] as List?)!.map((e) => DiscountOffer.fromJson(e as Map)).toList(), displayName: json['displayName'] as String?, displayPrice: json['displayPrice'] as String, id: json['id'] as String, nameAndroid: json['nameAndroid'] as String, - oneTimePurchaseOfferDetailsAndroid: - (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null - ? null - : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)! - .map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson( - e as Map)) - .toList(), + oneTimePurchaseOfferDetailsAndroid: (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null ? null : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)!.map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson(e as Map)).toList(), platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - productStatusAndroid: json['productStatusAndroid'] != null - ? ProductStatusAndroid.fromJson( - json['productStatusAndroid'] as String) - : null, - subscriptionOfferDetailsAndroid: - (json['subscriptionOfferDetailsAndroid'] as List?) == null - ? null - : (json['subscriptionOfferDetailsAndroid'] as List?)! - .map((e) => ProductSubscriptionAndroidOfferDetails.fromJson( - e as Map)) - .toList(), - subscriptionOffers: (json['subscriptionOffers'] as List?) == null - ? null - : (json['subscriptionOffers'] as List?)! - .map((e) => SubscriptionOffer.fromJson(e as Map)) - .toList(), + productStatusAndroid: json['productStatusAndroid'] != null ? ProductStatusAndroid.fromJson(json['productStatusAndroid'] as String) : null, + subscriptionOfferDetailsAndroid: (json['subscriptionOfferDetailsAndroid'] as List?) == null ? null : (json['subscriptionOfferDetailsAndroid'] as List?)!.map((e) => ProductSubscriptionAndroidOfferDetails.fromJson(e as Map)).toList(), + subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(), title: json['title'] as String, type: ProductType.fromJson(json['type'] as String), ); @@ -2151,28 +1995,17 @@ class ProductAndroid extends Product implements ProductCommon { 'currency': currency, 'debugDescription': debugDescription, 'description': description, - 'discountOffers': discountOffers == null - ? null - : discountOffers!.map((e) => e.toJson()).toList(), + 'discountOffers': discountOffers == null ? null : discountOffers!.map((e) => e.toJson()).toList(), 'displayName': displayName, 'displayPrice': displayPrice, 'id': id, 'nameAndroid': nameAndroid, - 'oneTimePurchaseOfferDetailsAndroid': - oneTimePurchaseOfferDetailsAndroid == null - ? null - : oneTimePurchaseOfferDetailsAndroid! - .map((e) => e.toJson()) - .toList(), + 'oneTimePurchaseOfferDetailsAndroid': oneTimePurchaseOfferDetailsAndroid == null ? null : oneTimePurchaseOfferDetailsAndroid!.map((e) => e.toJson()).toList(), 'platform': platform.toJson(), 'price': price, 'productStatusAndroid': productStatusAndroid?.toJson(), - 'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid == null - ? null - : subscriptionOfferDetailsAndroid!.map((e) => e.toJson()).toList(), - 'subscriptionOffers': subscriptionOffers == null - ? null - : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid == null ? null : subscriptionOfferDetailsAndroid!.map((e) => e.toJson()).toList(), + 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), 'title': title, 'type': type.toJson(), }; @@ -2204,72 +2037,46 @@ class ProductAndroidOneTimePurchaseOfferDetail { /// Only available for discounted offers final DiscountDisplayInfoAndroid? discountDisplayInfo; final String formattedPrice; - /// Full (non-discounted) price in micro-units /// Only available for discounted offers final String? fullPriceMicros; - /// Limited quantity information final LimitedQuantityInfoAndroid? limitedQuantityInfo; - /// Offer ID final String? offerId; - /// List of offer tags final List offerTags; - /// Offer token for use in BillingFlowParams when purchasing final String offerToken; - /// Pre-order details for products available for pre-order /// Available in Google Play Billing Library 8.1.0+ final PreorderDetailsAndroid? preorderDetailsAndroid; final String priceAmountMicros; final String priceCurrencyCode; - /// Purchase option ID for this offer (Android) /// Used to identify which purchase option the user selected. /// Available in Google Play Billing Library 7.0+ final String? purchaseOptionId; - /// Rental details for rental offers final RentalDetailsAndroid? rentalDetailsAndroid; - /// Valid time window for the offer final ValidTimeWindowAndroid? validTimeWindow; - factory ProductAndroidOneTimePurchaseOfferDetail.fromJson( - Map json) { + factory ProductAndroidOneTimePurchaseOfferDetail.fromJson(Map json) { return ProductAndroidOneTimePurchaseOfferDetail( - discountDisplayInfo: json['discountDisplayInfo'] != null - ? DiscountDisplayInfoAndroid.fromJson( - json['discountDisplayInfo'] as Map) - : null, + discountDisplayInfo: json['discountDisplayInfo'] != null ? DiscountDisplayInfoAndroid.fromJson(json['discountDisplayInfo'] as Map) : null, formattedPrice: json['formattedPrice'] as String, fullPriceMicros: json['fullPriceMicros'] as String?, - limitedQuantityInfo: json['limitedQuantityInfo'] != null - ? LimitedQuantityInfoAndroid.fromJson( - json['limitedQuantityInfo'] as Map) - : null, + limitedQuantityInfo: json['limitedQuantityInfo'] != null ? LimitedQuantityInfoAndroid.fromJson(json['limitedQuantityInfo'] as Map) : null, offerId: json['offerId'] as String?, - offerTags: - (json['offerTags'] as List).map((e) => e as String).toList(), + offerTags: (json['offerTags'] as List).map((e) => e as String).toList(), offerToken: json['offerToken'] as String, - preorderDetailsAndroid: json['preorderDetailsAndroid'] != null - ? PreorderDetailsAndroid.fromJson( - json['preorderDetailsAndroid'] as Map) - : null, + preorderDetailsAndroid: json['preorderDetailsAndroid'] != null ? PreorderDetailsAndroid.fromJson(json['preorderDetailsAndroid'] as Map) : null, priceAmountMicros: json['priceAmountMicros'] as String, priceCurrencyCode: json['priceCurrencyCode'] as String, purchaseOptionId: json['purchaseOptionId'] as String?, - rentalDetailsAndroid: json['rentalDetailsAndroid'] != null - ? RentalDetailsAndroid.fromJson( - json['rentalDetailsAndroid'] as Map) - : null, - validTimeWindow: json['validTimeWindow'] != null - ? ValidTimeWindowAndroid.fromJson( - json['validTimeWindow'] as Map) - : null, + rentalDetailsAndroid: json['rentalDetailsAndroid'] != null ? RentalDetailsAndroid.fromJson(json['rentalDetailsAndroid'] as Map) : null, + validTimeWindow: json['validTimeWindow'] != null ? ValidTimeWindowAndroid.fromJson(json['validTimeWindow'] as Map) : null, ); } @@ -2324,10 +2131,8 @@ class ProductIOS extends Product implements ProductCommon { final String jsonRepresentationIOS; final IapPlatform platform; final double? price; - /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. final SubscriptionInfoIOS? subscriptionInfoIOS; - /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// Note: iOS does not support one-time product discounts. @@ -2350,15 +2155,8 @@ class ProductIOS extends Product implements ProductCommon { jsonRepresentationIOS: json['jsonRepresentationIOS'] as String, platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - 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(), + 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, type: ProductType.fromJson(json['type'] as String), typeIOS: ProductTypeIOS.fromJson(json['typeIOS'] as String), @@ -2381,9 +2179,7 @@ class ProductIOS extends Product implements ProductCommon { 'platform': platform.toJson(), 'price': price, 'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(), - 'subscriptionOffers': subscriptionOffers == null - ? null - : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), 'title': title, 'type': type.toJson(), 'typeIOS': typeIOS.toJson(), @@ -2391,8 +2187,7 @@ class ProductIOS extends Product implements ProductCommon { } } -class ProductSubscriptionAndroid extends ProductSubscription - implements ProductCommon { +class ProductSubscriptionAndroid extends ProductSubscription implements ProductCommon { const ProductSubscriptionAndroid({ required this.currency, this.debugDescription, @@ -2415,7 +2210,6 @@ class ProductSubscriptionAndroid extends ProductSubscription final String currency; final String? debugDescription; final String description; - /// Standardized discount offers for one-time products. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#discount-offer @@ -2424,26 +2218,20 @@ class ProductSubscriptionAndroid extends ProductSubscription final String displayPrice; final String id; final String nameAndroid; - /// One-time purchase offer details including discounts (Android) /// Returns all eligible offers. Available in Google Play Billing Library 7.0+ /// @deprecated Use discountOffers instead for cross-platform compatibility. - final List? - oneTimePurchaseOfferDetailsAndroid; + final List? oneTimePurchaseOfferDetailsAndroid; final IapPlatform platform; final double? price; - /// Product-level status code indicating fetch result (Android 8.0+) /// OK = product fetched successfully /// NOT_FOUND = SKU doesn't exist /// NO_OFFERS_AVAILABLE = user not eligible for any offers /// Available in Google Play Billing Library 8.0.0+ final ProductStatusAndroid? productStatusAndroid; - /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. - final List - subscriptionOfferDetailsAndroid; - + final List subscriptionOfferDetailsAndroid; /// Standardized subscription offers. /// Cross-platform type with Android-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer @@ -2456,36 +2244,17 @@ class ProductSubscriptionAndroid extends ProductSubscription currency: json['currency'] as String, debugDescription: json['debugDescription'] as String?, description: json['description'] as String, - discountOffers: (json['discountOffers'] as List?) == null - ? null - : (json['discountOffers'] as List?)! - .map((e) => DiscountOffer.fromJson(e as Map)) - .toList(), + discountOffers: (json['discountOffers'] as List?) == null ? null : (json['discountOffers'] as List?)!.map((e) => DiscountOffer.fromJson(e as Map)).toList(), displayName: json['displayName'] as String?, displayPrice: json['displayPrice'] as String, id: json['id'] as String, nameAndroid: json['nameAndroid'] as String, - oneTimePurchaseOfferDetailsAndroid: - (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null - ? null - : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)! - .map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson( - e as Map)) - .toList(), + oneTimePurchaseOfferDetailsAndroid: (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null ? null : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)!.map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson(e as Map)).toList(), platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - productStatusAndroid: json['productStatusAndroid'] != null - ? ProductStatusAndroid.fromJson( - json['productStatusAndroid'] as String) - : null, - subscriptionOfferDetailsAndroid: - (json['subscriptionOfferDetailsAndroid'] as List) - .map((e) => ProductSubscriptionAndroidOfferDetails.fromJson( - e as Map)) - .toList(), - subscriptionOffers: (json['subscriptionOffers'] as List) - .map((e) => SubscriptionOffer.fromJson(e as Map)) - .toList(), + productStatusAndroid: json['productStatusAndroid'] != null ? ProductStatusAndroid.fromJson(json['productStatusAndroid'] as String) : null, + subscriptionOfferDetailsAndroid: (json['subscriptionOfferDetailsAndroid'] as List).map((e) => ProductSubscriptionAndroidOfferDetails.fromJson(e as Map)).toList(), + subscriptionOffers: (json['subscriptionOffers'] as List).map((e) => SubscriptionOffer.fromJson(e as Map)).toList(), title: json['title'] as String, type: ProductType.fromJson(json['type'] as String), ); @@ -2498,24 +2267,16 @@ class ProductSubscriptionAndroid extends ProductSubscription 'currency': currency, 'debugDescription': debugDescription, 'description': description, - 'discountOffers': discountOffers == null - ? null - : discountOffers!.map((e) => e.toJson()).toList(), + 'discountOffers': discountOffers == null ? null : discountOffers!.map((e) => e.toJson()).toList(), 'displayName': displayName, 'displayPrice': displayPrice, 'id': id, 'nameAndroid': nameAndroid, - 'oneTimePurchaseOfferDetailsAndroid': - oneTimePurchaseOfferDetailsAndroid == null - ? null - : oneTimePurchaseOfferDetailsAndroid! - .map((e) => e.toJson()) - .toList(), + 'oneTimePurchaseOfferDetailsAndroid': oneTimePurchaseOfferDetailsAndroid == null ? null : oneTimePurchaseOfferDetailsAndroid!.map((e) => e.toJson()).toList(), 'platform': platform.toJson(), 'price': price, 'productStatusAndroid': productStatusAndroid?.toJson(), - 'subscriptionOfferDetailsAndroid': - subscriptionOfferDetailsAndroid.map((e) => e.toJson()).toList(), + 'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid.map((e) => e.toJson()).toList(), 'subscriptionOffers': subscriptionOffers.map((e) => e.toJson()).toList(), 'title': title, 'type': type.toJson(), @@ -2537,7 +2298,6 @@ class ProductSubscriptionAndroidOfferDetails { }); final String basePlanId; - /// 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+ @@ -2547,20 +2307,14 @@ class ProductSubscriptionAndroidOfferDetails { final String offerToken; final PricingPhasesAndroid pricingPhases; - factory ProductSubscriptionAndroidOfferDetails.fromJson( - Map json) { + factory ProductSubscriptionAndroidOfferDetails.fromJson(Map json) { return ProductSubscriptionAndroidOfferDetails( basePlanId: json['basePlanId'] as String, - installmentPlanDetails: json['installmentPlanDetails'] != null - ? InstallmentPlanDetailsAndroid.fromJson( - json['installmentPlanDetails'] as Map) - : null, + installmentPlanDetails: json['installmentPlanDetails'] != null ? InstallmentPlanDetailsAndroid.fromJson(json['installmentPlanDetails'] as Map) : null, offerId: json['offerId'] as String?, - offerTags: - (json['offerTags'] as List).map((e) => e as String).toList(), + offerTags: (json['offerTags'] as List).map((e) => e as String).toList(), offerToken: json['offerToken'] as String, - pricingPhases: PricingPhasesAndroid.fromJson( - json['pricingPhases'] as Map), + pricingPhases: PricingPhasesAndroid.fromJson(json['pricingPhases'] as Map), ); } @@ -2577,8 +2331,7 @@ class ProductSubscriptionAndroidOfferDetails { } } -class ProductSubscriptionIOS extends ProductSubscription - implements ProductCommon { +class ProductSubscriptionIOS extends ProductSubscription implements ProductCommon { const ProductSubscriptionIOS({ required this.currency, this.debugDescription, @@ -2609,7 +2362,6 @@ class ProductSubscriptionIOS extends ProductSubscription final String currency; final String? debugDescription; final String description; - /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. final List? discountsIOS; final String? displayName; @@ -2625,10 +2377,8 @@ class ProductSubscriptionIOS extends ProductSubscription final String jsonRepresentationIOS; final IapPlatform platform; final double? price; - /// @deprecated Use subscriptionOffers instead for cross-platform compatibility. final SubscriptionInfoIOS? subscriptionInfoIOS; - /// Standardized subscription offers. /// Cross-platform type with iOS-specific fields using suffix. /// @see https://openiap.dev/docs/types#subscription-offer @@ -2644,46 +2394,24 @@ class ProductSubscriptionIOS extends ProductSubscription currency: json['currency'] as String, debugDescription: json['debugDescription'] as String?, description: json['description'] as String, - discountsIOS: (json['discountsIOS'] as List?) == null - ? null - : (json['discountsIOS'] as List?)! - .map((e) => DiscountIOS.fromJson(e as Map)) - .toList(), + discountsIOS: (json['discountsIOS'] as List?) == null ? null : (json['discountsIOS'] as List?)!.map((e) => DiscountIOS.fromJson(e as Map)).toList(), displayName: json['displayName'] as String?, displayNameIOS: json['displayNameIOS'] as String, displayPrice: json['displayPrice'] as String, id: json['id'] as String, - introductoryPriceAsAmountIOS: - json['introductoryPriceAsAmountIOS'] as String?, + introductoryPriceAsAmountIOS: json['introductoryPriceAsAmountIOS'] as String?, introductoryPriceIOS: json['introductoryPriceIOS'] as String?, - introductoryPriceNumberOfPeriodsIOS: - json['introductoryPriceNumberOfPeriodsIOS'] as String?, - introductoryPricePaymentModeIOS: PaymentModeIOS.fromJson( - json['introductoryPricePaymentModeIOS'] as String), - introductoryPriceSubscriptionPeriodIOS: - json['introductoryPriceSubscriptionPeriodIOS'] != null - ? SubscriptionPeriodIOS.fromJson( - json['introductoryPriceSubscriptionPeriodIOS'] as String) - : null, + introductoryPriceNumberOfPeriodsIOS: json['introductoryPriceNumberOfPeriodsIOS'] as String?, + introductoryPricePaymentModeIOS: PaymentModeIOS.fromJson(json['introductoryPricePaymentModeIOS'] as String), + introductoryPriceSubscriptionPeriodIOS: json['introductoryPriceSubscriptionPeriodIOS'] != null ? SubscriptionPeriodIOS.fromJson(json['introductoryPriceSubscriptionPeriodIOS'] as String) : null, isFamilyShareableIOS: json['isFamilyShareableIOS'] as bool, jsonRepresentationIOS: json['jsonRepresentationIOS'] as String, platform: IapPlatform.fromJson(json['platform'] as String), price: (json['price'] as num?)?.toDouble(), - 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(), - subscriptionPeriodNumberIOS: - json['subscriptionPeriodNumberIOS'] as String?, - subscriptionPeriodUnitIOS: json['subscriptionPeriodUnitIOS'] != null - ? SubscriptionPeriodIOS.fromJson( - json['subscriptionPeriodUnitIOS'] as String) - : null, + 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(), + subscriptionPeriodNumberIOS: json['subscriptionPeriodNumberIOS'] as String?, + subscriptionPeriodUnitIOS: json['subscriptionPeriodUnitIOS'] != null ? SubscriptionPeriodIOS.fromJson(json['subscriptionPeriodUnitIOS'] as String) : null, title: json['title'] as String, type: ProductType.fromJson(json['type'] as String), typeIOS: ProductTypeIOS.fromJson(json['typeIOS'] as String), @@ -2697,29 +2425,22 @@ class ProductSubscriptionIOS extends ProductSubscription 'currency': currency, 'debugDescription': debugDescription, 'description': description, - 'discountsIOS': discountsIOS == null - ? null - : discountsIOS!.map((e) => e.toJson()).toList(), + 'discountsIOS': discountsIOS == null ? null : discountsIOS!.map((e) => e.toJson()).toList(), 'displayName': displayName, 'displayNameIOS': displayNameIOS, 'displayPrice': displayPrice, 'id': id, 'introductoryPriceAsAmountIOS': introductoryPriceAsAmountIOS, 'introductoryPriceIOS': introductoryPriceIOS, - 'introductoryPriceNumberOfPeriodsIOS': - introductoryPriceNumberOfPeriodsIOS, - 'introductoryPricePaymentModeIOS': - introductoryPricePaymentModeIOS.toJson(), - 'introductoryPriceSubscriptionPeriodIOS': - introductoryPriceSubscriptionPeriodIOS?.toJson(), + 'introductoryPriceNumberOfPeriodsIOS': introductoryPriceNumberOfPeriodsIOS, + 'introductoryPricePaymentModeIOS': introductoryPricePaymentModeIOS.toJson(), + 'introductoryPriceSubscriptionPeriodIOS': introductoryPriceSubscriptionPeriodIOS?.toJson(), 'isFamilyShareableIOS': isFamilyShareableIOS, 'jsonRepresentationIOS': jsonRepresentationIOS, 'platform': platform.toJson(), 'price': price, 'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(), - 'subscriptionOffers': subscriptionOffers == null - ? null - : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), 'subscriptionPeriodNumberIOS': subscriptionPeriodNumberIOS, 'subscriptionPeriodUnitIOS': subscriptionPeriodUnitIOS?.toJson(), 'title': title, @@ -2764,7 +2485,6 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { final List? ids; final bool? isAcknowledgedAndroid; final bool isAutoRenewing; - /// Whether the subscription is suspended (Android) /// A suspended subscription means the user's payment method failed and they need to fix it. /// Users should be directed to the subscription center to resolve the issue. @@ -2774,7 +2494,6 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { final String? obfuscatedAccountIdAndroid; final String? obfuscatedProfileIdAndroid; final String? packageNameAndroid; - /// 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. @@ -2786,7 +2505,6 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { final String? purchaseToken; final int quantity; final String? signatureAndroid; - /// Store where purchase was made final IapStore store; final double transactionDate; @@ -2800,19 +2518,14 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon { dataAndroid: json['dataAndroid'] as String?, developerPayloadAndroid: json['developerPayloadAndroid'] as String?, id: json['id'] as String, - ids: (json['ids'] as List?) == null - ? null - : (json['ids'] as List?)!.map((e) => e as String).toList(), + ids: (json['ids'] as List?) == null ? null : (json['ids'] as List?)!.map((e) => e as String).toList(), isAcknowledgedAndroid: json['isAcknowledgedAndroid'] as bool?, isAutoRenewing: json['isAutoRenewing'] as bool, isSuspendedAndroid: json['isSuspendedAndroid'] as bool?, obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, packageNameAndroid: json['packageNameAndroid'] as String?, - pendingPurchaseUpdateAndroid: json['pendingPurchaseUpdateAndroid'] != null - ? PendingPurchaseUpdateAndroid.fromJson( - json['pendingPurchaseUpdateAndroid'] as Map) - : null, + pendingPurchaseUpdateAndroid: json['pendingPurchaseUpdateAndroid'] != null ? PendingPurchaseUpdateAndroid.fromJson(json['pendingPurchaseUpdateAndroid'] as Map) : null, platform: IapPlatform.fromJson(json['platform'] as String), productId: json['productId'] as String, purchaseState: PurchaseState.fromJson(json['purchaseState'] as String), @@ -2956,7 +2669,6 @@ class PurchaseIOS extends Purchase implements PurchaseCommon { final RenewalInfoIOS? renewalInfoIOS; final double? revocationDateIOS; final String? revocationReasonIOS; - /// Store where purchase was made final IapStore store; final String? storefrontCountryCodeIOS; @@ -2978,18 +2690,12 @@ class PurchaseIOS extends Purchase implements PurchaseCommon { environmentIOS: json['environmentIOS'] as String?, expirationDateIOS: (json['expirationDateIOS'] as num?)?.toDouble(), id: json['id'] as String, - ids: (json['ids'] as List?) == null - ? null - : (json['ids'] as List?)!.map((e) => e as String).toList(), + ids: (json['ids'] as List?) == null ? null : (json['ids'] as List?)!.map((e) => e as String).toList(), isAutoRenewing: json['isAutoRenewing'] as bool, isUpgradedIOS: json['isUpgradedIOS'] as bool?, - offerIOS: json['offerIOS'] != null - ? PurchaseOfferIOS.fromJson(json['offerIOS'] as Map) - : null, - originalTransactionDateIOS: - (json['originalTransactionDateIOS'] as num?)?.toDouble(), - originalTransactionIdentifierIOS: - json['originalTransactionIdentifierIOS'] as String?, + offerIOS: json['offerIOS'] != null ? PurchaseOfferIOS.fromJson(json['offerIOS'] as Map) : null, + originalTransactionDateIOS: (json['originalTransactionDateIOS'] as num?)?.toDouble(), + originalTransactionIdentifierIOS: json['originalTransactionIdentifierIOS'] as String?, ownershipTypeIOS: json['ownershipTypeIOS'] as String?, platform: IapPlatform.fromJson(json['platform'] as String), productId: json['productId'] as String, @@ -2998,12 +2704,8 @@ class PurchaseIOS extends Purchase implements PurchaseCommon { quantity: json['quantity'] as int, quantityIOS: json['quantityIOS'] as int?, reasonIOS: json['reasonIOS'] as String?, - reasonStringRepresentationIOS: - json['reasonStringRepresentationIOS'] as String?, - renewalInfoIOS: json['renewalInfoIOS'] != null - ? RenewalInfoIOS.fromJson( - json['renewalInfoIOS'] as Map) - : null, + reasonStringRepresentationIOS: json['reasonStringRepresentationIOS'] as String?, + renewalInfoIOS: json['renewalInfoIOS'] != null ? RenewalInfoIOS.fromJson(json['renewalInfoIOS'] as Map) : null, revocationDateIOS: (json['revocationDateIOS'] as num?)?.toDouble(), revocationReasonIOS: json['revocationReasonIOS'] as String?, store: IapStore.fromJson(json['store'] as String), @@ -3132,35 +2834,27 @@ class RenewalInfoIOS { }); final String? autoRenewPreference; - /// 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; - /// Grace period expiration date (milliseconds since epoch) /// When set, subscription is in grace period (billing issue but still has access) final double? gracePeriodExpirationDate; - /// True if subscription failed to renew due to billing issue and is retrying /// Note: Not directly available in RenewalInfo, available in Status final bool? isInBillingRetry; final String? jsonRepresentation; - /// Product ID that will be used on next renewal (when user upgrades/downgrades) /// If set and different from current productId, subscription will change on expiration final String? pendingUpgradeProductId; - /// User's response to subscription price increase /// Possible values: "AGREED", "PENDING", null (no price increase) final String? priceIncreaseStatus; - /// Expected renewal date (milliseconds since epoch) /// For active subscriptions, when the next renewal/charge will occur final double? renewalDate; - /// Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) final String? renewalOfferId; - /// Type of offer applied to next renewal /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. final String? renewalOfferType; @@ -3170,8 +2864,7 @@ class RenewalInfoIOS { return RenewalInfoIOS( autoRenewPreference: json['autoRenewPreference'] as String?, expirationReason: json['expirationReason'] as String?, - gracePeriodExpirationDate: - (json['gracePeriodExpirationDate'] as num?)?.toDouble(), + gracePeriodExpirationDate: (json['gracePeriodExpirationDate'] as num?)?.toDouble(), isInBillingRetry: json['isInBillingRetry'] as bool?, jsonRepresentation: json['jsonRepresentation'] as String?, pendingUpgradeProductId: json['pendingUpgradeProductId'] as String?, @@ -3212,7 +2905,6 @@ class RentalDetailsAndroid { /// Rental expiration period in ISO 8601 format /// Time after rental period ends when user can still extend final String? rentalExpirationPeriod; - /// Rental period in ISO 8601 format (e.g., P7D for 7 days) final String rentalPeriod; @@ -3255,13 +2947,11 @@ class RequestVerifyPurchaseWithIapkitResult { /// Whether the purchase is valid (not falsified). final bool isValid; - /// The current state of the purchase. final IapkitPurchaseState state; final IapStore store; - factory RequestVerifyPurchaseWithIapkitResult.fromJson( - Map json) { + factory RequestVerifyPurchaseWithIapkitResult.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitResult( isValid: json['isValid'] as bool, state: IapkitPurchaseState.fromJson(json['state'] as String), @@ -3294,19 +2984,10 @@ class SubscriptionInfoIOS { factory SubscriptionInfoIOS.fromJson(Map json) { return SubscriptionInfoIOS( - introductoryOffer: json['introductoryOffer'] != null - ? SubscriptionOfferIOS.fromJson( - json['introductoryOffer'] as Map) - : null, - promotionalOffers: (json['promotionalOffers'] as List?) == null - ? null - : (json['promotionalOffers'] as List?)! - .map((e) => - SubscriptionOfferIOS.fromJson(e as Map)) - .toList(), + introductoryOffer: json['introductoryOffer'] != null ? SubscriptionOfferIOS.fromJson(json['introductoryOffer'] as Map) : null, + 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), + subscriptionPeriod: SubscriptionPeriodValueIOS.fromJson(json['subscriptionPeriod'] as Map), ); } @@ -3314,9 +2995,7 @@ class SubscriptionInfoIOS { return { '__typename': 'SubscriptionInfoIOS', 'introductoryOffer': introductoryOffer?.toJson(), - 'promotionalOffers': promotionalOffers == null - ? null - : promotionalOffers!.map((e) => e.toJson()).toList(), + 'promotionalOffers': promotionalOffers == null ? null : promotionalOffers!.map((e) => e.toJson()).toList(), 'subscriptionGroupId': subscriptionGroupId, 'subscriptionPeriod': subscriptionPeriod.toJson(), }; @@ -3325,11 +3004,11 @@ class SubscriptionInfoIOS { /// Standardized subscription discount/promotional offer. /// Provides a unified interface for subscription offers across iOS and Android. -/// +/// /// Both platforms support subscription offers with different implementations: /// - iOS: Introductory offers, promotional offers with server-side signatures /// - Android: Offer tokens with pricing phases -/// +/// /// @see https://openiap.dev/docs/types/ios#discount-offer /// @see https://openiap.dev/docs/types/android#subscription-offer class SubscriptionOffer { @@ -3358,68 +3037,50 @@ class SubscriptionOffer { /// [Android] Base plan identifier. /// Identifies which base plan this offer belongs to. final String? basePlanIdAndroid; - /// Currency code (ISO 4217, e.g., "USD") final String? currency; - /// Formatted display price string (e.g., "$9.99/month") final String displayPrice; - /// Unique identifier for the offer. /// - iOS: Discount identifier from App Store Connect /// - Android: offerId from ProductSubscriptionAndroidOfferDetails final String id; - /// [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+ final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid; - /// [iOS] Key identifier for signature validation. /// Used with server-side signature generation for promotional offers. final String? keyIdentifierIOS; - /// [iOS] Localized price string. final String? localizedPriceIOS; - /// [iOS] Cryptographic nonce (UUID) for signature validation. /// Must be generated server-side for each purchase attempt. final String? nonceIOS; - /// [iOS] Number of billing periods for this discount. final int? numberOfPeriodsIOS; - /// [Android] List of tags associated with this offer. final List? offerTagsAndroid; - /// [Android] Offer token required for purchase. /// Must be passed to requestPurchase() when purchasing with this offer. final String? offerTokenAndroid; - /// Payment mode during the offer period final PaymentMode? paymentMode; - /// Subscription period for this offer final SubscriptionPeriod? period; - /// Number of periods the offer applies final int? periodCount; - /// Numeric price value final double price; - /// [Android] Pricing phases for this subscription offer. /// Contains detailed pricing information for each phase (trial, intro, regular). final PricingPhasesAndroid? pricingPhasesAndroid; - /// [iOS] Server-generated signature for promotional offer validation. /// Required when applying promotional offers on iOS. final String? signatureIOS; - /// [iOS] Timestamp when the signature was generated. /// Used for signature validation. final double? timestampIOS; - /// Type of subscription offer (Introductory or Promotional) final DiscountOfferType type; @@ -3429,33 +3090,18 @@ class SubscriptionOffer { currency: json['currency'] as String?, displayPrice: json['displayPrice'] as String, id: json['id'] as String, - installmentPlanDetailsAndroid: - json['installmentPlanDetailsAndroid'] != null - ? InstallmentPlanDetailsAndroid.fromJson( - json['installmentPlanDetailsAndroid'] as Map) - : null, + installmentPlanDetailsAndroid: json['installmentPlanDetailsAndroid'] != null ? InstallmentPlanDetailsAndroid.fromJson(json['installmentPlanDetailsAndroid'] as Map) : null, keyIdentifierIOS: json['keyIdentifierIOS'] as String?, localizedPriceIOS: json['localizedPriceIOS'] as String?, nonceIOS: json['nonceIOS'] as String?, numberOfPeriodsIOS: json['numberOfPeriodsIOS'] as int?, - offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null - ? null - : (json['offerTagsAndroid'] as List?)! - .map((e) => e as String) - .toList(), + offerTagsAndroid: (json['offerTagsAndroid'] as List?) == null ? null : (json['offerTagsAndroid'] as List?)!.map((e) => e as String).toList(), offerTokenAndroid: json['offerTokenAndroid'] as String?, - paymentMode: json['paymentMode'] != null - ? PaymentMode.fromJson(json['paymentMode'] as String) - : null, - period: json['period'] != null - ? SubscriptionPeriod.fromJson(json['period'] as Map) - : null, + paymentMode: json['paymentMode'] != null ? PaymentMode.fromJson(json['paymentMode'] as String) : null, + period: json['period'] != null ? SubscriptionPeriod.fromJson(json['period'] as Map) : null, periodCount: json['periodCount'] as int?, price: (json['price'] as num).toDouble(), - pricingPhasesAndroid: json['pricingPhasesAndroid'] != null - ? PricingPhasesAndroid.fromJson( - json['pricingPhasesAndroid'] as Map) - : null, + pricingPhasesAndroid: json['pricingPhasesAndroid'] != null ? PricingPhasesAndroid.fromJson(json['pricingPhasesAndroid'] as Map) : null, signatureIOS: json['signatureIOS'] as String?, timestampIOS: (json['timestampIOS'] as num?)?.toDouble(), type: DiscountOfferType.fromJson(json['type'] as String), @@ -3515,8 +3161,7 @@ class SubscriptionOfferIOS { displayPrice: json['displayPrice'] as String, id: json['id'] as String, paymentMode: PaymentModeIOS.fromJson(json['paymentMode'] as String), - period: SubscriptionPeriodValueIOS.fromJson( - json['period'] as Map), + period: SubscriptionPeriodValueIOS.fromJson(json['period'] as Map), periodCount: json['periodCount'] as int, price: (json['price'] as num).toDouble(), type: SubscriptionOfferTypeIOS.fromJson(json['type'] as String), @@ -3546,7 +3191,6 @@ class SubscriptionPeriod { /// The period unit (day, week, month, year) final SubscriptionPeriodUnit unit; - /// The number of units (e.g., 1 for monthly, 3 for quarterly) final int value; @@ -3602,9 +3246,7 @@ class SubscriptionStatusIOS { factory SubscriptionStatusIOS.fromJson(Map json) { return SubscriptionStatusIOS( - renewalInfo: json['renewalInfo'] != null - ? RenewalInfoIOS.fromJson(json['renewalInfo'] as Map) - : null, + renewalInfo: json['renewalInfo'] != null ? RenewalInfoIOS.fromJson(json['renewalInfo'] as Map) : null, state: json['state'] as String, ); } @@ -3628,15 +3270,13 @@ class UserChoiceBillingDetails { /// Token that must be reported to Google Play within 24 hours final String externalTransactionToken; - /// List of product IDs selected by the user final List products; factory UserChoiceBillingDetails.fromJson(Map json) { return UserChoiceBillingDetails( externalTransactionToken: json['externalTransactionToken'] as String, - products: - (json['products'] as List).map((e) => e as String).toList(), + products: (json['products'] as List).map((e) => e as String).toList(), ); } @@ -3659,7 +3299,6 @@ class ValidTimeWindowAndroid { /// End time in milliseconds since epoch final String endTimeMillis; - /// Start time in milliseconds since epoch final String startTimeMillis; @@ -3779,7 +3418,6 @@ class VerifyPurchaseResultHorizon extends VerifyPurchaseResult { /// Unix timestamp (seconds) when the entitlement was granted. final double? grantTime; - /// Whether the entitlement verification succeeded. final bool success; @@ -3810,13 +3448,10 @@ class VerifyPurchaseResultIOS extends VerifyPurchaseResult { /// Whether the receipt is valid final bool isValid; - /// JWS representation final String jwsRepresentation; - /// Latest transaction if available final Purchase? latestTransaction; - /// Receipt data string final String receiptData; @@ -3824,9 +3459,7 @@ class VerifyPurchaseResultIOS extends VerifyPurchaseResult { return VerifyPurchaseResultIOS( isValid: json['isValid'] as bool, jwsRepresentation: json['jwsRepresentation'] as String, - latestTransaction: json['latestTransaction'] != null - ? Purchase.fromJson(json['latestTransaction'] as Map) - : null, + latestTransaction: json['latestTransaction'] != null ? Purchase.fromJson(json['latestTransaction'] as Map) : null, receiptData: json['receiptData'] as String, ); } @@ -3877,25 +3510,15 @@ class VerifyPurchaseWithProviderResult { /// Error details if verification failed final List? errors; - /// IAPKit verification result final RequestVerifyPurchaseWithIapkitResult? iapkit; final PurchaseVerificationProvider provider; factory VerifyPurchaseWithProviderResult.fromJson(Map json) { return VerifyPurchaseWithProviderResult( - errors: (json['errors'] as List?) == null - ? null - : (json['errors'] as List?)! - .map((e) => VerifyPurchaseWithProviderError.fromJson( - e as Map)) - .toList(), - iapkit: json['iapkit'] != null - ? RequestVerifyPurchaseWithIapkitResult.fromJson( - json['iapkit'] as Map) - : null, - provider: - PurchaseVerificationProvider.fromJson(json['provider'] as String), + errors: (json['errors'] as List?) == null ? null : (json['errors'] as List?)!.map((e) => VerifyPurchaseWithProviderError.fromJson(e as Map)).toList(), + iapkit: json['iapkit'] != null ? RequestVerifyPurchaseWithIapkitResult.fromJson(json['iapkit'] as Map) : null, + provider: PurchaseVerificationProvider.fromJson(json['provider'] as String), ); } @@ -3921,7 +3544,6 @@ class AndroidSubscriptionOfferInput { /// Offer token final String offerToken; - /// Product SKU final String sku; @@ -3948,7 +3570,6 @@ class DeepLinkOptions { /// Android package name to target (required on Android) final String? packageNameAndroid; - /// Android SKU to open (required on Android) final String? skuAndroid; @@ -3979,20 +3600,15 @@ class DeveloperBillingOptionParamsAndroid { /// The billing program (should be EXTERNAL_PAYMENTS for external payments flow) final BillingProgramAndroid billingProgram; - /// The launch mode for the external payment link final DeveloperBillingLaunchModeAndroid launchMode; - /// The URI where the external payment will be processed final String linkUri; - factory DeveloperBillingOptionParamsAndroid.fromJson( - Map json) { + factory DeveloperBillingOptionParamsAndroid.fromJson(Map json) { return DeveloperBillingOptionParamsAndroid( - billingProgram: - BillingProgramAndroid.fromJson(json['billingProgram'] as String), - launchMode: DeveloperBillingLaunchModeAndroid.fromJson( - json['launchMode'] as String), + billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), + launchMode: DeveloperBillingLaunchModeAndroid.fromJson(json['launchMode'] as String), linkUri: json['linkUri'] as String, ); } @@ -4017,16 +3633,12 @@ class DiscountOfferInputIOS { /// Discount identifier final String identifier; - /// Key identifier for validation final String keyIdentifier; - /// Cryptographic nonce final String nonce; - /// Signature for validation final String signature; - /// Timestamp of discount offer final double timestamp; @@ -4063,7 +3675,6 @@ class InitConnectionConfig { /// @deprecated Use enableBillingProgramAndroid instead. /// Use USER_CHOICE_BILLING for user choice billing, EXTERNAL_OFFER for alternative only. final AlternativeBillingModeAndroid? alternativeBillingModeAndroid; - /// Enable a specific billing program for Android (7.0+) /// When set, enables the specified billing program for external transactions. /// - USER_CHOICE_BILLING: User can select between Google Play or alternative (7.0+) @@ -4074,15 +3685,8 @@ class InitConnectionConfig { factory InitConnectionConfig.fromJson(Map json) { return InitConnectionConfig( - alternativeBillingModeAndroid: - json['alternativeBillingModeAndroid'] != null - ? AlternativeBillingModeAndroid.fromJson( - json['alternativeBillingModeAndroid'] as String) - : null, - enableBillingProgramAndroid: json['enableBillingProgramAndroid'] != null - ? BillingProgramAndroid.fromJson( - json['enableBillingProgramAndroid'] as String) - : null, + alternativeBillingModeAndroid: json['alternativeBillingModeAndroid'] != null ? AlternativeBillingModeAndroid.fromJson(json['alternativeBillingModeAndroid'] as String) : null, + enableBillingProgramAndroid: json['enableBillingProgramAndroid'] != null ? BillingProgramAndroid.fromJson(json['enableBillingProgramAndroid'] as String) : null, ); } @@ -4107,22 +3711,17 @@ class LaunchExternalLinkParamsAndroid { /// The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER) final BillingProgramAndroid billingProgram; - /// The external link launch mode final ExternalLinkLaunchModeAndroid launchMode; - /// The type of the external link final ExternalLinkTypeAndroid linkType; - /// The URI where the content will be accessed from final String linkUri; factory LaunchExternalLinkParamsAndroid.fromJson(Map json) { return LaunchExternalLinkParamsAndroid( - billingProgram: - BillingProgramAndroid.fromJson(json['billingProgram'] as String), - launchMode: - ExternalLinkLaunchModeAndroid.fromJson(json['launchMode'] as String), + billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), + launchMode: ExternalLinkLaunchModeAndroid.fromJson(json['launchMode'] as String), linkType: ExternalLinkTypeAndroid.fromJson(json['linkType'] as String), linkUri: json['linkUri'] as String, ); @@ -4150,9 +3749,7 @@ class ProductRequest { factory ProductRequest.fromJson(Map json) { return ProductRequest( skus: (json['skus'] as List).map((e) => e as String).toList(), - type: json['type'] != null - ? ProductQueryType.fromJson(json['type'] as String) - : null, + type: json['type'] != null ? ProductQueryType.fromJson(json['type'] as String) : null, ); } @@ -4178,7 +3775,6 @@ class PromotionalOfferJWSInputIOS { /// The JWS should contain the promotional offer signature data. /// Format: header.payload.signature (base64url encoded) final String jws; - /// The promotional offer identifier from App Store Connect final String offerId; @@ -4208,20 +3804,17 @@ class PurchaseOptions { /// Also emit results through the iOS event listeners final bool? alsoPublishToEventListenerIOS; - /// Include suspended subscriptions in the result (Android 8.1+). /// Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. /// Users should be directed to the subscription center to resolve payment issues. /// Default: false (only active subscriptions are returned) final bool? includeSuspendedAndroid; - /// Limit to currently active items on iOS final bool? onlyIncludeActiveItemsIOS; factory PurchaseOptions.fromJson(Map json) { return PurchaseOptions( - alsoPublishToEventListenerIOS: - json['alsoPublishToEventListenerIOS'] as bool?, + alsoPublishToEventListenerIOS: json['alsoPublishToEventListenerIOS'] as bool?, includeSuspendedAndroid: json['includeSuspendedAndroid'] as bool?, onlyIncludeActiveItemsIOS: json['onlyIncludeActiveItemsIOS'] as bool?, ); @@ -4250,31 +3843,23 @@ class RequestPurchaseAndroidProps { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. final DeveloperBillingOptionParamsAndroid? developerBillingOption; - /// Personalized offer flag. /// When true, indicates the price was customized for this user. final bool? isOfferPersonalized; - /// Obfuscated account ID final String? obfuscatedAccountId; - /// Obfuscated profile ID final String? obfuscatedProfileId; - /// Offer token for one-time purchase discounts (7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. final String? offerToken; - /// List of product SKUs final List skus; factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOption: json['developerBillingOption'] != null - ? DeveloperBillingOptionParamsAndroid.fromJson( - json['developerBillingOption'] as Map) - : null, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, @@ -4310,19 +3895,14 @@ class RequestPurchaseIosProps { /// campaign tokens, affiliate IDs, or other attribution data. /// The data is formatted as JSON: {"signatureInfo": {"token": ""}} final String? advancedCommerceData; - /// Auto-finish transaction (dangerous) final bool? andDangerouslyFinishTransactionAutomatically; - /// App account token for user tracking final String? appAccountToken; - /// Purchase quantity final int? quantity; - /// Product SKU final String sku; - /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). /// iOS only supports promotional offers for auto-renewable subscriptions. final DiscountOfferInputIOS? withOffer; @@ -4330,23 +3910,18 @@ class RequestPurchaseIosProps { factory RequestPurchaseIosProps.fromJson(Map json) { return RequestPurchaseIosProps( advancedCommerceData: json['advancedCommerceData'] as String?, - andDangerouslyFinishTransactionAutomatically: - json['andDangerouslyFinishTransactionAutomatically'] as bool?, + andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?, appAccountToken: json['appAccountToken'] as String?, quantity: json['quantity'] as int?, sku: json['sku'] as String, - withOffer: json['withOffer'] != null - ? DiscountOfferInputIOS.fromJson( - json['withOffer'] as Map) - : null, + withOffer: json['withOffer'] != null ? DiscountOfferInputIOS.fromJson(json['withOffer'] as Map) : null, ); } Map toJson() { return { 'advancedCommerceData': advancedCommerceData, - 'andDangerouslyFinishTransactionAutomatically': - andDangerouslyFinishTransactionAutomatically, + 'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically, 'appAccountToken': appAccountToken, 'quantity': quantity, 'sku': sku, @@ -4358,19 +3933,17 @@ class RequestPurchaseIosProps { sealed class RequestPurchaseProps { const RequestPurchaseProps._(); - const factory RequestPurchaseProps.inApp( - ({ - RequestPurchaseIosProps? apple, - RequestPurchaseAndroidProps? google, - bool? useAlternativeBilling, - }) props) = _InAppPurchase; + const factory RequestPurchaseProps.inApp(({ + RequestPurchaseIosProps? apple, + RequestPurchaseAndroidProps? google, + bool? useAlternativeBilling, + }) props) = _InAppPurchase; - const factory RequestPurchaseProps.subs( - ({ - RequestSubscriptionIosProps? apple, - RequestSubscriptionAndroidProps? google, - bool? useAlternativeBilling, - }) props) = _SubsPurchase; + const factory RequestPurchaseProps.subs(({ + RequestSubscriptionIosProps? apple, + RequestSubscriptionAndroidProps? google, + bool? useAlternativeBilling, + }) props) = _SubsPurchase; Map toJson(); } @@ -4391,8 +3964,7 @@ class _InAppPurchase extends RequestPurchaseProps { if (props.google != null) 'android': props.google!.toJson(), }, 'type': ProductQueryType.InApp.toJson(), - if (props.useAlternativeBilling != null) - 'useAlternativeBilling': props.useAlternativeBilling, + if (props.useAlternativeBilling != null) 'useAlternativeBilling': props.useAlternativeBilling, }; } } @@ -4413,14 +3985,13 @@ class _SubsPurchase extends RequestPurchaseProps { if (props.google != null) 'android': props.google!.toJson(), }, 'type': ProductQueryType.Subs.toJson(), - if (props.useAlternativeBilling != null) - 'useAlternativeBilling': props.useAlternativeBilling, + if (props.useAlternativeBilling != null) 'useAlternativeBilling': props.useAlternativeBilling, }; } } /// Platform-specific purchase request parameters. -/// +/// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store /// - google: Targets Play Store by default, or Horizon when built with horizon flavor @@ -4435,34 +4006,19 @@ class RequestPurchasePropsByPlatforms { /// @deprecated Use google instead final RequestPurchaseAndroidProps? android; - /// Apple-specific purchase parameters final RequestPurchaseIosProps? apple; - /// Google-specific purchase parameters final RequestPurchaseAndroidProps? google; - /// @deprecated Use apple instead final RequestPurchaseIosProps? ios; factory RequestPurchasePropsByPlatforms.fromJson(Map json) { return RequestPurchasePropsByPlatforms( - android: json['android'] != null - ? RequestPurchaseAndroidProps.fromJson( - json['android'] as Map) - : null, - apple: json['apple'] != null - ? RequestPurchaseIosProps.fromJson( - json['apple'] as Map) - : null, - google: json['google'] != null - ? RequestPurchaseAndroidProps.fromJson( - json['google'] as Map) - : null, - ios: json['ios'] != null - ? RequestPurchaseIosProps.fromJson( - json['ios'] as Map) - : null, + android: json['android'] != null ? RequestPurchaseAndroidProps.fromJson(json['android'] as Map) : null, + apple: json['apple'] != null ? RequestPurchaseIosProps.fromJson(json['apple'] as Map) : null, + google: json['google'] != null ? RequestPurchaseAndroidProps.fromJson(json['google'] as Map) : null, + ios: json['ios'] != null ? RequestPurchaseIosProps.fromJson(json['ios'] as Map) : null, ); } @@ -4493,59 +4049,37 @@ class RequestSubscriptionAndroidProps { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. final DeveloperBillingOptionParamsAndroid? developerBillingOption; - /// Personalized offer flag. /// When true, indicates the price was customized for this user. final bool? isOfferPersonalized; - /// Obfuscated account ID final String? obfuscatedAccountId; - /// Obfuscated profile ID final String? obfuscatedProfileId; - /// Purchase token for upgrades/downgrades final String? purchaseToken; - /// Replacement mode for subscription changes /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) final int? replacementMode; - /// List of subscription SKUs final List skus; - /// Subscription offers final List? subscriptionOffers; - /// Product-level replacement parameters (8.1.0+) /// Use this instead of replacementMode for item-level replacement - final SubscriptionProductReplacementParamsAndroid? - subscriptionProductReplacementParams; + final SubscriptionProductReplacementParamsAndroid? subscriptionProductReplacementParams; factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOption: json['developerBillingOption'] != null - ? DeveloperBillingOptionParamsAndroid.fromJson( - json['developerBillingOption'] as Map) - : null, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, purchaseToken: json['purchaseToken'] as String?, replacementMode: json['replacementMode'] as int?, skus: (json['skus'] as List).map((e) => e as String).toList(), - subscriptionOffers: (json['subscriptionOffers'] as List?) == null - ? null - : (json['subscriptionOffers'] as List?)! - .map((e) => AndroidSubscriptionOfferInput.fromJson( - e as Map)) - .toList(), - subscriptionProductReplacementParams: - json['subscriptionProductReplacementParams'] != null - ? SubscriptionProductReplacementParamsAndroid.fromJson( - json['subscriptionProductReplacementParams'] - as Map) - : null, + subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => AndroidSubscriptionOfferInput.fromJson(e as Map)).toList(), + subscriptionProductReplacementParams: json['subscriptionProductReplacementParams'] != null ? SubscriptionProductReplacementParamsAndroid.fromJson(json['subscriptionProductReplacementParams'] as Map) : null, ); } @@ -4558,11 +4092,8 @@ class RequestSubscriptionAndroidProps { 'purchaseToken': purchaseToken, 'replacementMode': replacementMode, 'skus': skus, - 'subscriptionOffers': subscriptionOffers == null - ? null - : subscriptionOffers!.map((e) => e.toJson()).toList(), - 'subscriptionProductReplacementParams': - subscriptionProductReplacementParams?.toJson(), + 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), + 'subscriptionProductReplacementParams': subscriptionProductReplacementParams?.toJson(), }; } } @@ -4587,26 +4118,22 @@ class RequestSubscriptionIosProps { final String? advancedCommerceData; final bool? andDangerouslyFinishTransactionAutomatically; 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; final int? quantity; final String sku; - /// Win-back offer to apply (iOS 18+) /// Used to re-engage churned subscribers with a discount or free trial. /// 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; @@ -4614,33 +4141,21 @@ class RequestSubscriptionIosProps { factory RequestSubscriptionIosProps.fromJson(Map json) { return RequestSubscriptionIosProps( advancedCommerceData: json['advancedCommerceData'] as String?, - andDangerouslyFinishTransactionAutomatically: - json['andDangerouslyFinishTransactionAutomatically'] as bool?, + 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, + 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, + winBackOffer: json['winBackOffer'] != null ? WinBackOfferInputIOS.fromJson(json['winBackOffer'] as Map) : null, + withOffer: json['withOffer'] != null ? DiscountOfferInputIOS.fromJson(json['withOffer'] as Map) : null, ); } Map toJson() { return { 'advancedCommerceData': advancedCommerceData, - 'andDangerouslyFinishTransactionAutomatically': - andDangerouslyFinishTransactionAutomatically, + 'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically, 'appAccountToken': appAccountToken, 'introductoryOfferEligibility': introductoryOfferEligibility, 'promotionalOfferJWS': promotionalOfferJWS?.toJson(), @@ -4653,7 +4168,7 @@ class RequestSubscriptionIosProps { } /// Platform-specific subscription request parameters. -/// +/// /// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. /// - apple: Always targets App Store /// - google: Targets Play Store by default, or Horizon when built with horizon flavor @@ -4668,35 +4183,19 @@ class RequestSubscriptionPropsByPlatforms { /// @deprecated Use google instead final RequestSubscriptionAndroidProps? android; - /// Apple-specific subscription parameters final RequestSubscriptionIosProps? apple; - /// Google-specific subscription parameters final RequestSubscriptionAndroidProps? google; - /// @deprecated Use apple instead final RequestSubscriptionIosProps? ios; - factory RequestSubscriptionPropsByPlatforms.fromJson( - Map json) { + factory RequestSubscriptionPropsByPlatforms.fromJson(Map json) { return RequestSubscriptionPropsByPlatforms( - android: json['android'] != null - ? RequestSubscriptionAndroidProps.fromJson( - json['android'] as Map) - : null, - apple: json['apple'] != null - ? RequestSubscriptionIosProps.fromJson( - json['apple'] as Map) - : null, - google: json['google'] != null - ? RequestSubscriptionAndroidProps.fromJson( - json['google'] as Map) - : null, - ios: json['ios'] != null - ? RequestSubscriptionIosProps.fromJson( - json['ios'] as Map) - : null, + android: json['android'] != null ? RequestSubscriptionAndroidProps.fromJson(json['android'] as Map) : null, + apple: json['apple'] != null ? RequestSubscriptionIosProps.fromJson(json['apple'] as Map) : null, + google: json['google'] != null ? RequestSubscriptionAndroidProps.fromJson(json['google'] as Map) : null, + ios: json['ios'] != null ? RequestSubscriptionIosProps.fromJson(json['ios'] as Map) : null, ); } @@ -4718,8 +4217,7 @@ class RequestVerifyPurchaseWithIapkitAppleProps { /// The JWS token returned with the purchase response. final String jws; - factory RequestVerifyPurchaseWithIapkitAppleProps.fromJson( - Map json) { + factory RequestVerifyPurchaseWithIapkitAppleProps.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitAppleProps( jws: json['jws'] as String, ); @@ -4740,8 +4238,7 @@ class RequestVerifyPurchaseWithIapkitGoogleProps { /// The token provided to the user's device when the product or subscription was purchased. final String purchaseToken; - factory RequestVerifyPurchaseWithIapkitGoogleProps.fromJson( - Map json) { + factory RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitGoogleProps( purchaseToken: json['purchaseToken'] as String, ); @@ -4755,7 +4252,7 @@ class RequestVerifyPurchaseWithIapkitGoogleProps { } /// Platform-specific verification parameters for IAPKit. -/// +/// /// - apple: Verifies via App Store (JWS token) /// - google: Verifies via Play Store (purchase token) class RequestVerifyPurchaseWithIapkitProps { @@ -4767,25 +4264,16 @@ class RequestVerifyPurchaseWithIapkitProps { /// API key used for the Authorization header (Bearer {apiKey}). final String? apiKey; - /// Apple App Store verification parameters. final RequestVerifyPurchaseWithIapkitAppleProps? apple; - /// Google Play Store verification parameters. final RequestVerifyPurchaseWithIapkitGoogleProps? google; - factory RequestVerifyPurchaseWithIapkitProps.fromJson( - Map json) { + factory RequestVerifyPurchaseWithIapkitProps.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitProps( apiKey: json['apiKey'] as String?, - apple: json['apple'] != null - ? RequestVerifyPurchaseWithIapkitAppleProps.fromJson( - json['apple'] as Map) - : null, - google: json['google'] != null - ? RequestVerifyPurchaseWithIapkitGoogleProps.fromJson( - json['google'] as Map) - : null, + apple: json['apple'] != null ? RequestVerifyPurchaseWithIapkitAppleProps.fromJson(json['apple'] as Map) : null, + google: json['google'] != null ? RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(json['google'] as Map) : null, ); } @@ -4809,16 +4297,13 @@ class SubscriptionProductReplacementParamsAndroid { /// The old product ID that needs to be replaced final String oldProductId; - /// The replacement mode for this product change final SubscriptionReplacementModeAndroid replacementMode; - factory SubscriptionProductReplacementParamsAndroid.fromJson( - Map json) { + factory SubscriptionProductReplacementParamsAndroid.fromJson(Map json) { return SubscriptionProductReplacementParamsAndroid( oldProductId: json['oldProductId'] as String, - replacementMode: SubscriptionReplacementModeAndroid.fromJson( - json['replacementMode'] as String), + replacementMode: SubscriptionReplacementModeAndroid.fromJson(json['replacementMode'] as String), ); } @@ -4855,7 +4340,7 @@ class VerifyPurchaseAppleOptions { /// Google Play Store verification parameters. /// Used for server-side receipt validation via Google Play Developer API. -/// +/// /// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. class VerifyPurchaseGoogleOptions { const VerifyPurchaseGoogleOptions({ @@ -4869,17 +4354,13 @@ class VerifyPurchaseGoogleOptions { /// Google OAuth2 access token for API authentication. /// ⚠️ Sensitive: Do not log this value. final String accessToken; - /// Whether this is a subscription purchase (affects API endpoint used) final bool? isSub; - /// Android package name (e.g., com.example.app) final String packageName; - /// Purchase token from the purchase response. /// ⚠️ Sensitive: Do not log this value. final String purchaseToken; - /// Product SKU to validate final String sku; @@ -4907,7 +4388,7 @@ class VerifyPurchaseGoogleOptions { /// Meta Horizon (Quest) verification parameters. /// Used for server-side entitlement verification via Meta's S2S API. /// POST https://graph.oculus.com/$APP_ID/verify_entitlement -/// +/// /// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. class VerifyPurchaseHorizonOptions { const VerifyPurchaseHorizonOptions({ @@ -4919,10 +4400,8 @@ class VerifyPurchaseHorizonOptions { /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). /// ⚠️ Sensitive: Do not log this value. final String accessToken; - /// The SKU for the add-on item, defined in Meta Developer Dashboard final String sku; - /// The user ID of the user whose purchase you want to verify final String userId; @@ -4944,7 +4423,7 @@ class VerifyPurchaseHorizonOptions { } /// Platform-specific purchase verification parameters. -/// +/// /// - apple: Verifies via App Store Server API /// - google: Verifies via Google Play Developer API /// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) @@ -4957,27 +4436,16 @@ class VerifyPurchaseProps { /// Apple App Store verification parameters. final VerifyPurchaseAppleOptions? apple; - /// Google Play Store verification parameters. final VerifyPurchaseGoogleOptions? google; - /// Meta Horizon (Quest) verification parameters. final VerifyPurchaseHorizonOptions? horizon; factory VerifyPurchaseProps.fromJson(Map json) { return VerifyPurchaseProps( - apple: json['apple'] != null - ? VerifyPurchaseAppleOptions.fromJson( - json['apple'] as Map) - : null, - google: json['google'] != null - ? VerifyPurchaseGoogleOptions.fromJson( - json['google'] as Map) - : null, - horizon: json['horizon'] != null - ? VerifyPurchaseHorizonOptions.fromJson( - json['horizon'] as Map) - : null, + apple: json['apple'] != null ? VerifyPurchaseAppleOptions.fromJson(json['apple'] as Map) : null, + google: json['google'] != null ? VerifyPurchaseGoogleOptions.fromJson(json['google'] as Map) : null, + horizon: json['horizon'] != null ? VerifyPurchaseHorizonOptions.fromJson(json['horizon'] as Map) : null, ); } @@ -5001,12 +4469,8 @@ class VerifyPurchaseWithProviderProps { factory VerifyPurchaseWithProviderProps.fromJson(Map json) { return VerifyPurchaseWithProviderProps( - iapkit: json['iapkit'] != null - ? RequestVerifyPurchaseWithIapkitProps.fromJson( - json['iapkit'] as Map) - : null, - provider: - PurchaseVerificationProvider.fromJson(json['provider'] as String), + iapkit: json['iapkit'] != null ? RequestVerifyPurchaseWithIapkitProps.fromJson(json['iapkit'] as Map) : null, + provider: PurchaseVerificationProvider.fromJson(json['provider'] as String), ); } @@ -5094,14 +4558,11 @@ sealed class ProductOrSubscription { case 'ProductIOS': return ProductOrSubscriptionProduct(Product.fromJson(json)); case 'ProductSubscriptionAndroid': - return ProductOrSubscriptionProductSubscription( - ProductSubscription.fromJson(json)); + return ProductOrSubscriptionProductSubscription(ProductSubscription.fromJson(json)); case 'ProductSubscriptionIOS': - return ProductOrSubscriptionProductSubscription( - ProductSubscription.fromJson(json)); + return ProductOrSubscriptionProductSubscription(ProductSubscription.fromJson(json)); } - throw ArgumentError( - 'Unknown __typename for ProductOrSubscription: $typeName'); + throw ArgumentError('Unknown __typename for ProductOrSubscription: $typeName'); } Map toJson(); @@ -5134,8 +4595,7 @@ sealed class ProductSubscription implements ProductCommon { case 'ProductSubscriptionIOS': return ProductSubscriptionIOS.fromJson(json); } - throw ArgumentError( - 'Unknown __typename for ProductSubscription: $typeName'); + throw ArgumentError('Unknown __typename for ProductSubscription: $typeName'); } @override @@ -5194,13 +4654,11 @@ sealed class Purchase implements PurchaseCommon { String get productId; @override PurchaseState get purchaseState; - /// Unified purchase token (iOS JWS, Android purchaseToken) @override String? get purchaseToken; @override int get quantity; - /// Store where purchase was made @override IapStore get store; @@ -5223,8 +4681,7 @@ sealed class VerifyPurchaseResult { case 'VerifyPurchaseResultIOS': return VerifyPurchaseResultIOS.fromJson(json); } - throw ArgumentError( - 'Unknown __typename for VerifyPurchaseResult: $typeName'); + throw ArgumentError('Unknown __typename for VerifyPurchaseResult: $typeName'); } Map toJson(); @@ -5236,75 +4693,60 @@ sealed class VerifyPurchaseResult { abstract class MutationResolver { /// Acknowledge a non-consumable purchase or subscription Future acknowledgePurchaseAndroid(String purchaseToken); - /// Initiate a refund request for a product (iOS 15+) Future beginRefundRequestIOS(String sku); - /// Check if alternative billing is available for this user/device /// Step 1 of alternative billing flow - /// + /// /// Returns true if available, false otherwise /// Throws OpenIapError.NotPrepared if billing client not ready Future checkAlternativeBillingAvailabilityAndroid(); - /// Clear pending transactions from the StoreKit payment queue Future clearTransactionIOS(); - /// Consume a purchase token so it can be repurchased Future consumePurchaseAndroid(String purchaseToken); - /// Create external transaction token for Google Play reporting /// Step 3 of alternative billing flow /// Must be called AFTER successful payment in your payment system /// Token must be reported to Google Play backend within 24 hours - /// + /// /// Returns token string, or null if creation failed /// Throws OpenIapError.NotPrepared if billing client not ready Future createAlternativeBillingTokenAndroid(); - /// Create reporting details for a billing program /// Replaces the deprecated createExternalOfferReportingDetailsAsync API - /// + /// /// Available in Google Play Billing Library 8.2.0+ /// Returns external transaction token needed for reporting external transactions /// Throws OpenIapError.NotPrepared if billing client not ready - Future - createBillingProgramReportingDetailsAndroid( - BillingProgramAndroid program); - + Future createBillingProgramReportingDetailsAndroid(BillingProgramAndroid program); /// Open the native subscription management surface Future deepLinkToSubscriptions({ String? packageNameAndroid, String? skuAndroid, }); - /// Close the platform billing connection Future endConnection(); - /// Finish a transaction after validating receipts Future finishTransaction({ required PurchaseInput purchase, bool? isConsumable, }); - /// Establish the platform billing connection Future initConnection({ AlternativeBillingModeAndroid? alternativeBillingModeAndroid, BillingProgramAndroid? enableBillingProgramAndroid, }); - /// Check if a billing program is available for the current user /// Replaces the deprecated isExternalOfferAvailableAsync API - /// + /// /// Available in Google Play Billing Library 8.2.0+ /// Returns availability result with isAvailable flag /// Throws OpenIapError.NotPrepared if billing client not ready - Future - isBillingProgramAvailableAndroid(BillingProgramAndroid program); - + Future isBillingProgramAvailableAndroid(BillingProgramAndroid program); /// Launch external link flow for external billing programs /// Replaces the deprecated showExternalOfferInformationDialog API - /// + /// /// Available in Google Play Billing Library 8.2.0+ /// Shows Play Store dialog and optionally launches external URL /// Throws OpenIapError.NotPrepared if billing client not ready @@ -5314,69 +4756,52 @@ abstract class MutationResolver { required ExternalLinkTypeAndroid linkType, required String linkUri, }); - /// Present the App Store code redemption sheet Future presentCodeRedemptionSheetIOS(); - /// Present external purchase custom link with StoreKit UI - Future presentExternalPurchaseLinkIOS( - String url); - + Future presentExternalPurchaseLinkIOS(String url); /// Present external purchase notice sheet (iOS 17.4+). /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when user continues. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - Future - presentExternalPurchaseNoticeSheetIOS(); - + Future presentExternalPurchaseNoticeSheetIOS(); /// Initiate a purchase flow; rely on events for final state Future requestPurchase(RequestPurchaseProps params); - /// Purchase the promoted product surfaced by the App Store. - /// + /// /// @deprecated Use promotedProductListenerIOS to receive the productId, /// then call requestPurchase with that SKU instead. In StoreKit 2, /// promoted products can be purchased directly via the standard purchase flow. Future requestPurchaseOnPromotedProductIOS(); - /// Restore completed purchases across platforms Future restorePurchases(); - /// Show alternative billing information dialog to user /// Step 2 of alternative billing flow /// Must be called BEFORE processing payment in your payment system - /// + /// /// Returns true if user accepted, false if user canceled /// Throws OpenIapError.NotPrepared if billing client not ready Future showAlternativeBillingDialogAndroid(); - /// Show ExternalPurchaseCustomLink notice sheet (iOS 18.1+). /// Displays the system disclosure notice sheet for custom external purchase links. /// Call this after a deliberate customer interaction before linking out to external purchases. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - Future - showExternalPurchaseCustomLinkNoticeIOS( - ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); - + Future showExternalPurchaseCustomLinkNoticeIOS(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); /// Open subscription management UI and return changed purchases (iOS 15+) Future> showManageSubscriptionsIOS(); - /// Force a StoreKit sync for transactions (iOS 15+) Future syncIOS(); - /// Validate purchase receipts with the configured providers Future validateReceipt({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, }); - /// Verify purchases with the configured providers Future verifyPurchase({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, }); - /// Verify purchases with a specific provider (e.g., IAPKit) Future verifyPurchaseWithProvider({ RequestVerifyPurchaseWithIapkitProps? iapkit, @@ -5389,75 +4814,53 @@ abstract class QueryResolver { /// Check if external purchase notice sheet can be presented (iOS 17.4+) /// Uses ExternalPurchase.canPresent Future canPresentExternalPurchaseNoticeIOS(); - /// Get current StoreKit 2 entitlements (iOS 15+) Future currentEntitlementIOS(String sku); - /// Retrieve products or subscriptions from the store Future fetchProducts({ required List skus, ProductQueryType? type, }); - /// Get active subscriptions (filters by subscriptionIds when provided) - Future> getActiveSubscriptions( - [List? subscriptionIds]); - + Future> getActiveSubscriptions([List? subscriptionIds]); /// Fetch the current app transaction (iOS 16+) Future getAppTransactionIOS(); - /// Get all available purchases for the current user Future> getAvailablePurchases({ bool? alsoPublishToEventListenerIOS, bool? includeSuspendedAndroid, bool? onlyIncludeActiveItemsIOS, }); - /// Get external purchase token for reporting to Apple (iOS 18.1+). /// Use this token with Apple's External Purchase Server API to report transactions. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - Future - getExternalPurchaseCustomLinkTokenIOS( - ExternalPurchaseCustomLinkTokenTypeIOS tokenType); - + Future getExternalPurchaseCustomLinkTokenIOS(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); /// Retrieve all pending transactions in the StoreKit queue Future> getPendingTransactionsIOS(); - /// Get the currently promoted product (iOS 11+) Future getPromotedProductIOS(); - /// Get base64-encoded receipt data for validation Future getReceiptDataIOS(); - /// Get the current storefront country code Future getStorefront(); - /// Get the current App Store storefront country code Future getStorefrontIOS(); - /// Get the transaction JWS (StoreKit 2) Future getTransactionJwsIOS(String sku); - /// Check whether the user has active subscriptions Future hasActiveSubscriptions([List? subscriptionIds]); - /// Check if app is eligible for ExternalPurchaseCustomLink API (iOS 18.1+). /// Returns true if the app can use custom external purchase links. /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible Future isEligibleForExternalPurchaseCustomLinkIOS(); - /// Check introductory offer eligibility for a subscription group Future isEligibleForIntroOfferIOS(String groupID); - /// Verify a StoreKit 2 transaction signature Future isTransactionVerifiedIOS(String sku); - /// Get the latest transaction for a product using StoreKit 2 Future latestTransactionIOS(String sku); - /// Get StoreKit 2 subscription status details (iOS 15+) Future> subscriptionStatusIOS(String sku); - /// Validate a receipt for a specific product Future validateReceiptIOS({ VerifyPurchaseAppleOptions? apple, @@ -5473,18 +4876,13 @@ abstract class SubscriptionResolver { /// instead of Google Play Billing in the side-by-side choice dialog. /// Contains the externalTransactionToken needed to report the transaction. /// Available in Google Play Billing Library 8.3.0+ - Future - developerProvidedBillingAndroid(); - + Future developerProvidedBillingAndroid(); /// Fires when the App Store surfaces a promoted product (iOS only) Future promotedProductIOS(); - /// Fires when a purchase fails or is cancelled Future purchaseError(); - /// Fires when a purchase completes successfully or a pending purchase resolves Future purchaseUpdated(); - /// Fires when a user selects alternative billing in the User Choice Billing dialog (Android only) /// Only triggered when the user selects alternative billing instead of Google Play billing Future userChoiceBillingAndroid(); @@ -5494,20 +4892,13 @@ abstract class SubscriptionResolver { // MARK: - Mutation Helpers -typedef MutationAcknowledgePurchaseAndroidHandler = Future Function( - String purchaseToken); -typedef MutationBeginRefundRequestIOSHandler = Future Function( - String sku); -typedef MutationCheckAlternativeBillingAvailabilityAndroidHandler = Future - Function(); +typedef MutationAcknowledgePurchaseAndroidHandler = Future Function(String purchaseToken); +typedef MutationBeginRefundRequestIOSHandler = Future Function(String sku); +typedef MutationCheckAlternativeBillingAvailabilityAndroidHandler = Future Function(); typedef MutationClearTransactionIOSHandler = Future Function(); -typedef MutationConsumePurchaseAndroidHandler = Future Function( - String purchaseToken); -typedef MutationCreateAlternativeBillingTokenAndroidHandler = Future - Function(); -typedef MutationCreateBillingProgramReportingDetailsAndroidHandler - = Future Function( - BillingProgramAndroid program); +typedef MutationConsumePurchaseAndroidHandler = Future Function(String purchaseToken); +typedef MutationCreateAlternativeBillingTokenAndroidHandler = Future Function(); +typedef MutationCreateBillingProgramReportingDetailsAndroidHandler = Future Function(BillingProgramAndroid program); typedef MutationDeepLinkToSubscriptionsHandler = Future Function({ String? packageNameAndroid, String? skuAndroid, @@ -5521,9 +4912,7 @@ typedef MutationInitConnectionHandler = Future Function({ AlternativeBillingModeAndroid? alternativeBillingModeAndroid, BillingProgramAndroid? enableBillingProgramAndroid, }); -typedef MutationIsBillingProgramAvailableAndroidHandler - = Future Function( - BillingProgramAndroid program); +typedef MutationIsBillingProgramAvailableAndroidHandler = Future Function(BillingProgramAndroid program); typedef MutationLaunchExternalLinkAndroidHandler = Future Function({ required BillingProgramAndroid billingProgram, required ExternalLinkLaunchModeAndroid launchMode, @@ -5531,22 +4920,14 @@ typedef MutationLaunchExternalLinkAndroidHandler = Future Function({ required String linkUri, }); typedef MutationPresentCodeRedemptionSheetIOSHandler = Future Function(); -typedef MutationPresentExternalPurchaseLinkIOSHandler - = Future Function(String url); -typedef MutationPresentExternalPurchaseNoticeSheetIOSHandler - = Future Function(); -typedef MutationRequestPurchaseHandler = Future - Function(RequestPurchaseProps params); -typedef MutationRequestPurchaseOnPromotedProductIOSHandler = Future - Function(); +typedef MutationPresentExternalPurchaseLinkIOSHandler = Future Function(String url); +typedef MutationPresentExternalPurchaseNoticeSheetIOSHandler = Future Function(); +typedef MutationRequestPurchaseHandler = Future Function(RequestPurchaseProps params); +typedef MutationRequestPurchaseOnPromotedProductIOSHandler = Future Function(); typedef MutationRestorePurchasesHandler = Future Function(); -typedef MutationShowAlternativeBillingDialogAndroidHandler = Future - Function(); -typedef MutationShowExternalPurchaseCustomLinkNoticeIOSHandler - = Future Function( - ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); -typedef MutationShowManageSubscriptionsIOSHandler = Future> - Function(); +typedef MutationShowAlternativeBillingDialogAndroidHandler = Future Function(); +typedef MutationShowExternalPurchaseCustomLinkNoticeIOSHandler = Future Function(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); +typedef MutationShowManageSubscriptionsIOSHandler = Future> Function(); typedef MutationSyncIOSHandler = Future Function(); typedef MutationValidateReceiptHandler = Future Function({ VerifyPurchaseAppleOptions? apple, @@ -5558,8 +4939,7 @@ typedef MutationVerifyPurchaseHandler = Future Function({ VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, }); -typedef MutationVerifyPurchaseWithProviderHandler - = Future Function({ +typedef MutationVerifyPurchaseWithProviderHandler = Future Function({ RequestVerifyPurchaseWithIapkitProps? iapkit, required PurchaseVerificationProvider provider, }); @@ -5596,35 +4976,25 @@ class MutationHandlers { final MutationAcknowledgePurchaseAndroidHandler? acknowledgePurchaseAndroid; final MutationBeginRefundRequestIOSHandler? beginRefundRequestIOS; - final MutationCheckAlternativeBillingAvailabilityAndroidHandler? - checkAlternativeBillingAvailabilityAndroid; + final MutationCheckAlternativeBillingAvailabilityAndroidHandler? checkAlternativeBillingAvailabilityAndroid; final MutationClearTransactionIOSHandler? clearTransactionIOS; final MutationConsumePurchaseAndroidHandler? consumePurchaseAndroid; - final MutationCreateAlternativeBillingTokenAndroidHandler? - createAlternativeBillingTokenAndroid; - final MutationCreateBillingProgramReportingDetailsAndroidHandler? - createBillingProgramReportingDetailsAndroid; + final MutationCreateAlternativeBillingTokenAndroidHandler? createAlternativeBillingTokenAndroid; + final MutationCreateBillingProgramReportingDetailsAndroidHandler? createBillingProgramReportingDetailsAndroid; final MutationDeepLinkToSubscriptionsHandler? deepLinkToSubscriptions; final MutationEndConnectionHandler? endConnection; final MutationFinishTransactionHandler? finishTransaction; final MutationInitConnectionHandler? initConnection; - final MutationIsBillingProgramAvailableAndroidHandler? - isBillingProgramAvailableAndroid; + final MutationIsBillingProgramAvailableAndroidHandler? isBillingProgramAvailableAndroid; final MutationLaunchExternalLinkAndroidHandler? launchExternalLinkAndroid; - final MutationPresentCodeRedemptionSheetIOSHandler? - presentCodeRedemptionSheetIOS; - final MutationPresentExternalPurchaseLinkIOSHandler? - presentExternalPurchaseLinkIOS; - final MutationPresentExternalPurchaseNoticeSheetIOSHandler? - presentExternalPurchaseNoticeSheetIOS; + final MutationPresentCodeRedemptionSheetIOSHandler? presentCodeRedemptionSheetIOS; + final MutationPresentExternalPurchaseLinkIOSHandler? presentExternalPurchaseLinkIOS; + final MutationPresentExternalPurchaseNoticeSheetIOSHandler? presentExternalPurchaseNoticeSheetIOS; final MutationRequestPurchaseHandler? requestPurchase; - final MutationRequestPurchaseOnPromotedProductIOSHandler? - requestPurchaseOnPromotedProductIOS; + final MutationRequestPurchaseOnPromotedProductIOSHandler? requestPurchaseOnPromotedProductIOS; final MutationRestorePurchasesHandler? restorePurchases; - final MutationShowAlternativeBillingDialogAndroidHandler? - showAlternativeBillingDialogAndroid; - final MutationShowExternalPurchaseCustomLinkNoticeIOSHandler? - showExternalPurchaseCustomLinkNoticeIOS; + final MutationShowAlternativeBillingDialogAndroidHandler? showAlternativeBillingDialogAndroid; + final MutationShowExternalPurchaseCustomLinkNoticeIOSHandler? showExternalPurchaseCustomLinkNoticeIOS; final MutationShowManageSubscriptionsIOSHandler? showManageSubscriptionsIOS; final MutationSyncIOSHandler? syncIOS; final MutationValidateReceiptHandler? validateReceipt; @@ -5634,46 +5004,33 @@ class MutationHandlers { // MARK: - Query Helpers -typedef QueryCanPresentExternalPurchaseNoticeIOSHandler = Future - Function(); -typedef QueryCurrentEntitlementIOSHandler = Future Function( - String sku); +typedef QueryCanPresentExternalPurchaseNoticeIOSHandler = Future Function(); +typedef QueryCurrentEntitlementIOSHandler = Future Function(String sku); typedef QueryFetchProductsHandler = Future Function({ required List skus, ProductQueryType? type, }); -typedef QueryGetActiveSubscriptionsHandler = Future> - Function([List? subscriptionIds]); +typedef QueryGetActiveSubscriptionsHandler = Future> Function([List? subscriptionIds]); typedef QueryGetAppTransactionIOSHandler = Future Function(); typedef QueryGetAvailablePurchasesHandler = Future> Function({ bool? alsoPublishToEventListenerIOS, bool? includeSuspendedAndroid, bool? onlyIncludeActiveItemsIOS, }); -typedef QueryGetExternalPurchaseCustomLinkTokenIOSHandler - = Future Function( - ExternalPurchaseCustomLinkTokenTypeIOS tokenType); -typedef QueryGetPendingTransactionsIOSHandler = Future> - Function(); +typedef QueryGetExternalPurchaseCustomLinkTokenIOSHandler = Future Function(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); +typedef QueryGetPendingTransactionsIOSHandler = Future> Function(); typedef QueryGetPromotedProductIOSHandler = Future Function(); typedef QueryGetReceiptDataIOSHandler = Future Function(); typedef QueryGetStorefrontHandler = Future Function(); typedef QueryGetStorefrontIOSHandler = Future Function(); typedef QueryGetTransactionJwsIOSHandler = Future Function(String sku); -typedef QueryHasActiveSubscriptionsHandler = Future Function( - [List? subscriptionIds]); -typedef QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler = Future - Function(); -typedef QueryIsEligibleForIntroOfferIOSHandler = Future Function( - String groupID); -typedef QueryIsTransactionVerifiedIOSHandler = Future Function( - String sku); -typedef QueryLatestTransactionIOSHandler = Future Function( - String sku); -typedef QuerySubscriptionStatusIOSHandler = Future> - Function(String sku); -typedef QueryValidateReceiptIOSHandler = Future - Function({ +typedef QueryHasActiveSubscriptionsHandler = Future Function([List? subscriptionIds]); +typedef QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler = Future Function(); +typedef QueryIsEligibleForIntroOfferIOSHandler = Future Function(String groupID); +typedef QueryIsTransactionVerifiedIOSHandler = Future Function(String sku); +typedef QueryLatestTransactionIOSHandler = Future Function(String sku); +typedef QuerySubscriptionStatusIOSHandler = Future> Function(String sku); +typedef QueryValidateReceiptIOSHandler = Future Function({ VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, @@ -5703,15 +5060,13 @@ class QueryHandlers { this.validateReceiptIOS, }); - final QueryCanPresentExternalPurchaseNoticeIOSHandler? - canPresentExternalPurchaseNoticeIOS; + final QueryCanPresentExternalPurchaseNoticeIOSHandler? canPresentExternalPurchaseNoticeIOS; final QueryCurrentEntitlementIOSHandler? currentEntitlementIOS; final QueryFetchProductsHandler? fetchProducts; final QueryGetActiveSubscriptionsHandler? getActiveSubscriptions; final QueryGetAppTransactionIOSHandler? getAppTransactionIOS; final QueryGetAvailablePurchasesHandler? getAvailablePurchases; - final QueryGetExternalPurchaseCustomLinkTokenIOSHandler? - getExternalPurchaseCustomLinkTokenIOS; + final QueryGetExternalPurchaseCustomLinkTokenIOSHandler? getExternalPurchaseCustomLinkTokenIOS; final QueryGetPendingTransactionsIOSHandler? getPendingTransactionsIOS; final QueryGetPromotedProductIOSHandler? getPromotedProductIOS; final QueryGetReceiptDataIOSHandler? getReceiptDataIOS; @@ -5719,8 +5074,7 @@ class QueryHandlers { final QueryGetStorefrontIOSHandler? getStorefrontIOS; final QueryGetTransactionJwsIOSHandler? getTransactionJwsIOS; final QueryHasActiveSubscriptionsHandler? hasActiveSubscriptions; - final QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler? - isEligibleForExternalPurchaseCustomLinkIOS; + final QueryIsEligibleForExternalPurchaseCustomLinkIOSHandler? isEligibleForExternalPurchaseCustomLinkIOS; final QueryIsEligibleForIntroOfferIOSHandler? isEligibleForIntroOfferIOS; final QueryIsTransactionVerifiedIOSHandler? isTransactionVerifiedIOS; final QueryLatestTransactionIOSHandler? latestTransactionIOS; @@ -5730,13 +5084,11 @@ class QueryHandlers { // MARK: - Subscription Helpers -typedef SubscriptionDeveloperProvidedBillingAndroidHandler - = Future Function(); +typedef SubscriptionDeveloperProvidedBillingAndroidHandler = Future Function(); typedef SubscriptionPromotedProductIOSHandler = Future Function(); typedef SubscriptionPurchaseErrorHandler = Future Function(); typedef SubscriptionPurchaseUpdatedHandler = Future Function(); -typedef SubscriptionUserChoiceBillingAndroidHandler - = Future Function(); +typedef SubscriptionUserChoiceBillingAndroidHandler = Future Function(); class SubscriptionHandlers { const SubscriptionHandlers({ @@ -5747,8 +5099,7 @@ class SubscriptionHandlers { this.userChoiceBillingAndroid, }); - final SubscriptionDeveloperProvidedBillingAndroidHandler? - developerProvidedBillingAndroid; + final SubscriptionDeveloperProvidedBillingAndroidHandler? developerProvidedBillingAndroid; final SubscriptionPromotedProductIOSHandler? promotedProductIOS; final SubscriptionPurchaseErrorHandler? purchaseError; final SubscriptionPurchaseUpdatedHandler? purchaseUpdated; diff --git a/packages/gql/scripts/sync-to-platforms.mjs b/packages/gql/scripts/sync-to-platforms.mjs index 51445f25..4fd1b3bc 100755 --- a/packages/gql/scripts/sync-to-platforms.mjs +++ b/packages/gql/scripts/sync-to-platforms.mjs @@ -84,6 +84,10 @@ if (existsSync(swiftSource)) { } // Sync Dart to flutter_inapp_purchase +// Note: the flutter_inapp_purchase CLAUDE.md explicitly excludes +// `lib/types.dart` from the Dart format check, so we intentionally copy +// the raw generator output verbatim. `bun run generate` is reproducible +// because no formatter mutates the file afterwards. if (existsSync(dartSource)) { mkdirSync(dirname(dartTarget), { recursive: true }); copyFileSync(dartSource, dartTarget); From 94c83bbf7c3b2c6206f93d253c2228e1bbd8bff6 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 21:55:26 +0900 Subject: [PATCH 8/9] ci(gql): catch untracked files in drift check Broaden the drift check to also fail on untracked files that appear after \`bun run generate\`. The previous \`git diff --exit-code\` only flagged modified *tracked* files, so a plugin that starts emitting a new output would slip through silently. Combine \`git status --porcelain\` with the existing diff check and print the offending paths so the failure mode is easy to diagnose. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5698b5c8..8afd3c75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,10 +95,19 @@ jobs: # as the files checked into the repo. If this step fails, # somebody changed a codegen plugin or the schema without # re-running `bun run generate` + committing the output. - if ! git diff --exit-code; then + # + # Use `git status --porcelain` (plus `git diff`) so newly- + # generated but untracked files also fail the check — a + # plain `git diff` would silently pass if the generator + # starts emitting a new target file. + status="$(git status --porcelain)" + if [ -n "$status" ] || ! git diff --exit-code; then echo "" echo "::error::Generated types differ from checked-in copies." echo "Run 'cd packages/gql && bun run generate' locally and commit the result." + echo "" + echo "Untracked or modified paths:" + printf '%s\n' "$status" exit 1 fi From 9790e6a37760ab832b53c26ccd3e522d5419a849 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 15 Apr 2026 22:23:13 +0900 Subject: [PATCH 9/9] fix(google): populate debugMessage at remaining error call sites Addresses the remaining CodeRabbit flags from the 6th review round (3086661442 / 3086661449 / 3086661461): every \`OpenIapError.X()\` throw/emit where a precise local cause is known now forwards that context through \`debugMessage\`. - \`requestAlternativeBillingAndroid\`: \`PurchaseFailed()\` for the null-token branch becomes \`PurchaseFailed("Alternative billing token creation returned null")\`, and the alternative-billing dialog-dismissed path upgrades from \`UserCancelled()\` to include the concrete cause. - Concurrent purchase rejection (compareAndSet fails): throws \`DeveloperError("Another purchase is already in progress")\`. - \`launchBillingFlow\` result and \`onPurchasesUpdated\` USER_CANCELED branches forward \`result.debugMessage\` / \`billingResult.debugMessage\` to \`UserCancelled\`, matching the pattern the other response codes already use. - \`verifyPurchaseWithProvider\` and Horizon's \`verifyPurchase\` / \`verifyPurchaseWithProvider\` null-guards now throw \`DeveloperError\` with the concrete missing-parameter text ("Missing IAPKit verification parameters" / "Horizon verifyPurchase requires appId to be set during initConnection") instead of a bare \`DeveloperError()\`. All changes mirrored across the play and horizon flavors where the same code exists. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../java/dev/hyo/openiap/OpenIapModule.kt | 10 +++++++--- .../java/dev/hyo/openiap/OpenIapModule.kt | 20 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index b5183044..6f36e70a 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -514,7 +514,7 @@ class OpenIapModule( OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) OpenIapError.DeveloperError(result.debugMessage) } - BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled() + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled(result.debugMessage) else -> OpenIapError.PurchaseFailed(result.debugMessage) } purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } } @@ -702,7 +702,9 @@ class OpenIapModule( override val verifyPurchase: MutationVerifyPurchaseHandler = { props -> // Use Horizon API if horizon options provided, otherwise fallback to Google Play if (props.horizon != null) { - val horizonAppId = appId ?: throw OpenIapError.DeveloperError() + val horizonAppId = appId ?: throw OpenIapError.DeveloperError( + "Horizon verifyPurchase requires appId to be set during initConnection" + ) val horizonResult = verifyPurchaseWithHorizon(props, horizonAppId, TAG) if (!horizonResult.success) { throw OpenIapError.InvalidPurchaseVerification @@ -717,7 +719,9 @@ class OpenIapModule( if (props.provider != PurchaseVerificationProvider.Iapkit) { throw OpenIapError.FeatureNotSupported() } - val options = props.iapkit ?: throw OpenIapError.DeveloperError() + val options = props.iapkit ?: throw OpenIapError.DeveloperError( + "Missing IAPKit verification parameters" + ) VerifyPurchaseWithProviderResult( iapkit = verifyPurchaseWithIapkit(options, TAG), provider = props.provider diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt index 660b9ab6..436175f4 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt @@ -773,7 +773,9 @@ class OpenIapModule( @Suppress("DEPRECATION") val dialogSuccess = showAlternativeBillingInformationDialog(activity) if (!dialogSuccess) { - val err = OpenIapError.UserCancelled() + val err = OpenIapError.UserCancelled( + "User dismissed the alternative billing information dialog" + ) for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() } @@ -831,7 +833,9 @@ class OpenIapModule( // Return empty list - app should handle purchase via alternative billing return@withContext emptyList() } else { - val err = OpenIapError.PurchaseFailed() + val err = OpenIapError.PurchaseFailed( + "Alternative billing token creation returned null" + ) for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() } @@ -871,7 +875,9 @@ class OpenIapModule( } if (!currentPurchaseCallback.compareAndSet(null, callback)) { OpenIapLog.w("requestPurchase rejected: another purchase is already in progress", TAG) - if (continuation.isActive) continuation.resumeWithException(OpenIapError.DeveloperError()) + if (continuation.isActive) continuation.resumeWithException( + OpenIapError.DeveloperError("Another purchase is already in progress") + ) return@suspendCancellableCoroutine } continuation.invokeOnCancellation { currentPurchaseCallback.compareAndSet(callback, null) } @@ -1052,7 +1058,7 @@ class OpenIapModule( OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) OpenIapError.DeveloperError(result.debugMessage) } - BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled() + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled(result.debugMessage) else -> OpenIapError.PurchaseFailed(result.debugMessage) } for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } @@ -1209,7 +1215,9 @@ class OpenIapModule( if (props.provider != PurchaseVerificationProvider.Iapkit) { throw OpenIapError.FeatureNotSupported() } - val options = props.iapkit ?: throw OpenIapError.DeveloperError() + val options = props.iapkit ?: throw OpenIapError.DeveloperError( + "Missing IAPKit verification parameters" + ) VerifyPurchaseWithProviderResult( iapkit = verifyPurchaseWithIapkit(options, TAG), provider = props.provider @@ -1365,7 +1373,7 @@ class OpenIapModule( } else { when (billingResult.responseCode) { BillingClient.BillingResponseCode.USER_CANCELED -> { - val err = OpenIapError.UserCancelled() + val err = OpenIapError.UserCancelled(billingResult.debugMessage) for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) }