diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88fb7e7a..8afd3c75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,29 @@ 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. + # + # 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 + test-android: name: Test Android runs-on: ubuntu-latest diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index 7bb7aec7..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', @@ -1105,6 +1106,7 @@ export interface PurchaseCommon { export interface PurchaseError { code: ErrorCode; + debugMessage?: (string | null); message: string; productId?: (string | null); } 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/flutter_inapp_purchase/lib/helpers.dart b/libraries/flutter_inapp_purchase/lib/helpers.dart index d2dac1b3..91e922dc 100644 --- a/libraries/flutter_inapp_purchase/lib/helpers.dart +++ b/libraries/flutter_inapp_purchase/lib/helpers.dart @@ -408,6 +408,9 @@ iap_err.PurchaseError convertToPurchaseError( return iap_err.PurchaseError( message: result.message ?? 'Unknown error', 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..67d7d3f5 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/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': @@ -2570,17 +2573,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 +2596,7 @@ class PurchaseError { return { '__typename': 'PurchaseError', 'code': code.toJson(), + 'debugMessage': debugMessage, 'message': message, 'productId': productId, }; diff --git a/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart b/libraries/flutter_inapp_purchase/test/helpers_unit_test.dart index a32dd899..70446e32 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,34 @@ 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); + 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..fed9bd9b 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+ @@ -299,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 @@ -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 != 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: @@ -383,9 +393,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() @@ -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 != 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+ @@ -495,7 +508,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 @@ -516,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: @@ -563,7 +577,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 @@ -580,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: @@ -596,7 +611,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() @@ -634,13 +649,14 @@ 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 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 @@ -650,17 +666,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. @@ -670,7 +686,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() @@ -726,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 @@ -734,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: @@ -756,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 @@ -854,7 +877,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() @@ -867,15 +890,16 @@ 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+). 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() @@ -887,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 @@ -896,7 +922,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() @@ -909,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. @@ -917,9 +944,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() @@ -941,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+ @@ -1097,11 +1126,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+) @@ -1196,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: @@ -1255,7 +1287,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 @@ -1264,7 +1296,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 @@ -1276,7 +1308,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() @@ -1325,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: @@ -1352,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: @@ -1360,11 +1395,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 @@ -1441,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: @@ -1478,11 +1516,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+) @@ -1577,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: @@ -1636,7 +1677,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 @@ -1668,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"): @@ -1686,11 +1728,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 @@ -1702,12 +1744,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: @@ -1810,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: @@ -1850,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: @@ -1872,26 +1921,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 @@ -1963,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: @@ -1980,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: @@ -1999,7 +2060,8 @@ class PurchaseAndroid: class PurchaseError: var code: ErrorCode var message: String = "" - var product_id: String = "" + var product_id: Variant = null + var debug_message: Variant = null static func from_dict(data: Dictionary) -> PurchaseError: var obj = PurchaseError.new() @@ -2013,6 +2075,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: @@ -2022,7 +2086,10 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - dict["productId"] = product_id + if product_id != null: + dict["productId"] = product_id + if debug_message != null: + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2030,36 +2097,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: @@ -2158,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: @@ -2173,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: @@ -2229,7 +2318,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() @@ -2242,30 +2331,31 @@ 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 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() @@ -2295,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+ @@ -2313,7 +2413,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() @@ -2326,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: @@ -2429,31 +2530,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. @@ -2525,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: @@ -2534,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() @@ -2739,10 +2850,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 = "" @@ -2800,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 @@ -2823,7 +2938,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() @@ -2836,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: @@ -2871,7 +2987,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() @@ -2884,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: @@ -2981,9 +3098,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() @@ -3232,7 +3349,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 @@ -3312,11 +3429,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() @@ -3342,13 +3459,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 @@ -3394,15 +3511,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() @@ -3450,7 +3567,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() @@ -3558,15 +3675,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+) @@ -3644,9 +3761,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+) @@ -3654,9 +3771,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() @@ -3814,7 +3931,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. @@ -3910,7 +4027,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() @@ -4127,6 +4244,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", @@ -4343,6 +4461,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 8a45bfab..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 @@ -8,7 +8,6 @@ package io.github.hyochan.kmpiap.openiap - // MARK: - Enums /** @@ -34,6 +33,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 +83,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 +123,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 +156,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 @@ -203,13 +206,16 @@ 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"), 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 @@ -302,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 @@ -323,6 +332,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 +360,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 +394,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 +421,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 +450,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 +478,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 +504,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 +569,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 +600,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 +620,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 +663,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 +690,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 +716,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 +758,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 +779,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 +800,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 +826,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 +847,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 +877,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 +901,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 +926,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 +960,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 +1019,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 +2620,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 +2629,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 +2639,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..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 { @@ -153,5 +155,7 @@ 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" + 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 7bb7aec7..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', @@ -1105,6 +1106,7 @@ export interface PurchaseCommon { export interface PurchaseError { code: ErrorCode; + debugMessage?: (string | null); message: string; productId?: (string | null); } 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]: 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 9fa68ab6..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": @@ -454,35 +457,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 +493,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 +523,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 +555,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 +566,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 +587,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 +672,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 +696,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 +775,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 +814,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 +868,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 +908,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 +917,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 +1034,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 +1074,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 +1093,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 +1111,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 +1123,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 +1187,7 @@ public struct SubscriptionPeriodValueIOS: Codable { } public struct SubscriptionStatusIOS: Codable { - public var renewalInfo: RenewalInfoIOS? + public var renewalInfo: RenewalInfoIOS? = nil public var state: String } @@ -1208,10 +1212,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 +1234,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 +1245,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..448364fc 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 ) @@ -1534,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/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 46a8a9a9..702b8792 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -23,6 +23,201 @@ 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 2.0.0 + +
+
    +
  • + + Breaking: OpenIapError error types are now data + classes + {' '} + — 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{' '} + launchBillingFlow sync-failure path now also + produces DeveloperError (matching the{' '} + onPurchasesUpdated async path) instead of a generic{' '} + 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. +
  • +
+

+ + 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 2.0.0 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 2.0.0 (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..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.DEVELOPER_ERROR -> OpenIapError.DeveloperError - 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.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(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 70c3eccc..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 @@ -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 + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled(result.debugMessage) + else -> OpenIapError.PurchaseFailed(result.debugMessage) } purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) @@ -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 @@ -715,9 +717,11 @@ 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 + 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/main/java/dev/hyo/openiap/OpenIapError.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt index 983ffb57..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 @@ -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 + data 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() { @@ -56,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" + } } /** @@ -122,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() { @@ -186,82 +201,104 @@ 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" + } } - object DeveloperError : OpenIapError() { - val CODE = ErrorCode.DeveloperError.rawValue + data 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() { - 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 { + val CODE = ErrorCode.ServiceTimeout.rawValue + 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..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 @@ -2527,6 +2530,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 +2539,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 +2549,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 90cd11bb..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.DEVELOPER_ERROR -> OpenIapError.DeveloperError - 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.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(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 fcf67f80..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 @@ -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 ?: e.javaClass.simpleName) + ) } } 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 ?: e.javaClass.simpleName) } } } @@ -767,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() } @@ -825,13 +833,15 @@ 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() } } 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() } @@ -865,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) } @@ -1044,10 +1056,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 + BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled(result.debugMessage) + else -> OpenIapError.PurchaseFailed(result.debugMessage) } for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) @@ -1113,7 +1125,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 +1145,7 @@ class OpenIapModule( } if (result.responseCode != BillingClient.BillingResponseCode.OK) { - throw OpenIapError.PurchaseFailed + throw OpenIapError.PurchaseFailed(result.debugMessage) } } } @@ -1201,9 +1213,11 @@ 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 + val options = props.iapkit ?: throw OpenIapError.DeveloperError( + "Missing IAPKit verification parameters" + ) VerifyPurchaseWithProviderResult( iapkit = verifyPurchaseWithIapkit(options, TAG), provider = props.provider @@ -1359,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())) } 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..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 @@ -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 @@ -44,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) } @@ -80,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) } @@ -137,70 +145,78 @@ 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) } @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 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) } @@ -210,16 +226,16 @@ 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, - OpenIapError.BillingError, + OpenIapError.BillingError(), OpenIapError.InvalidReceipt, OpenIapError.NetworkError, OpenIapError.VerificationFailed, OpenIapError.RestoreFailed, - OpenIapError.UnknownError, + OpenIapError.UnknownError(), OpenIapError.NotPrepared, OpenIapError.InitConnection, OpenIapError.QueryProduct, @@ -227,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.DeveloperError, - OpenIapError.FeatureNotSupported, - OpenIapError.ServiceDisconnected, - OpenIapError.ServiceTimeout + OpenIapError.UserCancelled(), + OpenIapError.ItemAlreadyOwned(), + OpenIapError.ItemNotOwned(), + OpenIapError.ServiceUnavailable(), + OpenIapError.BillingUnavailable(), + OpenIapError.ItemUnavailable(), + OpenIapError.DeveloperError(), + OpenIapError.FeatureNotSupported(), + OpenIapError.ServiceDisconnected(), + OpenIapError.ServiceTimeout() ) errors.forEach { error -> @@ -269,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`() { @@ -317,16 +357,16 @@ 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, - 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, @@ -334,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.DeveloperError to ErrorCode.DeveloperError.rawValue, - OpenIapError.FeatureNotSupported to ErrorCode.FeatureNotSupported.rawValue, - OpenIapError.ServiceDisconnected to ErrorCode.ServiceDisconnected.rawValue, - OpenIapError.ServiceTimeout to "service-timeout" + 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" ) errors.forEach { (error, expectedCode) -> diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts index 0ad158a4..75e42415 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,11 +429,38 @@ 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 { + // Non-nullable scalars carry their type-appropriate default + // (e.g. "" for String) and are always emitted. 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; @@ -452,7 +488,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/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/scripts/sync-to-platforms.mjs b/packages/gql/scripts/sync-to-platforms.mjs index c95ede36..4fd1b3bc 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,81 @@ if (existsSync(swiftSource)) { console.warn('⚠️ Swift types not found, skipping Apple sync'); } +// 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); + 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 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)) { + 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 + // 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 bcaa0c6c..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 @@ -50,4 +51,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..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 @@ -2614,6 +2618,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 +2627,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 +2637,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..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": @@ -454,35 +457,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 +493,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 +523,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 +555,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 +566,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 +587,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 +672,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 +696,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 +775,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 +814,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 +868,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 +908,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 +917,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 +1034,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 +1074,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 +1093,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 +1111,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 +1123,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 +1187,7 @@ public struct SubscriptionPeriodValueIOS: Codable { } public struct SubscriptionStatusIOS: Codable { - public var renewalInfo: RenewalInfoIOS? + public var renewalInfo: RenewalInfoIOS? = nil public var state: String } @@ -1208,10 +1212,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 +1234,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 +1245,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..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': @@ -2570,17 +2573,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 +2596,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..fed9bd9b 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+ @@ -299,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 @@ -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 != 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: @@ -383,9 +393,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() @@ -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 != 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+ @@ -495,7 +508,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 @@ -516,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: @@ -563,7 +577,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 @@ -580,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: @@ -596,7 +611,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() @@ -634,13 +649,14 @@ 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 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 @@ -650,17 +666,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. @@ -670,7 +686,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() @@ -726,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 @@ -734,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: @@ -756,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 @@ -854,7 +877,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() @@ -867,15 +890,16 @@ 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+). 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() @@ -887,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 @@ -896,7 +922,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() @@ -909,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. @@ -917,9 +944,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() @@ -941,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+ @@ -1097,11 +1126,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+) @@ -1196,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: @@ -1255,7 +1287,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 @@ -1264,7 +1296,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 @@ -1276,7 +1308,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() @@ -1325,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: @@ -1352,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: @@ -1360,11 +1395,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 @@ -1441,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: @@ -1478,11 +1516,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+) @@ -1577,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: @@ -1636,7 +1677,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 @@ -1668,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"): @@ -1686,11 +1728,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 @@ -1702,12 +1744,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: @@ -1810,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: @@ -1850,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: @@ -1872,26 +1921,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 @@ -1963,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: @@ -1980,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: @@ -1999,7 +2060,8 @@ class PurchaseAndroid: class PurchaseError: var code: ErrorCode var message: String = "" - var product_id: String = "" + var product_id: Variant = null + var debug_message: Variant = null static func from_dict(data: Dictionary) -> PurchaseError: var obj = PurchaseError.new() @@ -2013,6 +2075,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: @@ -2022,7 +2086,10 @@ class PurchaseError: else: dict["code"] = code dict["message"] = message - dict["productId"] = product_id + if product_id != null: + dict["productId"] = product_id + if debug_message != null: + dict["debugMessage"] = debug_message return dict class PurchaseIOS: @@ -2030,36 +2097,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: @@ -2158,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: @@ -2173,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: @@ -2229,7 +2318,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() @@ -2242,30 +2331,31 @@ 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 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() @@ -2295,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+ @@ -2313,7 +2413,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() @@ -2326,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: @@ -2429,31 +2530,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. @@ -2525,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: @@ -2534,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() @@ -2739,10 +2850,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 = "" @@ -2800,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 @@ -2823,7 +2938,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() @@ -2836,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: @@ -2871,7 +2987,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() @@ -2884,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: @@ -2981,9 +3098,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() @@ -3232,7 +3349,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 @@ -3312,11 +3429,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() @@ -3342,13 +3459,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 @@ -3394,15 +3511,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() @@ -3450,7 +3567,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() @@ -3558,15 +3675,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+) @@ -3644,9 +3761,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+) @@ -3654,9 +3771,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() @@ -3814,7 +3931,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. @@ -3910,7 +4027,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() @@ -4127,6 +4244,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", @@ -4343,6 +4461,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 7bb7aec7..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', @@ -1105,6 +1106,7 @@ export interface PurchaseCommon { export interface PurchaseError { code: ErrorCode; + debugMessage?: (string | null); message: string; productId?: (string | null); }