From ab1cf17d9c796b6f3f76c8991f3a8cc96737db7b Mon Sep 17 00:00:00 2001 From: Hyo Date: Thu, 13 Aug 2026 15:23:25 +0900 Subject: [PATCH 01/12] fix: tolerate future IAPKit values - degrade generated enum decoders to neutral members - drop unreadable optional payloads and MAUI response rows - cover generator and MAUI contract behavior Closes #330 --- .../flutter_inapp_purchase/lib/types.dart | 58 ++++++-- libraries/godot-iap/addons/godot-iap/types.gd | 98 +++++++++----- .../io/github/hyochan/kmpiap/openiap/Types.kt | 68 +++++----- libraries/maui-iap/src/OpenIap.Maui/KitApi.cs | 65 +++++++++ libraries/maui-iap/src/OpenIap.Maui/Types.cs | 113 +++++++++++++--- .../OpenIap.Maui.ContractTests/Program.cs | 78 +++++++++++ .../tests/OpenIap.Maui.Tests/EnumJsonTests.cs | 6 +- .../OpenIap.Maui.Tests/KitApiClientTests.cs | 104 +++++++++++++++ .../src/main/java/dev/hyo/openiap/Types.kt | 68 +++++----- packages/gql/codegen/plugins/base-plugin.ts | 24 ++++ packages/gql/codegen/plugins/csharp.ts | 54 +++++++- packages/gql/codegen/plugins/dart.ts | 26 +++- packages/gql/codegen/plugins/gdscript.ts | 42 +++++- packages/gql/codegen/plugins/kotlin.ts | 59 +++++++-- .../src/enum-decoder-compatibility.test.ts | 124 ++++++++++++++++++ packages/gql/src/generated/Types.cs | 113 +++++++++++++--- packages/gql/src/generated/Types.kt | 68 +++++----- packages/gql/src/generated/types.dart | 58 ++++++-- packages/gql/src/generated/types.gd | 98 +++++++++----- 19 files changed, 1068 insertions(+), 256 deletions(-) create mode 100644 packages/gql/src/enum-decoder-compatibility.test.ts diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 13e69dde8..3cd4c0c61 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -329,7 +329,7 @@ enum ErrorCode { case 'duplicate-purchase': return ErrorCode.DuplicatePurchase; } - throw ArgumentError('Unknown ErrorCode value: $value'); + return ErrorCode.Unknown; } String toJson() => value; @@ -584,7 +584,7 @@ enum IapkitPurchaseState { case 'inauthentic': return IapkitPurchaseState.Inauthentic; } - throw ArgumentError('Unknown IapkitPurchaseState value: $value'); + return IapkitPurchaseState.Unknown; } String toJson() => value; @@ -635,7 +635,7 @@ enum IapStore { case 'amazon': return IapStore.Amazon; } - throw ArgumentError('Unknown IapStore value: $value'); + return IapStore.Unknown; } String toJson() => value; @@ -661,7 +661,7 @@ enum InAppMessageCategoryAndroid { case 'transactional': return InAppMessageCategoryAndroid.Transactional; } - throw ArgumentError('Unknown InAppMessageCategoryAndroid value: $value'); + return InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; } String toJson() => value; @@ -720,7 +720,7 @@ enum PaymentMode { case 'unknown': return PaymentMode.Unknown; } - throw ArgumentError('Unknown PaymentMode value: $value'); + return PaymentMode.Unknown; } String toJson() => value; @@ -806,7 +806,7 @@ enum ProductStatusAndroid { case 'unknown': return ProductStatusAndroid.Unknown; } - throw ArgumentError('Unknown ProductStatusAndroid value: $value'); + return ProductStatusAndroid.Unknown; } String toJson() => value; @@ -886,7 +886,7 @@ enum PurchaseState { case 'unknown': return PurchaseState.Unknown; } - throw ArgumentError('Unknown PurchaseState value: $value'); + return PurchaseState.Unknown; } String toJson() => value; @@ -960,7 +960,7 @@ enum SubscriptionBillingPlanTypeIOS { case 'up-front': return SubscriptionBillingPlanTypeIOS.UpFront; } - throw ArgumentError('Unknown SubscriptionBillingPlanTypeIOS value: $value'); + return SubscriptionBillingPlanTypeIOS.Unknown; } String toJson() => value; @@ -1047,7 +1047,7 @@ enum SubscriptionPeriodUnit { case 'unknown': return SubscriptionPeriodUnit.Unknown; } - throw ArgumentError('Unknown SubscriptionPeriodUnit value: $value'); + return SubscriptionPeriodUnit.Unknown; } String toJson() => value; @@ -1093,7 +1093,7 @@ enum SubscriptionReplacementModeAndroid { case 'keep-existing': return SubscriptionReplacementModeAndroid.KeepExisting; } - throw ArgumentError('Unknown SubscriptionReplacementModeAndroid value: $value'); + return SubscriptionReplacementModeAndroid.UnknownReplacementMode; } String toJson() => value; @@ -1264,7 +1264,7 @@ class AdvancedCommerceInfoIOS { displayName: json['displayName'] as String?, estimatedTax: json['estimatedTax'] as String?, items: (json['items'] as List).map((e) => AdvancedCommerceItemIOS.fromJson(e as Map)).toList(), - period: json['period'] != null ? SubscriptionPeriodValueIOS.fromJson(json['period'] as Map) : null, + period: json['period'] is Map ? SubscriptionPeriodValueIOS._tryFromJson(json['period'] as Map) : null, requestReferenceId: json['requestReferenceId'] as String?, taxCode: json['taxCode'] as String?, taxExclusivePrice: json['taxExclusivePrice'] as String?, @@ -2092,6 +2092,16 @@ class IapkitProductClientPayload { ); } + static IapkitProductClientPayload? _tryFromJson(Map json) { + try { + return IapkitProductClientPayload.fromJson(json); + } on ArgumentError { + return null; + } on TypeError { + return null; + } + } + Map toJson() { return { '__typename': 'IapkitProductClientPayload', @@ -3374,7 +3384,7 @@ class RequestVerifyPurchaseWithIapkitResult { factory RequestVerifyPurchaseWithIapkitResult.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitResult( - clientPayload: json['clientPayload'] != null ? IapkitProductClientPayload.fromJson(json['clientPayload'] as Map) : null, + clientPayload: json['clientPayload'] is Map ? IapkitProductClientPayload._tryFromJson(json['clientPayload'] as Map) : null, environment: json['environment'] as String?, isValid: json['isValid'] as bool, productId: json['productId'] as String?, @@ -3600,6 +3610,16 @@ class SubscriptionPeriodValueIOS { ); } + static SubscriptionPeriodValueIOS? _tryFromJson(Map json) { + try { + return SubscriptionPeriodValueIOS.fromJson(json); + } on ArgumentError { + return null; + } on TypeError { + return null; + } + } + Map toJson() { return { '__typename': 'SubscriptionPeriodValueIOS', @@ -4126,6 +4146,16 @@ class DeveloperBillingOptionParamsAndroid { ); } + static DeveloperBillingOptionParamsAndroid? _tryFromJson(Map json) { + try { + return DeveloperBillingOptionParamsAndroid.fromJson(json); + } on ArgumentError { + return null; + } on TypeError { + return null; + } + } + Map toJson() { return { 'billingProgram': billingProgram.toJson(), @@ -4466,7 +4496,7 @@ class RequestPurchaseAndroidProps { factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, @@ -4669,7 +4699,7 @@ class RequestSubscriptionAndroidProps { factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 097d3c06d..2e5b03b11 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -470,8 +470,6 @@ class AdvancedCommerceInfoIOS: if data.has("period") and data["period"] != null: if data["period"] is Dictionary: obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) - else: - obj.period = data["period"] if data.has("requestReferenceId") and data["requestReferenceId"] != null: obj.request_reference_id = data["requestReferenceId"] if data.has("taxCode") and data["taxCode"] != null: @@ -1259,6 +1257,8 @@ class IapkitProductClientPayload: var updated_at: float = 0.0 static func from_dict(data: Dictionary) -> IapkitProductClientPayload: + if not data.has("format") or not data["format"] is String or not IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING.has(data["format"]): + return null var obj = IapkitProductClientPayload.new() if data.has("format") and data["format"] != null: var enum_str = data["format"] @@ -1522,8 +1522,10 @@ class ProductAndroid: var enum_str = data["productStatusAndroid"] if enum_str is String: obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN) - else: + elif enum_str is int: obj.product_status_android = enum_str + else: + obj.product_status_android = ProductStatusAndroid.UNKNOWN if data.has("discountOffers") and data["discountOffers"] != null: if data["discountOffers"] is Array: var arr: Array[DiscountOffer] = [] @@ -1778,8 +1780,10 @@ class ProductSubscriptionAndroid: var enum_str = data["productStatusAndroid"] if enum_str is String: obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN) - else: + elif enum_str is int: obj.product_status_android = enum_str + else: + obj.product_status_android = ProductStatusAndroid.UNKNOWN if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: if data["subscriptionOffers"] is Array: var arr: Array[SubscriptionOffer] = [] @@ -2098,16 +2102,20 @@ class PurchaseAndroid: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("quantity") and data["quantity"] != null: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] if enum_str is String: obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.purchase_state = enum_str + else: + obj.purchase_state = PurchaseState.UNKNOWN if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2208,8 +2216,10 @@ class PurchaseError: var enum_str = data["code"] if enum_str is String: obj.code = ERROR_CODE_FROM_STRING.get(enum_str, ErrorCode.UNKNOWN) - else: + elif enum_str is int: obj.code = enum_str + else: + obj.code = ErrorCode.UNKNOWN if data.has("message") and data["message"] != null: obj.message = data["message"] if data.has("productId") and data["productId"] != null: @@ -2340,16 +2350,20 @@ class PurchaseIOS: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("quantity") and data["quantity"] != null: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] if enum_str is String: obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.purchase_state = enum_str + else: + obj.purchase_state = PurchaseState.UNKNOWN if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2412,8 +2426,10 @@ class PurchaseIOS: var enum_str = data["billingPlanTypeIOS"] if enum_str is String: obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.billing_plan_type_ios = enum_str + else: + obj.billing_plan_type_ios = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null: if data["commitmentInfoIOS"] is Dictionary: obj.commitment_info_ios = TransactionCommitmentInfoIOS.from_dict(data["commitmentInfoIOS"]) @@ -2590,8 +2606,10 @@ class RenewalCommitmentInfoIOS: var enum_str = data["commitmentRenewalBillingPlanType"] if enum_str is String: obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.commitment_renewal_billing_plan_type = enum_str + else: + obj.commitment_renewal_billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null: obj.commitment_renewal_date = data["commitmentRenewalDate"] if data.has("commitmentRenewalPrice") and data["commitmentRenewalPrice"] != null: @@ -2672,8 +2690,10 @@ class RenewalInfoIOS: var enum_str = data["renewalBillingPlanType"] if enum_str is String: obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.renewal_billing_plan_type = enum_str + else: + obj.renewal_billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("commitmentInfo") and data["commitmentInfo"] != null: if data["commitmentInfo"] is Dictionary: obj.commitment_info = RenewalCommitmentInfoIOS.from_dict(data["commitmentInfo"]) @@ -2772,8 +2792,10 @@ class RequestVerifyPurchaseWithIapkitResult: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("environment") and data["environment"] != null: obj.environment = data["environment"] if data.has("isValid") and data["isValid"] != null: @@ -2782,15 +2804,15 @@ class RequestVerifyPurchaseWithIapkitResult: var enum_str = data["state"] if enum_str is String: obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING.get(enum_str, IapkitPurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.state = enum_str + else: + obj.state = IapkitPurchaseState.UNKNOWN if data.has("productId") and data["productId"] != null: obj.product_id = data["productId"] if data.has("clientPayload") and data["clientPayload"] != null: if data["clientPayload"] is Dictionary: obj.client_payload = IapkitProductClientPayload.from_dict(data["clientPayload"]) - else: - obj.client_payload = data["clientPayload"] return obj func to_dict() -> Dictionary: @@ -2910,8 +2932,10 @@ class SubscriptionOffer: var enum_str = data["paymentMode"] if enum_str is String: obj.payment_mode = PAYMENT_MODE_FROM_STRING.get(enum_str, PaymentMode.UNKNOWN) - else: + elif enum_str is int: obj.payment_mode = enum_str + else: + obj.payment_mode = PaymentMode.UNKNOWN if data.has("keyIdentifierIOS") and data["keyIdentifierIOS"] != null: obj.key_identifier_ios = data["keyIdentifierIOS"] if data.has("nonceIOS") and data["nonceIOS"] != null: @@ -3009,8 +3033,10 @@ class SubscriptionPeriod: var enum_str = data["unit"] if enum_str is String: obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.get(enum_str, SubscriptionPeriodUnit.UNKNOWN) - else: + elif enum_str is int: obj.unit = enum_str + else: + obj.unit = SubscriptionPeriodUnit.UNKNOWN if data.has("value") and data["value"] != null: obj.value = data["value"] return obj @@ -3029,6 +3055,8 @@ class SubscriptionPeriodValueIOS: var value: int = 0 static func from_dict(data: Dictionary) -> SubscriptionPeriodValueIOS: + if not data.has("unit") or not data["unit"] is String or not SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(data["unit"]): + return null var obj = SubscriptionPeriodValueIOS.new() if data.has("unit") and data["unit"] != null: var enum_str = data["unit"] @@ -3070,8 +3098,10 @@ class SubscriptionPricingTermsIOS: var enum_str = data["billingPlanType"] if enum_str is String: obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.billing_plan_type = enum_str + else: + obj.billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("billingPrice") and data["billingPrice"] != null: obj.billing_price = data["billingPrice"] if data.has("commitmentInfo") and data["commitmentInfo"] != null: @@ -3566,6 +3596,8 @@ class DeveloperBillingOptionParamsAndroid: var external_transaction_token: Variant = null static func from_dict(data: Dictionary) -> DeveloperBillingOptionParamsAndroid: + if not data.has("billingProgram") or not data["billingProgram"] is String or not BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"]): + return null var obj = DeveloperBillingOptionParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3697,8 +3729,8 @@ class InAppMessageParamsAndroid: if data["categories"] is Array: var arr: Array[InAppMessageCategoryAndroid] = [] for item in data["categories"]: - if item is String and IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.has(item): - arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING[item]) + if item is String: + arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.get(item, InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID)) elif item is int: arr.append(item) obj.categories = arr @@ -3904,16 +3936,20 @@ class PurchaseInput: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("quantity") and data["quantity"] != null: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] if enum_str is String: obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.purchase_state = enum_str + else: + obj.purchase_state = PurchaseState.UNKNOWN if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] return obj @@ -4024,8 +4060,6 @@ class RequestPurchaseAndroidProps: if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) - else: - obj.developer_billing_option = data["developerBillingOption"] return obj func to_dict() -> Dictionary: @@ -4251,8 +4285,6 @@ class RequestSubscriptionAndroidProps: if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) - else: - obj.developer_billing_option = data["developerBillingOption"] return obj func to_dict() -> Dictionary: @@ -4336,8 +4368,10 @@ class RequestSubscriptionIosProps: var enum_str = data["billingPlanType"] if enum_str is String: obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.billing_plan_type = enum_str + else: + obj.billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("compactJWS") and data["compactJWS"] != null: obj.compact_jws = data["compactJWS"] if data.has("advancedCommerceData") and data["advancedCommerceData"] != null: @@ -4559,10 +4593,12 @@ class SubscriptionProductReplacementParamsAndroid: obj.old_product_id = data["oldProductId"] if data.has("replacementMode") and data["replacementMode"] != null: var enum_str = data["replacementMode"] - if enum_str is String and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING.has(enum_str): - obj.replacement_mode = SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING[enum_str] - else: + if enum_str is String: + obj.replacement_mode = SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING.get(enum_str, SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE) + elif enum_str is int: obj.replacement_mode = enum_str + else: + obj.replacement_mode = SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE return obj func to_dict() -> Dictionary: 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 295f1e5c5..ea8beda7b 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 @@ -394,7 +394,7 @@ public enum class ErrorCode(val rawValue: String) { "duplicate-purchase" -> ErrorCode.DuplicatePurchase "DUPLICATE_PURCHASE" -> ErrorCode.DuplicatePurchase "DuplicatePurchase" -> ErrorCode.DuplicatePurchase - else -> throw IllegalArgumentException("Unknown ErrorCode value: $value") + else -> ErrorCode.Unknown } } @@ -689,7 +689,7 @@ public enum class IapkitPurchaseState(val rawValue: String) { "UNKNOWN" -> IapkitPurchaseState.Unknown "inauthentic" -> IapkitPurchaseState.Inauthentic "INAUTHENTIC" -> IapkitPurchaseState.Inauthentic - else -> throw IllegalArgumentException("Unknown IapkitPurchaseState value: $value") + else -> IapkitPurchaseState.Unknown } } @@ -738,7 +738,7 @@ public enum class IapStore(val rawValue: String) { "amazon" -> IapStore.Amazon "AMAZON" -> IapStore.Amazon "Amazon" -> IapStore.Amazon - else -> throw IllegalArgumentException("Unknown IapStore value: $value") + else -> IapStore.Unknown } } @@ -766,7 +766,7 @@ public enum class InAppMessageCategoryAndroid(val rawValue: String) { "UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID" -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId "transactional" -> InAppMessageCategoryAndroid.Transactional "TRANSACTIONAL" -> InAppMessageCategoryAndroid.Transactional - else -> throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid value: $value") + else -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } } @@ -837,7 +837,7 @@ public enum class PaymentMode(val rawValue: String) { "unknown" -> PaymentMode.Unknown "UNKNOWN" -> PaymentMode.Unknown "Unknown" -> PaymentMode.Unknown - else -> throw IllegalArgumentException("Unknown PaymentMode value: $value") + else -> PaymentMode.Unknown } } @@ -928,7 +928,7 @@ public enum class ProductStatusAndroid(val rawValue: String) { "NO_OFFERS_AVAILABLE" -> ProductStatusAndroid.NoOffersAvailable "unknown" -> ProductStatusAndroid.Unknown "UNKNOWN" -> ProductStatusAndroid.Unknown - else -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value") + else -> ProductStatusAndroid.Unknown } } @@ -1011,7 +1011,7 @@ public enum class PurchaseState(val rawValue: String) { "unknown" -> PurchaseState.Unknown "UNKNOWN" -> PurchaseState.Unknown "Unknown" -> PurchaseState.Unknown - else -> throw IllegalArgumentException("Unknown PurchaseState value: $value") + else -> PurchaseState.Unknown } } @@ -1091,7 +1091,7 @@ public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) { "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront "UP_FRONT" -> SubscriptionBillingPlanTypeIOS.UpFront "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront - else -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value") + else -> SubscriptionBillingPlanTypeIOS.Unknown } } @@ -1183,7 +1183,7 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { "unknown" -> SubscriptionPeriodUnit.Unknown "UNKNOWN" -> SubscriptionPeriodUnit.Unknown "Unknown" -> SubscriptionPeriodUnit.Unknown - else -> throw IllegalArgumentException("Unknown SubscriptionPeriodUnit value: $value") + else -> SubscriptionPeriodUnit.Unknown } } @@ -1241,7 +1241,7 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { "DEFERRED" -> SubscriptionReplacementModeAndroid.Deferred "keep-existing" -> SubscriptionReplacementModeAndroid.KeepExisting "KEEP_EXISTING" -> SubscriptionReplacementModeAndroid.KeepExisting - else -> throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid value: $value") + else -> SubscriptionReplacementModeAndroid.UnknownReplacementMode } } @@ -1428,7 +1428,7 @@ public data class AdvancedCommerceInfoIOS( displayName = json["displayName"] as? String, estimatedTax = json["estimatedTax"] as? String, items = (json["items"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AdvancedCommerceItemIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for AdvancedCommerceItemIOS") } ?: emptyList(), - period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) }, + period = (json["period"] as? Map)?.let { runCatching { SubscriptionPeriodValueIOS.fromJson(it) }.getOrNull() }, requestReferenceId = json["requestReferenceId"] as? String, taxCode = json["taxCode"] as? String, taxExclusivePrice = json["taxExclusivePrice"] as? String, @@ -2262,7 +2262,7 @@ public data class IapkitProductClientPayload( fun fromJson(json: Map): IapkitProductClientPayload { return IapkitProductClientPayload( body = json["body"] as? String ?: "", - format = (json["format"] as? String)?.let { IapkitClientPayloadFormat.fromJson(it) } ?: IapkitClientPayloadFormat.Toml, + format = (json["format"] as? String)?.let { IapkitClientPayloadFormat.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for IapkitClientPayloadFormat"), updatedAt = (json["updatedAt"] as? Number)?.toDouble() ?: 0.0, version = (json["version"] as? Number)?.toDouble() ?: 0.0, ) @@ -2545,7 +2545,7 @@ public data class ProductAndroid( nameAndroid = json["nameAndroid"] as? String ?: "", platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, price = (json["price"] as? Number)?.toDouble(), - productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { runCatching { ProductStatusAndroid.fromJson(it) }.getOrNull() ?: ProductStatusAndroid.Unknown }, + productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, @@ -2685,7 +2685,7 @@ public data class ProductSubscriptionAndroid( nameAndroid = json["nameAndroid"] as? String ?: "", platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, price = (json["price"] as? Number)?.toDouble(), - productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { runCatching { ProductStatusAndroid.fromJson(it) }.getOrNull() ?: ProductStatusAndroid.Unknown }, + productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(), title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, @@ -2888,11 +2888,11 @@ public data class PurchaseAndroid( packageNameAndroid = json["packageNameAndroid"] as? String, pendingPurchaseUpdateAndroid = (json["pendingPurchaseUpdateAndroid"] as? Map)?.let { PendingPurchaseUpdateAndroid.fromJson(it) }, productId = json["productId"] as? String ?: "", - purchaseState = runCatching { (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } }.getOrNull() ?: PurchaseState.Unknown, + purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Unknown, purchaseToken = json["purchaseToken"] as? String, quantity = (json["quantity"] as? Number)?.toInt() ?: 0, signatureAndroid = json["signatureAndroid"] as? String, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, transactionDate = (json["transactionDate"] as? Number)?.toDouble() ?: 0.0, transactionId = json["transactionId"] as? String, userIdAmazon = json["userIdAmazon"] as? String, @@ -2969,7 +2969,7 @@ public data class PurchaseError( companion object { fun fromJson(json: Map): PurchaseError { return PurchaseError( - code = runCatching { (json["code"] as? String)?.let { ErrorCode.fromJson(it) } }.getOrNull() ?: ErrorCode.Unknown, + code = (json["code"] as? String)?.let { ErrorCode.fromJson(it) } ?: ErrorCode.Unknown, debugMessage = json["debugMessage"] as? String, isEmptyProductList = json["isEmptyProductList"] as? Boolean, message = json["message"] as? String ?: "", @@ -3091,7 +3091,7 @@ public data class PurchaseIOS( advancedCommerceInfoIOS = (json["advancedCommerceInfoIOS"] as? Map)?.let { AdvancedCommerceInfoIOS.fromJson(it) }, appAccountToken = json["appAccountToken"] as? String, appBundleIdIOS = json["appBundleIdIOS"] as? String, - billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown }, + billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }, bundleOriginalTransactionIdIOS = json["bundleOriginalTransactionIdIOS"] as? String, bundleProductIdIOS = json["bundleProductIdIOS"] as? String, bundleSubscriptionGroupIdIOS = json["bundleSubscriptionGroupIdIOS"] as? String, @@ -3113,7 +3113,7 @@ public data class PurchaseIOS( ownershipTypeIOS = json["ownershipTypeIOS"] as? String, previousOriginalTransactionIdIOS = json["previousOriginalTransactionIdIOS"] as? String, productId = json["productId"] as? String ?: "", - purchaseState = runCatching { (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } }.getOrNull() ?: PurchaseState.Unknown, + purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Unknown, purchaseToken = json["purchaseToken"] as? String, quantity = (json["quantity"] as? Number)?.toInt() ?: 0, quantityIOS = (json["quantityIOS"] as? Number)?.toInt(), @@ -3123,7 +3123,7 @@ public data class PurchaseIOS( revocationDateIOS = (json["revocationDateIOS"] as? Number)?.toDouble(), revocationReasonIOS = json["revocationReasonIOS"] as? String, revocationTypeIOS = json["revocationTypeIOS"] as? String, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, storefrontCountryCodeIOS = json["storefrontCountryCodeIOS"] as? String, subscriptionGroupIdIOS = json["subscriptionGroupIdIOS"] as? String, transactionDate = (json["transactionDate"] as? Number)?.toDouble() ?: 0.0, @@ -3239,7 +3239,7 @@ public data class RenewalCommitmentInfoIOS( return RenewalCommitmentInfoIOS( commitmentAutoRenewProductId = json["commitmentAutoRenewProductId"] as? String ?: "", commitmentAutoRenewStatus = json["commitmentAutoRenewStatus"] as? Boolean ?: false, - commitmentRenewalBillingPlanType = runCatching { (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown, + commitmentRenewalBillingPlanType = (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown, commitmentRenewalDate = (json["commitmentRenewalDate"] as? Number)?.toDouble() ?: 0.0, commitmentRenewalPrice = (json["commitmentRenewalPrice"] as? Number)?.toDouble() ?: 0.0, ) @@ -3344,7 +3344,7 @@ public data class RenewalInfoIOS( jsonRepresentation = json["jsonRepresentation"] as? String, pendingUpgradeProductId = json["pendingUpgradeProductId"] as? String, priceIncreaseStatus = json["priceIncreaseStatus"] as? String, - renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown }, + renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }, renewalDate = (json["renewalDate"] as? Number)?.toDouble(), renewalOfferId = json["renewalOfferId"] as? String, renewalOfferType = json["renewalOfferType"] as? String, @@ -3494,9 +3494,9 @@ public data class RequestVerifyPurchaseWithIapkitResult( fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitResult { return RequestVerifyPurchaseWithIapkitResult( isValid = json["isValid"] as? Boolean ?: false, - state = runCatching { (json["state"] as? String)?.let { IapkitPurchaseState.fromJson(it) } }.getOrNull() ?: IapkitPurchaseState.Unknown, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, - clientPayload = (json["clientPayload"] as? Map)?.let { IapkitProductClientPayload.fromJson(it) }, + state = (json["state"] as? String)?.let { IapkitPurchaseState.fromJson(it) } ?: IapkitPurchaseState.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, + clientPayload = (json["clientPayload"] as? Map)?.let { runCatching { IapkitProductClientPayload.fromJson(it) }.getOrNull() }, productId = json["productId"] as? String, environment = json["environment"] as? String, ) @@ -3652,7 +3652,7 @@ public data class SubscriptionOffer( numberOfPeriodsIOS = (json["numberOfPeriodsIOS"] as? Number)?.toInt(), offerTagsAndroid = (json["offerTagsAndroid"] as? List<*>)?.mapNotNull { it as? String }, offerTokenAndroid = json["offerTokenAndroid"] as? String, - paymentMode = (json["paymentMode"] as? String)?.let { runCatching { PaymentMode.fromJson(it) }.getOrNull() ?: PaymentMode.Unknown }, + paymentMode = (json["paymentMode"] as? String)?.let { PaymentMode.fromJson(it) }, period = (json["period"] as? Map)?.let { SubscriptionPeriod.fromJson(it) }, periodCount = (json["periodCount"] as? Number)?.toInt(), price = (json["price"] as? Number)?.toDouble() ?: 0.0, @@ -3705,7 +3705,7 @@ public data class SubscriptionPeriod( companion object { fun fromJson(json: Map): SubscriptionPeriod { return SubscriptionPeriod( - unit = runCatching { (json["unit"] as? String)?.let { SubscriptionPeriodUnit.fromJson(it) } }.getOrNull() ?: SubscriptionPeriodUnit.Unknown, + unit = (json["unit"] as? String)?.let { SubscriptionPeriodUnit.fromJson(it) } ?: SubscriptionPeriodUnit.Unknown, value = (json["value"] as? Number)?.toInt() ?: 0, ) } @@ -3753,7 +3753,7 @@ public data class SubscriptionPricingTermsIOS( return SubscriptionPricingTermsIOS( billingDisplayPrice = json["billingDisplayPrice"] as? String ?: "", billingPeriod = (json["billingPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"), - billingPlanType = runCatching { (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown, + billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown, billingPrice = (json["billingPrice"] as? Number)?.toDouble() ?: 0.0, commitmentInfo = (json["commitmentInfo"] as? Map)?.let { SubscriptionCommitmentInfoIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionCommitmentInfoIOS"), subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, @@ -4241,7 +4241,7 @@ public data class DeveloperBillingOptionParamsAndroid( ) { companion object { fun fromJson(json: Map): DeveloperBillingOptionParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String val launchMode = (json["launchMode"] as? String)?.let { DeveloperBillingLaunchModeAndroid.fromJson(it) } val linkUri = json["linkUri"] as? String @@ -4360,7 +4360,7 @@ public data class InAppMessageParamsAndroid( companion object { fun fromJson(json: Map): InAppMessageParamsAndroid { return InAppMessageParamsAndroid( - categories = (json["categories"] as? List<*>)?.mapNotNull { runCatching { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } }.getOrNull() ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), + categories = (json["categories"] as? List<*>)?.mapNotNull { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), ) } } @@ -4616,7 +4616,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4848,7 +4848,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4933,7 +4933,7 @@ public data class RequestSubscriptionIosProps( val advancedCommerceData = json["advancedCommerceData"] as? String val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean val appAccountToken = json["appAccountToken"] as? String - val billingPlanType = (json["billingPlanType"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown } + val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } val compactJWS = json["compactJWS"] as? String val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } val quantity = (json["quantity"] as? Number)?.toInt() @@ -5205,7 +5205,7 @@ public data class SubscriptionProductReplacementParamsAndroid( companion object { fun fromJson(json: Map): SubscriptionProductReplacementParamsAndroid? { val oldProductId = json["oldProductId"] as? String - val replacementMode = runCatching { (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } }.getOrNull() ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode + val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode if (oldProductId == null) return null return SubscriptionProductReplacementParamsAndroid( oldProductId = oldProductId, diff --git a/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs b/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs index ea18bc480..7189716ad 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs @@ -24,6 +24,67 @@ public sealed record KitApiOptions public HttpClient? HttpClient { get; init; } } +internal static class TolerantJson +{ + public static T? Deserialize(JsonElement element, JsonSerializerOptions options) + where T : class + { + try + { + return element.Deserialize(options); + } + catch (JsonException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + catch (NotSupportedException) + { + return null; + } + } +} + +internal sealed class TolerantJsonObjectConverter : JsonConverter + where T : class +{ + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + return TolerantJson.Deserialize(document.RootElement, options); + } + + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + => JsonSerializer.Serialize(writer, value, options); +} + +internal sealed class TolerantJsonListConverter : JsonConverter> + where T : class +{ + public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + if (document.RootElement.ValueKind != JsonValueKind.Array) + { + throw new JsonException($"Expected an array of {typeof(T).Name} values."); + } + + var values = new List(); + foreach (var element in document.RootElement.EnumerateArray()) + { + var value = TolerantJson.Deserialize(element, options); + if (value is not null) values.Add(value); + } + return values; + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) + => JsonSerializer.Serialize(writer, value, options); +} + public sealed record KitSubscription { [JsonPropertyName("id")] @@ -63,6 +124,7 @@ public sealed record EntitlementsResponse [JsonPropertyName("productIds")] public required IReadOnlyList ProductIds { get; init; } [JsonPropertyName("subscriptions")] + [JsonConverter(typeof(TolerantJsonListConverter))] public required IReadOnlyList Subscriptions { get; init; } } @@ -71,6 +133,7 @@ public sealed record StatusResponse [JsonPropertyName("active")] public required bool Active { get; init; } [JsonPropertyName("subscription")] + [JsonConverter(typeof(TolerantJsonObjectConverter))] public KitSubscription? Subscription { get; init; } } @@ -136,6 +199,7 @@ public sealed record KitProduct [JsonPropertyName("billingPeriod")] public string? BillingPeriod { get; init; } [JsonPropertyName("offers")] + [JsonConverter(typeof(TolerantJsonListConverter))] public IReadOnlyList? Offers { get; init; } [JsonPropertyName("updatedAt")] public required double UpdatedAt { get; init; } @@ -154,6 +218,7 @@ public sealed record KitProductsOptions public sealed record KitProductsResponse { [JsonPropertyName("products")] + [JsonConverter(typeof(TolerantJsonListConverter))] public required IReadOnlyList Products { get; init; } [JsonPropertyName("hasMore")] public bool? HasMore { get; init; } diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index 0c6d8a4c1..36c18b74d 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -607,7 +607,7 @@ public override ErrorCode Read(ref Utf8JsonReader reader, Type typeToConvert, Js { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown ErrorCode value: {raw}"); + return ErrorCode.Unknown; } public override void Write(Utf8JsonWriter writer, ErrorCode value, JsonSerializerOptions options) @@ -617,7 +617,7 @@ public override void Write(Utf8JsonWriter writer, ErrorCode value, JsonSerialize internal static string ToRawString(ErrorCode value) => _toString[value]; internal static ErrorCode FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ErrorCode value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : ErrorCode.Unknown; } public static class ErrorCodeExtensions @@ -1107,7 +1107,7 @@ public override IapkitPurchaseState Read(ref Utf8JsonReader reader, Type typeToC { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown IapkitPurchaseState value: {raw}"); + return IapkitPurchaseState.Unknown; } public override void Write(Utf8JsonWriter writer, IapkitPurchaseState value, JsonSerializerOptions options) @@ -1117,7 +1117,7 @@ public override void Write(Utf8JsonWriter writer, IapkitPurchaseState value, Jso internal static string ToRawString(IapkitPurchaseState value) => _toString[value]; internal static IapkitPurchaseState FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown IapkitPurchaseState value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : IapkitPurchaseState.Unknown; } public static class IapkitPurchaseStateExtensions @@ -1217,7 +1217,7 @@ public override IapStore Read(ref Utf8JsonReader reader, Type typeToConvert, Jso { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown IapStore value: {raw}"); + return IapStore.Unknown; } public override void Write(Utf8JsonWriter writer, IapStore value, JsonSerializerOptions options) @@ -1227,7 +1227,7 @@ public override void Write(Utf8JsonWriter writer, IapStore value, JsonSerializer internal static string ToRawString(IapStore value) => _toString[value]; internal static IapStore FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown IapStore value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : IapStore.Unknown; } public static class IapStoreExtensions @@ -1270,7 +1270,7 @@ public override InAppMessageCategoryAndroid Read(ref Utf8JsonReader reader, Type { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown InAppMessageCategoryAndroid value: {raw}"); + return InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; } public override void Write(Utf8JsonWriter writer, InAppMessageCategoryAndroid value, JsonSerializerOptions options) @@ -1280,7 +1280,7 @@ public override void Write(Utf8JsonWriter writer, InAppMessageCategoryAndroid va internal static string ToRawString(InAppMessageCategoryAndroid value) => _toString[value]; internal static InAppMessageCategoryAndroid FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown InAppMessageCategoryAndroid value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; } public static class InAppMessageCategoryAndroidExtensions @@ -1389,7 +1389,7 @@ public override PaymentMode Read(ref Utf8JsonReader reader, Type typeToConvert, { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown PaymentMode value: {raw}"); + return PaymentMode.Unknown; } public override void Write(Utf8JsonWriter writer, PaymentMode value, JsonSerializerOptions options) @@ -1399,7 +1399,7 @@ public override void Write(Utf8JsonWriter writer, PaymentMode value, JsonSeriali internal static string ToRawString(PaymentMode value) => _toString[value]; internal static PaymentMode FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown PaymentMode value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : PaymentMode.Unknown; } public static class PaymentModeExtensions @@ -1564,7 +1564,7 @@ public override ProductStatusAndroid Read(ref Utf8JsonReader reader, Type typeTo { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown ProductStatusAndroid value: {raw}"); + return ProductStatusAndroid.Unknown; } public override void Write(Utf8JsonWriter writer, ProductStatusAndroid value, JsonSerializerOptions options) @@ -1574,7 +1574,7 @@ public override void Write(Utf8JsonWriter writer, ProductStatusAndroid value, Js internal static string ToRawString(ProductStatusAndroid value) => _toString[value]; internal static ProductStatusAndroid FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ProductStatusAndroid value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : ProductStatusAndroid.Unknown; } public static class ProductStatusAndroidExtensions @@ -1735,7 +1735,7 @@ public override PurchaseState Read(ref Utf8JsonReader reader, Type typeToConvert { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown PurchaseState value: {raw}"); + return PurchaseState.Unknown; } public override void Write(Utf8JsonWriter writer, PurchaseState value, JsonSerializerOptions options) @@ -1745,7 +1745,7 @@ public override void Write(Utf8JsonWriter writer, PurchaseState value, JsonSeria internal static string ToRawString(PurchaseState value) => _toString[value]; internal static PurchaseState FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown PurchaseState value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : PurchaseState.Unknown; } public static class PurchaseStateExtensions @@ -1891,7 +1891,7 @@ public override SubscriptionBillingPlanTypeIOS Read(ref Utf8JsonReader reader, T { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown SubscriptionBillingPlanTypeIOS value: {raw}"); + return SubscriptionBillingPlanTypeIOS.Unknown; } public override void Write(Utf8JsonWriter writer, SubscriptionBillingPlanTypeIOS value, JsonSerializerOptions options) @@ -1901,7 +1901,7 @@ public override void Write(Utf8JsonWriter writer, SubscriptionBillingPlanTypeIOS internal static string ToRawString(SubscriptionBillingPlanTypeIOS value) => _toString[value]; internal static SubscriptionBillingPlanTypeIOS FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionBillingPlanTypeIOS value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : SubscriptionBillingPlanTypeIOS.Unknown; } public static class SubscriptionBillingPlanTypeIOSExtensions @@ -2075,7 +2075,7 @@ public override SubscriptionPeriodUnit Read(ref Utf8JsonReader reader, Type type { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown SubscriptionPeriodUnit value: {raw}"); + return SubscriptionPeriodUnit.Unknown; } public override void Write(Utf8JsonWriter writer, SubscriptionPeriodUnit value, JsonSerializerOptions options) @@ -2085,7 +2085,7 @@ public override void Write(Utf8JsonWriter writer, SubscriptionPeriodUnit value, internal static string ToRawString(SubscriptionPeriodUnit value) => _toString[value]; internal static SubscriptionPeriodUnit FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionPeriodUnit value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : SubscriptionPeriodUnit.Unknown; } public static class SubscriptionPeriodUnitExtensions @@ -2153,7 +2153,7 @@ public override SubscriptionReplacementModeAndroid Read(ref Utf8JsonReader reade { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown SubscriptionReplacementModeAndroid value: {raw}"); + return SubscriptionReplacementModeAndroid.UnknownReplacementMode; } public override void Write(Utf8JsonWriter writer, SubscriptionReplacementModeAndroid value, JsonSerializerOptions options) @@ -2163,7 +2163,7 @@ public override void Write(Utf8JsonWriter writer, SubscriptionReplacementModeAnd internal static string ToRawString(SubscriptionReplacementModeAndroid value) => _toString[value]; internal static SubscriptionReplacementModeAndroid FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionReplacementModeAndroid value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : SubscriptionReplacementModeAndroid.UnknownReplacementMode; } public static class SubscriptionReplacementModeAndroidExtensions @@ -2374,6 +2374,7 @@ public sealed record AdvancedCommerceInfoIOS /// or visionOS 2.4+). /// [JsonPropertyName("period")] + [JsonConverter(typeof(SubscriptionPeriodValueIOSNullableJsonConverter))] public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking [JsonPropertyName("requestReferenceId")] @@ -2839,6 +2840,29 @@ public sealed record IapkitProductClientPayload public required double Version { get; init; } } +public sealed class IapkitProductClientPayloadNullableJsonConverter : JsonConverter +{ + public override IapkitProductClientPayload? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + try + { + return document.RootElement.Deserialize(options); + } + catch (JsonException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, IapkitProductClientPayload? value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} + /// /// Result from showing Play billing in-app messages (Android) /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 @@ -3515,6 +3539,7 @@ public sealed record RequestVerifyPurchaseWithIapkitResult /// Apple or Google receipt is valid, and a payload exists for that product. /// [JsonPropertyName("clientPayload")] + [JsonConverter(typeof(IapkitProductClientPayloadNullableJsonConverter))] public IapkitProductClientPayload? ClientPayload { get; init; } /// /// Available in OpenIAP Spec 3.2.0 / openiap-apple 3.2.0 / openiap-google 3.3.0. @@ -3679,6 +3704,29 @@ public sealed record SubscriptionPeriodValueIOS public required int Value { get; init; } } +public sealed class SubscriptionPeriodValueIOSNullableJsonConverter : JsonConverter +{ + public override SubscriptionPeriodValueIOS? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + try + { + return document.RootElement.Deserialize(options); + } + catch (JsonException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, SubscriptionPeriodValueIOS? value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} + public sealed record SubscriptionPricingTermsIOS { [JsonPropertyName("billingDisplayPrice")] @@ -3936,6 +3984,29 @@ public sealed record DeveloperBillingOptionParamsAndroid public string? ExternalTransactionToken { get; init; } } +public sealed class DeveloperBillingOptionParamsAndroidNullableJsonConverter : JsonConverter +{ + public override DeveloperBillingOptionParamsAndroid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + try + { + return document.RootElement.Deserialize(options); + } + catch (JsonException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, DeveloperBillingOptionParamsAndroid? value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} + public sealed record DiscountOfferInputIOS { /// Discount identifier @@ -4132,6 +4203,7 @@ public sealed record RequestPurchaseAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] + [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -4258,6 +4330,7 @@ public sealed record RequestSubscriptionAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] + [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } diff --git a/libraries/maui-iap/tests/OpenIap.Maui.ContractTests/Program.cs b/libraries/maui-iap/tests/OpenIap.Maui.ContractTests/Program.cs index 159b64218..1159877b3 100644 --- a/libraries/maui-iap/tests/OpenIap.Maui.ContractTests/Program.cs +++ b/libraries/maui-iap/tests/OpenIap.Maui.ContractTests/Program.cs @@ -16,8 +16,10 @@ private static readonly (string Name, Func Run)[] Tests = (nameof(ProductsRequirePlatformForClientPayload), ProductsRequirePlatformForClientPayload), (nameof(ProductsUseEscapedUriAndQuery), ProductsUseEscapedUriAndQuery), (nameof(ProductsDeserializeNestedClientPayload), ProductsDeserializeNestedClientPayload), + (nameof(KitApiDropsUnreadableRows), KitApiDropsUnreadableRows), (nameof(ClientPayloadUsesEscapedUriAndDeserializes), ClientPayloadUsesEscapedUriAndDeserializes), (nameof(GeneratedVerificationResultDeserializesClientPayload), GeneratedVerificationResultDeserializesClientPayload), + (nameof(GeneratedVerificationResultDegradesUnknownValues), GeneratedVerificationResultDegradesUnknownValues), (nameof(GeneratedAmazonVerificationContractRoundTrips), GeneratedAmazonVerificationContractRoundTrips), ]; @@ -175,6 +177,57 @@ private static async Task ClientPayloadUsesEscapedUriAndDeserializes() AssertEqual(1720000000456D, response.ClientPayload.UpdatedAt, "payload updatedAt"); } + private static async Task KitApiDropsUnreadableRows() + { + using var entitlementsHandler = new FakeHttpMessageHandler(""" + { + "userId": "user-1", + "productIds": ["premium.monthly"], + "subscriptions": [ + { + "id": "sub-future", "productId": "future.plan", "platform": "horizon", + "state": "active", "startedAt": 1, "updatedAt": 2, "purchaseToken": "future" + }, + { + "id": "sub-ios", "productId": "premium.monthly", "platform": "ios", + "state": "active", "startedAt": 1, "updatedAt": 2, "purchaseToken": "ios" + } + ] + } + """); + using var entitlementsHttpClient = new HttpClient(entitlementsHandler); + var entitlements = await CreateClient(entitlementsHttpClient) + .EntitlementsAsync("user-1") + .ConfigureAwait(false); + AssertEqual(1, entitlements.Subscriptions.Count, "readable subscription count"); + AssertEqual("sub-ios", entitlements.Subscriptions[0].Id, "readable subscription"); + + using var productsHandler = new FakeHttpMessageHandler(""" + { + "products": [ + { + "productId": "future.product", "platform": "Horizon", "type": "in-app", + "title": "Future", "state": "active", "updatedAt": 1 + }, + { + "productId": "premium.monthly", "platform": "IOS", "type": "subscription", + "title": "Premium", "state": "active", "updatedAt": 2, + "offers": [ + { "id": "broken" }, + { "id": "intro", "kind": "introductory" } + ] + } + ] + } + """); + using var productsHttpClient = new HttpClient(productsHandler); + var products = await CreateClient(productsHttpClient).ProductsAsync().ConfigureAwait(false); + AssertEqual(1, products.Products.Count, "readable product count"); + var offers = AssertNotNull(products.Products[0].Offers, "readable offers"); + AssertEqual(1, offers.Count, "readable offer count"); + AssertEqual("intro", offers[0].Id, "readable offer"); + } + private static Task GeneratedVerificationResultDeserializesClientPayload() { const string json = """ @@ -208,6 +261,31 @@ private static Task GeneratedVerificationResultDeserializesClientPayload() return Task.CompletedTask; } + private static Task GeneratedVerificationResultDegradesUnknownValues() + { + const string json = """ + { + "isValid": true, + "state": "future-state", + "store": "future-store", + "clientPayload": { + "format": "future-format", + "body": "opaque", + "version": 1, + "updatedAt": 2 + } + } + """; + + var result = AssertNotNull( + JsonSerializer.Deserialize(json), + "tolerant generated verification result"); + AssertEqual(IapkitPurchaseState.Unknown, result.State, "unknown purchase state"); + AssertEqual(IapStore.Unknown, result.Store, "unknown store"); + AssertEqual(null, result.ClientPayload, "unknown client payload"); + return Task.CompletedTask; + } + private static Task GeneratedAmazonVerificationContractRoundTrips() { var props = new RequestVerifyPurchaseWithIapkitAmazonProps diff --git a/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs b/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs index 18e0d8cb9..cfbd1607c 100644 --- a/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs +++ b/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs @@ -110,10 +110,10 @@ public void ErrorCode_SerializesToKebabCase() } [Fact] - public void ErrorCode_RejectsUnknownRawValue() + public void ErrorCode_DegradesUnknownRawValue() { - Assert.Throws(() => ErrorCodeExtensions.FromJson("no-such-code")); - Assert.Throws(() => JsonSerializer.Deserialize("\"no-such-code\"")); + Assert.Equal(ErrorCode.Unknown, ErrorCodeExtensions.FromJson("no-such-code")); + Assert.Equal(ErrorCode.Unknown, JsonSerializer.Deserialize("\"no-such-code\"")); } [Theory] diff --git a/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs b/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs index 334bfe1d8..6dcf151fc 100644 --- a/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs +++ b/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs @@ -134,6 +134,46 @@ public async Task Products_DeserializeNestedClientPayload() Assert.Equal(1720000000123D, payload.UpdatedAt); } + [Fact] + public async Task Products_DropUnreadableProductsAndOffersIndependently() + { + using var handler = new RecordingHttpMessageHandler(""" + { + "products": [ + { + "productId": "future.product", + "platform": "Horizon", + "type": "in-app", + "title": "Future Product", + "state": "active", + "updatedAt": 1720000000000 + }, + { + "productId": "premium.monthly", + "platform": "IOS", + "type": "subscription", + "title": "Premium Monthly", + "state": "active", + "updatedAt": 1720000000001, + "offers": [ + { "id": "broken-offer" }, + { "id": "intro", "kind": "introductory", "duration": "P1M" } + ] + } + ] + } + """); + using var httpClient = new HttpClient(handler); + var client = CreateClient(httpClient); + + var response = await client.ProductsAsync(); + + var product = Assert.Single(response.Products); + Assert.Equal("premium.monthly", product.ProductId); + var offer = Assert.Single(Assert.IsAssignableFrom>(product.Offers)); + Assert.Equal("intro", offer.Id); + } + [Fact] public async Task ClientPayload_UsesEscapedUriAndDeserializes() { @@ -241,6 +281,32 @@ public async Task Status_BuildsEscapedUriAndDeserializes() Assert.Null(subscription.ExpiresAt); } + [Fact] + public async Task Status_DropsSubscriptionWithUnknownPlatform() + { + using var handler = new RecordingHttpMessageHandler(""" + { + "active": true, + "subscription": { + "id": "sub-1", + "productId": "premium.monthly", + "platform": "horizon", + "state": "active", + "startedAt": 1719000000000, + "updatedAt": 1720000000000, + "purchaseToken": "token-1" + } + } + """); + using var httpClient = new HttpClient(handler); + var client = CreateClient(httpClient); + + var response = await client.StatusAsync("user-1"); + + Assert.True(response.Active); + Assert.Null(response.Subscription); + } + [Fact] public async Task Entitlements_BuildsUriAndDeserializes() { @@ -264,6 +330,44 @@ public async Task Entitlements_BuildsUriAndDeserializes() Assert.Empty(response.Subscriptions); } + [Fact] + public async Task Entitlements_DropOnlySubscriptionsWithUnknownPlatform() + { + using var handler = new RecordingHttpMessageHandler(""" + { + "userId": "user-1", + "productIds": ["premium.monthly"], + "subscriptions": [ + { + "id": "sub-future", + "productId": "future.plan", + "platform": "horizon", + "state": "active", + "startedAt": 1719000000000, + "updatedAt": 1720000000000, + "purchaseToken": "token-future" + }, + { + "id": "sub-ios", + "productId": "premium.monthly", + "platform": "ios", + "state": "active", + "startedAt": 1719000000001, + "updatedAt": 1720000000001, + "purchaseToken": "token-ios" + } + ] + } + """); + using var httpClient = new HttpClient(handler); + var client = CreateClient(httpClient); + + var response = await client.EntitlementsAsync("user-1"); + + var subscription = Assert.Single(response.Subscriptions); + Assert.Equal("sub-ios", subscription.Id); + } + [Fact] public async Task BindUser_PostsJsonBodyAndDeserializes() { 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 2dbdca7fe..e702b3c99 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 @@ -411,7 +411,7 @@ public enum class ErrorCode(val rawValue: String) { "duplicate-purchase" -> ErrorCode.DuplicatePurchase "DUPLICATE_PURCHASE" -> ErrorCode.DuplicatePurchase "DuplicatePurchase" -> ErrorCode.DuplicatePurchase - else -> throw IllegalArgumentException("Unknown ErrorCode value: $value") + else -> ErrorCode.Unknown } } @@ -722,7 +722,7 @@ public enum class IapkitPurchaseState(val rawValue: String) { "inauthentic" -> IapkitPurchaseState.Inauthentic "INAUTHENTIC" -> IapkitPurchaseState.Inauthentic "Inauthentic" -> IapkitPurchaseState.Inauthentic - else -> throw IllegalArgumentException("Unknown IapkitPurchaseState value: $value") + else -> IapkitPurchaseState.Unknown } } @@ -772,7 +772,7 @@ public enum class IapStore(val rawValue: String) { "amazon" -> IapStore.Amazon "AMAZON" -> IapStore.Amazon "Amazon" -> IapStore.Amazon - else -> throw IllegalArgumentException("Unknown IapStore value: $value") + else -> IapStore.Unknown } } @@ -802,7 +802,7 @@ public enum class InAppMessageCategoryAndroid(val rawValue: String) { "transactional" -> InAppMessageCategoryAndroid.Transactional "TRANSACTIONAL" -> InAppMessageCategoryAndroid.Transactional "Transactional" -> InAppMessageCategoryAndroid.Transactional - else -> throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid value: $value") + else -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } } @@ -875,7 +875,7 @@ public enum class PaymentMode(val rawValue: String) { "unknown" -> PaymentMode.Unknown "UNKNOWN" -> PaymentMode.Unknown "Unknown" -> PaymentMode.Unknown - else -> throw IllegalArgumentException("Unknown PaymentMode value: $value") + else -> PaymentMode.Unknown } } @@ -970,7 +970,7 @@ public enum class ProductStatusAndroid(val rawValue: String) { "unknown" -> ProductStatusAndroid.Unknown "UNKNOWN" -> ProductStatusAndroid.Unknown "Unknown" -> ProductStatusAndroid.Unknown - else -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value") + else -> ProductStatusAndroid.Unknown } } @@ -1053,7 +1053,7 @@ public enum class PurchaseState(val rawValue: String) { "unknown" -> PurchaseState.Unknown "UNKNOWN" -> PurchaseState.Unknown "Unknown" -> PurchaseState.Unknown - else -> throw IllegalArgumentException("Unknown PurchaseState value: $value") + else -> PurchaseState.Unknown } } @@ -1136,7 +1136,7 @@ public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) { "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront "UP_FRONT" -> SubscriptionBillingPlanTypeIOS.UpFront "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront - else -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value") + else -> SubscriptionBillingPlanTypeIOS.Unknown } } @@ -1228,7 +1228,7 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { "unknown" -> SubscriptionPeriodUnit.Unknown "UNKNOWN" -> SubscriptionPeriodUnit.Unknown "Unknown" -> SubscriptionPeriodUnit.Unknown - else -> throw IllegalArgumentException("Unknown SubscriptionPeriodUnit value: $value") + else -> SubscriptionPeriodUnit.Unknown } } @@ -1293,7 +1293,7 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { "keep-existing" -> SubscriptionReplacementModeAndroid.KeepExisting "KEEP_EXISTING" -> SubscriptionReplacementModeAndroid.KeepExisting "KeepExisting" -> SubscriptionReplacementModeAndroid.KeepExisting - else -> throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid value: $value") + else -> SubscriptionReplacementModeAndroid.UnknownReplacementMode } } @@ -1480,7 +1480,7 @@ public data class AdvancedCommerceInfoIOS( displayName = json["displayName"] as? String, estimatedTax = json["estimatedTax"] as? String, items = (json["items"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AdvancedCommerceItemIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for AdvancedCommerceItemIOS") } ?: emptyList(), - period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) }, + period = (json["period"] as? Map)?.let { runCatching { SubscriptionPeriodValueIOS.fromJson(it) }.getOrNull() }, requestReferenceId = json["requestReferenceId"] as? String, taxCode = json["taxCode"] as? String, taxExclusivePrice = json["taxExclusivePrice"] as? String, @@ -2314,7 +2314,7 @@ public data class IapkitProductClientPayload( fun fromJson(json: Map): IapkitProductClientPayload { return IapkitProductClientPayload( body = json["body"] as? String ?: "", - format = (json["format"] as? String)?.let { IapkitClientPayloadFormat.fromJson(it) } ?: IapkitClientPayloadFormat.Toml, + format = (json["format"] as? String)?.let { IapkitClientPayloadFormat.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for IapkitClientPayloadFormat"), updatedAt = (json["updatedAt"] as? Number)?.toDouble() ?: 0.0, version = (json["version"] as? Number)?.toDouble() ?: 0.0, ) @@ -2597,7 +2597,7 @@ public data class ProductAndroid( nameAndroid = json["nameAndroid"] as? String ?: "", platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, price = (json["price"] as? Number)?.toDouble(), - productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { runCatching { ProductStatusAndroid.fromJson(it) }.getOrNull() ?: ProductStatusAndroid.Unknown }, + productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, @@ -2737,7 +2737,7 @@ public data class ProductSubscriptionAndroid( nameAndroid = json["nameAndroid"] as? String ?: "", platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, price = (json["price"] as? Number)?.toDouble(), - productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { runCatching { ProductStatusAndroid.fromJson(it) }.getOrNull() ?: ProductStatusAndroid.Unknown }, + productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(), title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, @@ -2940,11 +2940,11 @@ public data class PurchaseAndroid( packageNameAndroid = json["packageNameAndroid"] as? String, pendingPurchaseUpdateAndroid = (json["pendingPurchaseUpdateAndroid"] as? Map)?.let { PendingPurchaseUpdateAndroid.fromJson(it) }, productId = json["productId"] as? String ?: "", - purchaseState = runCatching { (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } }.getOrNull() ?: PurchaseState.Unknown, + purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Unknown, purchaseToken = json["purchaseToken"] as? String, quantity = (json["quantity"] as? Number)?.toInt() ?: 0, signatureAndroid = json["signatureAndroid"] as? String, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, transactionDate = (json["transactionDate"] as? Number)?.toDouble() ?: 0.0, transactionId = json["transactionId"] as? String, userIdAmazon = json["userIdAmazon"] as? String, @@ -3021,7 +3021,7 @@ public data class PurchaseError( companion object { fun fromJson(json: Map): PurchaseError { return PurchaseError( - code = runCatching { (json["code"] as? String)?.let { ErrorCode.fromJson(it) } }.getOrNull() ?: ErrorCode.Unknown, + code = (json["code"] as? String)?.let { ErrorCode.fromJson(it) } ?: ErrorCode.Unknown, debugMessage = json["debugMessage"] as? String, isEmptyProductList = json["isEmptyProductList"] as? Boolean, message = json["message"] as? String ?: "", @@ -3143,7 +3143,7 @@ public data class PurchaseIOS( advancedCommerceInfoIOS = (json["advancedCommerceInfoIOS"] as? Map)?.let { AdvancedCommerceInfoIOS.fromJson(it) }, appAccountToken = json["appAccountToken"] as? String, appBundleIdIOS = json["appBundleIdIOS"] as? String, - billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown }, + billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }, bundleOriginalTransactionIdIOS = json["bundleOriginalTransactionIdIOS"] as? String, bundleProductIdIOS = json["bundleProductIdIOS"] as? String, bundleSubscriptionGroupIdIOS = json["bundleSubscriptionGroupIdIOS"] as? String, @@ -3165,7 +3165,7 @@ public data class PurchaseIOS( ownershipTypeIOS = json["ownershipTypeIOS"] as? String, previousOriginalTransactionIdIOS = json["previousOriginalTransactionIdIOS"] as? String, productId = json["productId"] as? String ?: "", - purchaseState = runCatching { (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } }.getOrNull() ?: PurchaseState.Unknown, + purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Unknown, purchaseToken = json["purchaseToken"] as? String, quantity = (json["quantity"] as? Number)?.toInt() ?: 0, quantityIOS = (json["quantityIOS"] as? Number)?.toInt(), @@ -3175,7 +3175,7 @@ public data class PurchaseIOS( revocationDateIOS = (json["revocationDateIOS"] as? Number)?.toDouble(), revocationReasonIOS = json["revocationReasonIOS"] as? String, revocationTypeIOS = json["revocationTypeIOS"] as? String, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, storefrontCountryCodeIOS = json["storefrontCountryCodeIOS"] as? String, subscriptionGroupIdIOS = json["subscriptionGroupIdIOS"] as? String, transactionDate = (json["transactionDate"] as? Number)?.toDouble() ?: 0.0, @@ -3291,7 +3291,7 @@ public data class RenewalCommitmentInfoIOS( return RenewalCommitmentInfoIOS( commitmentAutoRenewProductId = json["commitmentAutoRenewProductId"] as? String ?: "", commitmentAutoRenewStatus = json["commitmentAutoRenewStatus"] as? Boolean ?: false, - commitmentRenewalBillingPlanType = runCatching { (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown, + commitmentRenewalBillingPlanType = (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown, commitmentRenewalDate = (json["commitmentRenewalDate"] as? Number)?.toDouble() ?: 0.0, commitmentRenewalPrice = (json["commitmentRenewalPrice"] as? Number)?.toDouble() ?: 0.0, ) @@ -3396,7 +3396,7 @@ public data class RenewalInfoIOS( jsonRepresentation = json["jsonRepresentation"] as? String, pendingUpgradeProductId = json["pendingUpgradeProductId"] as? String, priceIncreaseStatus = json["priceIncreaseStatus"] as? String, - renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown }, + renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }, renewalDate = (json["renewalDate"] as? Number)?.toDouble(), renewalOfferId = json["renewalOfferId"] as? String, renewalOfferType = json["renewalOfferType"] as? String, @@ -3546,9 +3546,9 @@ public data class RequestVerifyPurchaseWithIapkitResult( fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitResult { return RequestVerifyPurchaseWithIapkitResult( isValid = json["isValid"] as? Boolean ?: false, - state = runCatching { (json["state"] as? String)?.let { IapkitPurchaseState.fromJson(it) } }.getOrNull() ?: IapkitPurchaseState.Unknown, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, - clientPayload = (json["clientPayload"] as? Map)?.let { IapkitProductClientPayload.fromJson(it) }, + state = (json["state"] as? String)?.let { IapkitPurchaseState.fromJson(it) } ?: IapkitPurchaseState.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, + clientPayload = (json["clientPayload"] as? Map)?.let { runCatching { IapkitProductClientPayload.fromJson(it) }.getOrNull() }, productId = json["productId"] as? String, environment = json["environment"] as? String, ) @@ -3704,7 +3704,7 @@ public data class SubscriptionOffer( numberOfPeriodsIOS = (json["numberOfPeriodsIOS"] as? Number)?.toInt(), offerTagsAndroid = (json["offerTagsAndroid"] as? List<*>)?.mapNotNull { it as? String }, offerTokenAndroid = json["offerTokenAndroid"] as? String, - paymentMode = (json["paymentMode"] as? String)?.let { runCatching { PaymentMode.fromJson(it) }.getOrNull() ?: PaymentMode.Unknown }, + paymentMode = (json["paymentMode"] as? String)?.let { PaymentMode.fromJson(it) }, period = (json["period"] as? Map)?.let { SubscriptionPeriod.fromJson(it) }, periodCount = (json["periodCount"] as? Number)?.toInt(), price = (json["price"] as? Number)?.toDouble() ?: 0.0, @@ -3757,7 +3757,7 @@ public data class SubscriptionPeriod( companion object { fun fromJson(json: Map): SubscriptionPeriod { return SubscriptionPeriod( - unit = runCatching { (json["unit"] as? String)?.let { SubscriptionPeriodUnit.fromJson(it) } }.getOrNull() ?: SubscriptionPeriodUnit.Unknown, + unit = (json["unit"] as? String)?.let { SubscriptionPeriodUnit.fromJson(it) } ?: SubscriptionPeriodUnit.Unknown, value = (json["value"] as? Number)?.toInt() ?: 0, ) } @@ -3805,7 +3805,7 @@ public data class SubscriptionPricingTermsIOS( return SubscriptionPricingTermsIOS( billingDisplayPrice = json["billingDisplayPrice"] as? String ?: "", billingPeriod = (json["billingPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"), - billingPlanType = runCatching { (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown, + billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown, billingPrice = (json["billingPrice"] as? Number)?.toDouble() ?: 0.0, commitmentInfo = (json["commitmentInfo"] as? Map)?.let { SubscriptionCommitmentInfoIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionCommitmentInfoIOS"), subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, @@ -4293,7 +4293,7 @@ public data class DeveloperBillingOptionParamsAndroid( ) { companion object { fun fromJson(json: Map): DeveloperBillingOptionParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String val launchMode = (json["launchMode"] as? String)?.let { DeveloperBillingLaunchModeAndroid.fromJson(it) } val linkUri = json["linkUri"] as? String @@ -4412,7 +4412,7 @@ public data class InAppMessageParamsAndroid( companion object { fun fromJson(json: Map): InAppMessageParamsAndroid { return InAppMessageParamsAndroid( - categories = (json["categories"] as? List<*>)?.mapNotNull { runCatching { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } }.getOrNull() ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), + categories = (json["categories"] as? List<*>)?.mapNotNull { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), ) } } @@ -4668,7 +4668,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4900,7 +4900,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4985,7 +4985,7 @@ public data class RequestSubscriptionIosProps( val advancedCommerceData = json["advancedCommerceData"] as? String val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean val appAccountToken = json["appAccountToken"] as? String - val billingPlanType = (json["billingPlanType"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown } + val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } val compactJWS = json["compactJWS"] as? String val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } val quantity = (json["quantity"] as? Number)?.toInt() @@ -5257,7 +5257,7 @@ public data class SubscriptionProductReplacementParamsAndroid( companion object { fun fromJson(json: Map): SubscriptionProductReplacementParamsAndroid? { val oldProductId = json["oldProductId"] as? String - val replacementMode = runCatching { (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } }.getOrNull() ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode + val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode if (oldProductId == null) return null return SubscriptionProductReplacementParamsAndroid( oldProductId = oldProductId, diff --git a/packages/gql/codegen/plugins/base-plugin.ts b/packages/gql/codegen/plugins/base-plugin.ts index 3fecfbc93..e701e3887 100644 --- a/packages/gql/codegen/plugins/base-plugin.ts +++ b/packages/gql/codegen/plugins/base-plugin.ts @@ -239,6 +239,30 @@ export abstract class CodegenPlugin { return fields; } + protected enumUnknownValue(irEnum: IREnum): IREnum['values'][number] | null { + return irEnum.values.find((value) => value.name.toLowerCase().startsWith('unknown')) ?? null; + } + + protected typeHasRequiredEnumWithoutUnknown(typeName: string, schema: IRSchema): boolean { + const container = [...schema.objects, ...schema.inputs].find((candidate) => candidate.name === typeName); + return ( + container?.fields.some((field) => { + if (field.type.kind !== 'enum' || field.type.nullable || !field.type.name) return false; + const irEnum = schema.enums.find((candidate) => candidate.name === field.type.name); + return irEnum !== undefined && this.enumUnknownValue(irEnum) === null; + }) ?? false + ); + } + + protected typeNeedsTolerantNullableDecoder(typeName: string, schema: IRSchema): boolean { + if (!this.typeHasRequiredEnumWithoutUnknown(typeName, schema)) return false; + return [...schema.objects, ...schema.inputs].some((container) => + container.fields.some( + (field) => field.type.nullable && ['object', 'input'].includes(field.type.kind) && field.type.name === typeName, + ), + ); + } + /** * Resolve a custom input in canonical contract order. Language plugins own * rendering only; the field set and order live in CUSTOM_INPUT_CONTRACTS. diff --git a/packages/gql/codegen/plugins/csharp.ts b/packages/gql/codegen/plugins/csharp.ts index 1024594bb..9de9612da 100644 --- a/packages/gql/codegen/plugins/csharp.ts +++ b/packages/gql/codegen/plugins/csharp.ts @@ -319,7 +319,12 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(' {'); this.emit(' var raw = reader.GetString();'); this.emit(` if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value;`); - this.emit(` throw new JsonException($"Unknown ${irEnum.name} value: {raw}");`); + const unknownValue = this.enumUnknownValue(irEnum); + if (unknownValue) { + this.emit(` return ${irEnum.name}.${this.enumValueCase(unknownValue.name)};`); + } else { + this.emit(` throw new JsonException($"Unknown ${irEnum.name} value: {raw}");`); + } this.emit(' }'); this.emit(''); this.emit(` public override void Write(Utf8JsonWriter writer, ${irEnum.name} value, JsonSerializerOptions options)`); @@ -329,9 +334,10 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(''); this.emit(` internal static string ToRawString(${irEnum.name} value) => _toString[value];`); this.emit(` internal static ${irEnum.name} FromRawString(string value) =>`); - this.emit( - ` _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ${irEnum.name} value: {value}");`, - ); + const fromRawFallback = unknownValue + ? `${irEnum.name}.${this.enumValueCase(unknownValue.name)}` + : `throw new ArgumentException($"Unknown ${irEnum.name} value: {value}")`; + this.emit(` _fromString.TryGetValue(value, out var v) ? v : ${fromRawFallback};`); this.emit('}'); this.emit(''); @@ -467,6 +473,7 @@ export class CSharpPlugin extends CodegenPlugin { this.emitProperties(sortedFields, this.inheritedUnionFieldNames(irObject)); this.emit('}'); this.emit(''); + this.emitNullableJsonConverter(irObject.name); } private computeBaseTypes(irObject: IRObject): string[] { @@ -512,6 +519,13 @@ export class CSharpPlugin extends CodegenPlugin { const jsonName = field.name; const overrideModifier = inheritedFields.has(field.name) ? 'override ' : ''; this.emit(` [JsonPropertyName("${jsonName}")]`); + if ( + field.type.nullable && + ['object', 'input'].includes(field.type.kind) && + this.typeHasRequiredEnumWithoutUnknown(field.type.name!, this.schema) + ) { + this.emit(` [JsonConverter(typeof(${field.type.name}NullableJsonConverter))]`); + } // Required vs. nullable — non-nullable scalars/objects get the C# // `required` modifier so callers must initialize them; nullable @@ -618,6 +632,38 @@ export class CSharpPlugin extends CodegenPlugin { this.emitProperties(irInput.fields); this.emit('}'); this.emit(''); + this.emitNullableJsonConverter(irInput.name); + } + + private emitNullableJsonConverter(typeName: string): void { + if (!this.typeNeedsTolerantNullableDecoder(typeName, this.schema)) return; + this.emit(`public sealed class ${typeName}NullableJsonConverter : JsonConverter<${typeName}?>`); + this.emit('{'); + this.emit( + ` public override ${typeName}? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)`, + ); + this.emit(' {'); + this.emit(' using var document = JsonDocument.ParseValue(ref reader);'); + this.emit(' try'); + this.emit(' {'); + this.emit(` return document.RootElement.Deserialize<${typeName}>(options);`); + this.emit(' }'); + this.emit(' catch (JsonException)'); + this.emit(' {'); + this.emit(' return null;'); + this.emit(' }'); + this.emit(' catch (InvalidOperationException)'); + this.emit(' {'); + this.emit(' return null;'); + this.emit(' }'); + this.emit(' }'); + this.emit(''); + this.emit( + ` public override void Write(Utf8JsonWriter writer, ${typeName}? value, JsonSerializerOptions options) =>`, + ); + this.emit(' JsonSerializer.Serialize(writer, value, options);'); + this.emit('}'); + this.emit(''); } private generateCustomInput(irInput: IRInput): void { diff --git a/packages/gql/codegen/plugins/dart.ts b/packages/gql/codegen/plugins/dart.ts index 3f8227a4c..b58d93dec 100644 --- a/packages/gql/codegen/plugins/dart.ts +++ b/packages/gql/codegen/plugins/dart.ts @@ -184,7 +184,12 @@ export class DartPlugin extends CodegenPlugin { } this.emit(' }'); - this.emit(` throw ArgumentError('Unknown ${irEnum.name} value: \$value');`); + const unknownValue = this.enumUnknownValue(irEnum); + if (unknownValue) { + this.emit(` return ${irEnum.name}.${this.escapeKeyword(this.enumValueCase(unknownValue.name))};`); + } else { + this.emit(` throw ArgumentError('Unknown ${irEnum.name} value: \$value');`); + } this.emit(' }'); this.emit(''); this.emit(' String toJson() => value;'); @@ -295,6 +300,7 @@ export class DartPlugin extends CodegenPlugin { } this.emit(' );'); this.emit(' }'); + this.emitTolerantFromJsonHelper(irObject.name); // toJson method this.emit(''); @@ -398,6 +404,7 @@ export class DartPlugin extends CodegenPlugin { } this.emit(' );'); this.emit(' }'); + this.emitTolerantFromJsonHelper(irInput.name); // toJson method this.emit(''); @@ -824,6 +831,9 @@ export class DartPlugin extends CodegenPlugin { } if (['object', 'input', 'interface', 'union'].includes(type.kind)) { + if (type.nullable && this.typeHasRequiredEnumWithoutUnknown(type.name!, this.schema)) { + return `${sourceExpr} is Map ? ${type.name}._tryFromJson(${sourceExpr} as Map) : null`; + } return type.nullable ? `${sourceExpr} != null ? ${type.name}.fromJson(${sourceExpr} as Map) : null` : `${type.name}.fromJson(${sourceExpr} as Map)`; @@ -832,6 +842,20 @@ export class DartPlugin extends CodegenPlugin { return sourceExpr; } + private emitTolerantFromJsonHelper(typeName: string): void { + if (!this.typeNeedsTolerantNullableDecoder(typeName, this.schema)) return; + this.emit(''); + this.emit(` static ${typeName}? _tryFromJson(Map json) {`); + this.emit(' try {'); + this.emit(` return ${typeName}.fromJson(json);`); + this.emit(' } on ArgumentError {'); + this.emit(' return null;'); + this.emit(' } on TypeError {'); + this.emit(' return null;'); + this.emit(' }'); + this.emit(' }'); + } + private buildDefaultValueExpression(field: IRField): string | null { if (field.defaultValue === undefined) return null; return this.buildDefaultValueForType(field.type, field.defaultValue); diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts index 4222640ab..1031dab8b 100644 --- a/packages/gql/codegen/plugins/gdscript.ts +++ b/packages/gql/codegen/plugins/gdscript.ts @@ -316,7 +316,7 @@ export class GDScriptPlugin extends CodegenPlugin { private getEnumUnknownFallback(typeName: string): string | null { const irEnum = this.schema.enums.find((e) => e.name === typeName); - const unknown = irEnum?.values.find((value) => value.name.toLowerCase() === 'unknown' || value.rawValue.toLowerCase() === 'unknown'); + const unknown = irEnum ? this.enumUnknownValue(irEnum) : null; return unknown ? `${typeName}.${this.enumValueCase(unknown.name)}` : null; } @@ -328,8 +328,10 @@ export class GDScriptPlugin extends CodegenPlugin { if (fallback) { this.emit(`${indent}if enum_str is String:`); this.emit(`${indent}\t${target} = ${enumReverseLookup}.get(enum_str, ${fallback})`); - this.emit(`${indent}else:`); + this.emit(`${indent}elif enum_str is int:`); this.emit(`${indent}\t${target} = enum_str`); + this.emit(`${indent}else:`); + this.emit(`${indent}\t${target} = ${fallback}`); return; } @@ -403,6 +405,9 @@ export class GDScriptPlugin extends CodegenPlugin { // from_dict method this.emit(''); this.emit(`\tstatic func from_dict(data: Dictionary) -> ${irObject.name}:`); + if (this.typeNeedsTolerantNullableDecoder(irObject.name, this.schema)) { + this.emitRequiredEnumGuards(fields); + } this.emit(`\t\tvar obj = ${irObject.name}.new()`); for (const field of fields) { const fieldName = this.getGdscriptFieldName(field.name, irObject.name); @@ -436,8 +441,10 @@ export class GDScriptPlugin extends CodegenPlugin { const typeName = type.name!; this.emit(`\t\t\tif data["${graphqlName}"] is Dictionary:`); this.emit(`\t\t\t\tobj.${fieldName} = ${typeName}.from_dict(data["${graphqlName}"])`); - this.emit(`\t\t\telse:`); - this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`); + if (!type.nullable || !this.typeHasRequiredEnumWithoutUnknown(typeName, this.schema)) { + this.emit(`\t\t\telse:`); + this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`); + } } else if (type.kind === 'enum') { this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`); } else { @@ -625,6 +632,9 @@ export class GDScriptPlugin extends CodegenPlugin { // from_dict method this.emit(''); this.emit(`\tstatic func from_dict(data: Dictionary) -> ${irInput.name}:`); + if (this.typeNeedsTolerantNullableDecoder(irInput.name, this.schema)) { + this.emitRequiredEnumGuards(fields); + } this.emit(`\t\tvar obj = ${irInput.name}.new()`); for (const field of fields) { const fieldName = this.getGdscriptFieldName(field.name, irInput.name); @@ -646,6 +656,24 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(''); } + private emitRequiredEnumGuards(fields: IRField[]): void { + for (const field of fields) { + if (field.type.kind !== 'enum' || field.type.nullable || !field.type.name) continue; + const irEnum = this.schema.enums.find((candidate) => candidate.name === field.type.name); + if (!irEnum || this.enumUnknownValue(irEnum)) continue; + const source = `data["${field.name}"]`; + const lookup = `${toConstantCase(field.type.name)}_FROM_STRING`; + if (field.defaultValue === undefined) { + this.emit(`\t\tif not data.has("${field.name}") or not ${source} is String or not ${lookup}.has(${source}):`); + } else { + this.emit( + `\t\tif data.has("${field.name}") and ${source} != null and (not ${source} is String or not ${lookup}.has(${source})):`, + ); + } + this.emit('\t\t\treturn null'); + } + } + /** * RequestPurchaseProps is a tagged union even though GraphQL input objects * cannot express one-of semantics. Reject ambiguous or mismatched @@ -739,8 +767,10 @@ export class GDScriptPlugin extends CodegenPlugin { const typeName = type.name!; this.emit(`\t\t\tif data["${graphqlName}"] is Dictionary:`); this.emit(`\t\t\t\tobj.${fieldName} = ${typeName}.from_dict(data["${graphqlName}"])`); - this.emit(`\t\t\telse:`); - this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`); + if (!type.nullable || !this.typeHasRequiredEnumWithoutUnknown(typeName, this.schema)) { + this.emit(`\t\t\telse:`); + this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`); + } } else if (type.kind === 'enum') { this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`); } else { diff --git a/packages/gql/codegen/plugins/kotlin.ts b/packages/gql/codegen/plugins/kotlin.ts index 1d1d94454..7ec0de31f 100644 --- a/packages/gql/codegen/plugins/kotlin.ts +++ b/packages/gql/codegen/plugins/kotlin.ts @@ -227,7 +227,11 @@ export class KotlinPlugin extends CodegenPlugin { } } - this.emit(` else -> throw IllegalArgumentException("Unknown ${irEnum.name} value: $value")`); + const unknownValue = this.enumUnknownValue(irEnum); + const unknownExpression = unknownValue + ? `${irEnum.name}.${this.escapeKeyword(this.enumValueCase(unknownValue.name))}` + : `throw IllegalArgumentException("Unknown ${irEnum.name} value: $value")`; + this.emit(` else -> ${unknownExpression}`); this.emit(' }'); this.emit(' }'); this.emit(''); @@ -326,9 +330,18 @@ export class KotlinPlugin extends CodegenPlugin { this.emit(` fun fromJson(json: Map): ${irObject.name} {`); this.emit(` return ${irObject.name}(`); + const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irObject.name, this.schema); + for (const field of sortedFields) { const propertyName = this.escapeKeyword(this.fieldNameCase(field.name)); - const expression = this.buildFromJsonExpression(field.type, `json["${field.name}"]`); + const expression = this.buildFromJsonExpression( + field.type, + `json["${field.name}"]`, + false, + false, + undefined, + rejectMissingStrictEnums, + ); this.emit(` ${propertyName} = ${expression},`); } @@ -423,8 +436,16 @@ export class KotlinPlugin extends CodegenPlugin { this.emit(' companion object {'); this.emit(` fun fromJson(json: Map): ${irObject.name} {`); this.emit(` return ${irObject.name}(`); + const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irObject.name, this.schema); for (const value of [...primaryFields, ...extraFields]) { - const expression = this.buildFromJsonExpression(value.type, `json["${value.name}"]`); + const expression = this.buildFromJsonExpression( + value.type, + `json["${value.name}"]`, + false, + false, + undefined, + rejectMissingStrictEnums, + ); this.emit(` ${value.name} = ${expression},`); } this.emit(' )'); @@ -485,6 +506,7 @@ export class KotlinPlugin extends CodegenPlugin { // Sort fields alphabetically for Kotlin const sortedFields = [...irInput.fields].sort((a, b) => a.name.localeCompare(b.name)); + const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irInput.name, this.schema); this.generateDocComment(irInput.description); this.generateDeprecationAnnotation(irInput.description); @@ -518,6 +540,7 @@ export class KotlinPlugin extends CodegenPlugin { false, true, this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, ); this.emit(` val ${propertyName} = ${expression}`); } @@ -548,6 +571,7 @@ export class KotlinPlugin extends CodegenPlugin { false, false, this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, ); this.emit(` ${propertyName} = ${expression},`); } @@ -703,6 +727,7 @@ export class KotlinPlugin extends CodegenPlugin { } private generateStandardInput(irInput: IRInput): void { + const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irInput.name, this.schema); this.generateDocComment(irInput.description); this.generateDeprecationAnnotation(irInput.description); this.emit(`public data class ${irInput.name}(`); @@ -735,6 +760,7 @@ export class KotlinPlugin extends CodegenPlugin { false, true, this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, ); this.emit(` val ${propertyName} = ${expression}`); } @@ -765,6 +791,7 @@ export class KotlinPlugin extends CodegenPlugin { false, false, this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, ); this.emit(` ${propertyName} = ${expression},`); } @@ -1039,9 +1066,17 @@ export class KotlinPlugin extends CodegenPlugin { isListElement: boolean = false, forNullableFromJson: boolean = false, defaultExpression?: string | null, + rejectMissingStrictEnums: boolean = false, ): string { if (type.kind === 'list') { - const element = this.buildFromJsonExpression(type.elementType!, 'it', true, forNullableFromJson); + const element = this.buildFromJsonExpression( + type.elementType!, + 'it', + true, + forNullableFromJson, + undefined, + rejectMissingStrictEnums, + ); const mapFn = type.elementType!.nullable ? 'map' : 'mapNotNull'; if (defaultExpression) { return `(${sourceExpr} as? List<*>)?.${mapFn} { ${element} } ?: ${defaultExpression}`; @@ -1088,9 +1123,9 @@ export class KotlinPlugin extends CodegenPlugin { } if (unknownFallback) { if (type.nullable) { - return `(${sourceExpr} as? String)?.let { runCatching { ${type.name}.fromJson(it) }.getOrNull() ?: ${unknownFallback} }`; + return enumRead; } - return `runCatching { ${enumRead} }.getOrNull() ?: ${unknownFallback}`; + return `${enumRead} ?: ${unknownFallback}`; } if (type.nullable) { return enumRead; @@ -1102,15 +1137,19 @@ export class KotlinPlugin extends CodegenPlugin { return `(${sourceExpr} as? String)?.let { ${type.name}.fromJson(it) } ?: ${type.name}.Empty`; } const firstValue = irEnum?.values[0]; - const fallback = firstValue - ? `${type.name}.${this.escapeKeyword(this.enumValueCase(firstValue.name))}` - : `throw IllegalArgumentException("Missing required enum value for ${type.name}")`; + const fallback = + rejectMissingStrictEnums || !firstValue + ? `throw IllegalArgumentException("Missing required enum value for ${type.name}")` + : `${type.name}.${this.escapeKeyword(this.enumValueCase(firstValue.name))}`; return `(${sourceExpr} as? String)?.let { ${type.name}.fromJson(it) } ?: ${fallback}`; } if (['object', 'input', 'interface', 'union'].includes(type.kind)) { const callTarget = type.name!; if (type.nullable || forNullableFromJson) { + if (this.typeHasRequiredEnumWithoutUnknown(callTarget, this.schema)) { + return `(${sourceExpr} as? Map)?.let { runCatching { ${callTarget}.fromJson(it) }.getOrNull() }`; + } return `(${sourceExpr} as? Map)?.let { ${callTarget}.fromJson(it) }`; } // Check if input has required fields (nullable fromJson) @@ -1180,7 +1219,7 @@ export class KotlinPlugin extends CodegenPlugin { private buildUnknownEnumFallbackExpression(type: IRType): string | null { if (type.kind !== 'enum' || !type.name) return null; const irEnum = this.schema.enums.find((e) => e.name === type.name); - const unknownValue = irEnum?.values.find((value) => value.name.toLowerCase().startsWith('unknown')); + const unknownValue = irEnum ? this.enumUnknownValue(irEnum) : null; return unknownValue ? `${type.name}.${this.escapeKeyword(this.enumValueCase(unknownValue.name))}` : null; } diff --git a/packages/gql/src/enum-decoder-compatibility.test.ts b/packages/gql/src/enum-decoder-compatibility.test.ts new file mode 100644 index 000000000..e86d21fb9 --- /dev/null +++ b/packages/gql/src/enum-decoder-compatibility.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import type { IREnum, IRField, IRSchema, IRType } from '../codegen/core/types'; +import { CSharpPlugin } from '../codegen/plugins/csharp'; +import { DartPlugin } from '../codegen/plugins/dart'; +import { GDScriptPlugin } from '../codegen/plugins/gdscript'; +import { KotlinPlugin } from '../codegen/plugins/kotlin'; + +const tolerantStatus: IREnum = { + name: 'TolerantStatus', + isErrorCode: false, + values: [ + { name: 'Unknown', rawValue: 'unknown', legacyAliases: [] }, + { name: 'Active', rawValue: 'active', legacyAliases: [] }, + ], +}; + +const strictFormat: IREnum = { + name: 'StrictFormat', + isErrorCode: false, + values: [ + { name: 'Toml', rawValue: 'toml', legacyAliases: [] }, + { name: 'Json', rawValue: 'json', legacyAliases: [] }, + ], +}; + +function field(name: string, type: IRType): IRField { + return { name, type, isOverride: false }; +} + +function decoderSchema(): IRSchema { + return { + enums: [tolerantStatus, strictFormat], + interfaces: [], + objects: [ + { + name: 'Payload', + fields: [ + field('format', { + kind: 'enum', + name: 'StrictFormat', + nullable: false, + }), + field('body', { kind: 'scalar', name: 'String', nullable: false }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'Envelope', + fields: [ + field('status', { + kind: 'enum', + name: 'TolerantStatus', + nullable: false, + }), + field('payload', { kind: 'object', name: 'Payload', nullable: true }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + ], + inputs: [], + unions: [], + operations: [], + }; +} + +describe('unknown enum decoder compatibility', () => { + it('degrades neutral Kotlin enums and drops an unreadable optional payload', () => { + const output = new KotlinPlugin({ outputPath: 'Types.kt' }).generate(decoderSchema()); + + expect(output).toContain('else -> TolerantStatus.Unknown'); + expect(output).not.toContain('Unknown TolerantStatus value'); + expect(output).toContain('else -> throw IllegalArgumentException("Unknown StrictFormat value: $value")'); + expect(output).toContain( + 'format = (json["format"] as? String)?.let { StrictFormat.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for StrictFormat")', + ); + expect(output).toContain( + 'payload = (json["payload"] as? Map)?.let { runCatching { Payload.fromJson(it) }.getOrNull() }', + ); + }); + + it('degrades neutral Dart enums and drops an unreadable optional payload', () => { + const output = new DartPlugin({ outputPath: 'types.dart' }).generate(decoderSchema()); + + expect(output).toContain('return TolerantStatus.Unknown;'); + expect(output).not.toContain('Unknown TolerantStatus value'); + expect(output).toContain("throw ArgumentError('Unknown StrictFormat value: $value');"); + expect(output).toContain('static Payload? _tryFromJson(Map json)'); + expect(output).toContain('} on ArgumentError {'); + expect(output).toContain('} on TypeError {'); + expect(output).toContain( + "payload: json['payload'] is Map ? Payload._tryFromJson(json['payload'] as Map) : null", + ); + }); + + it('degrades neutral C# enums and drops an unreadable optional payload', () => { + const output = new CSharpPlugin({ outputPath: 'Types.cs' }).generate(decoderSchema()); + + expect(output).toContain('return TolerantStatus.Unknown;'); + expect(output).not.toContain('Unknown TolerantStatus value'); + expect(output).toContain('throw new JsonException($"Unknown StrictFormat value: {raw}");'); + expect(output).toContain('public sealed class PayloadNullableJsonConverter : JsonConverter'); + expect(output).toContain('[JsonConverter(typeof(PayloadNullableJsonConverter))]'); + expect(output).toContain('return document.RootElement.Deserialize(options);'); + expect(output).toContain('catch (JsonException)'); + }); + + it('degrades neutral GDScript enums and drops an unreadable optional payload', () => { + const output = new GDScriptPlugin({ outputPath: 'types.gd' }).generate(decoderSchema()); + + expect(output).toContain('obj.status = TOLERANT_STATUS_FROM_STRING.get(enum_str, TolerantStatus.UNKNOWN)'); + expect(output).toContain('obj.status = TolerantStatus.UNKNOWN'); + expect(output).toContain( + 'if not data.has("format") or not data["format"] is String or not STRICT_FORMAT_FROM_STRING.has(data["format"]):', + ); + expect(output).toContain('\t\t\treturn null'); + expect(output).toContain( + 'if data["payload"] is Dictionary:\n\t\t\t\tobj.payload = Payload.from_dict(data["payload"])\n\t\treturn obj', + ); + }); +}); diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index 0c6d8a4c1..36c18b74d 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -607,7 +607,7 @@ public override ErrorCode Read(ref Utf8JsonReader reader, Type typeToConvert, Js { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown ErrorCode value: {raw}"); + return ErrorCode.Unknown; } public override void Write(Utf8JsonWriter writer, ErrorCode value, JsonSerializerOptions options) @@ -617,7 +617,7 @@ public override void Write(Utf8JsonWriter writer, ErrorCode value, JsonSerialize internal static string ToRawString(ErrorCode value) => _toString[value]; internal static ErrorCode FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ErrorCode value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : ErrorCode.Unknown; } public static class ErrorCodeExtensions @@ -1107,7 +1107,7 @@ public override IapkitPurchaseState Read(ref Utf8JsonReader reader, Type typeToC { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown IapkitPurchaseState value: {raw}"); + return IapkitPurchaseState.Unknown; } public override void Write(Utf8JsonWriter writer, IapkitPurchaseState value, JsonSerializerOptions options) @@ -1117,7 +1117,7 @@ public override void Write(Utf8JsonWriter writer, IapkitPurchaseState value, Jso internal static string ToRawString(IapkitPurchaseState value) => _toString[value]; internal static IapkitPurchaseState FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown IapkitPurchaseState value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : IapkitPurchaseState.Unknown; } public static class IapkitPurchaseStateExtensions @@ -1217,7 +1217,7 @@ public override IapStore Read(ref Utf8JsonReader reader, Type typeToConvert, Jso { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown IapStore value: {raw}"); + return IapStore.Unknown; } public override void Write(Utf8JsonWriter writer, IapStore value, JsonSerializerOptions options) @@ -1227,7 +1227,7 @@ public override void Write(Utf8JsonWriter writer, IapStore value, JsonSerializer internal static string ToRawString(IapStore value) => _toString[value]; internal static IapStore FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown IapStore value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : IapStore.Unknown; } public static class IapStoreExtensions @@ -1270,7 +1270,7 @@ public override InAppMessageCategoryAndroid Read(ref Utf8JsonReader reader, Type { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown InAppMessageCategoryAndroid value: {raw}"); + return InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; } public override void Write(Utf8JsonWriter writer, InAppMessageCategoryAndroid value, JsonSerializerOptions options) @@ -1280,7 +1280,7 @@ public override void Write(Utf8JsonWriter writer, InAppMessageCategoryAndroid va internal static string ToRawString(InAppMessageCategoryAndroid value) => _toString[value]; internal static InAppMessageCategoryAndroid FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown InAppMessageCategoryAndroid value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; } public static class InAppMessageCategoryAndroidExtensions @@ -1389,7 +1389,7 @@ public override PaymentMode Read(ref Utf8JsonReader reader, Type typeToConvert, { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown PaymentMode value: {raw}"); + return PaymentMode.Unknown; } public override void Write(Utf8JsonWriter writer, PaymentMode value, JsonSerializerOptions options) @@ -1399,7 +1399,7 @@ public override void Write(Utf8JsonWriter writer, PaymentMode value, JsonSeriali internal static string ToRawString(PaymentMode value) => _toString[value]; internal static PaymentMode FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown PaymentMode value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : PaymentMode.Unknown; } public static class PaymentModeExtensions @@ -1564,7 +1564,7 @@ public override ProductStatusAndroid Read(ref Utf8JsonReader reader, Type typeTo { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown ProductStatusAndroid value: {raw}"); + return ProductStatusAndroid.Unknown; } public override void Write(Utf8JsonWriter writer, ProductStatusAndroid value, JsonSerializerOptions options) @@ -1574,7 +1574,7 @@ public override void Write(Utf8JsonWriter writer, ProductStatusAndroid value, Js internal static string ToRawString(ProductStatusAndroid value) => _toString[value]; internal static ProductStatusAndroid FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ProductStatusAndroid value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : ProductStatusAndroid.Unknown; } public static class ProductStatusAndroidExtensions @@ -1735,7 +1735,7 @@ public override PurchaseState Read(ref Utf8JsonReader reader, Type typeToConvert { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown PurchaseState value: {raw}"); + return PurchaseState.Unknown; } public override void Write(Utf8JsonWriter writer, PurchaseState value, JsonSerializerOptions options) @@ -1745,7 +1745,7 @@ public override void Write(Utf8JsonWriter writer, PurchaseState value, JsonSeria internal static string ToRawString(PurchaseState value) => _toString[value]; internal static PurchaseState FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown PurchaseState value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : PurchaseState.Unknown; } public static class PurchaseStateExtensions @@ -1891,7 +1891,7 @@ public override SubscriptionBillingPlanTypeIOS Read(ref Utf8JsonReader reader, T { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown SubscriptionBillingPlanTypeIOS value: {raw}"); + return SubscriptionBillingPlanTypeIOS.Unknown; } public override void Write(Utf8JsonWriter writer, SubscriptionBillingPlanTypeIOS value, JsonSerializerOptions options) @@ -1901,7 +1901,7 @@ public override void Write(Utf8JsonWriter writer, SubscriptionBillingPlanTypeIOS internal static string ToRawString(SubscriptionBillingPlanTypeIOS value) => _toString[value]; internal static SubscriptionBillingPlanTypeIOS FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionBillingPlanTypeIOS value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : SubscriptionBillingPlanTypeIOS.Unknown; } public static class SubscriptionBillingPlanTypeIOSExtensions @@ -2075,7 +2075,7 @@ public override SubscriptionPeriodUnit Read(ref Utf8JsonReader reader, Type type { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown SubscriptionPeriodUnit value: {raw}"); + return SubscriptionPeriodUnit.Unknown; } public override void Write(Utf8JsonWriter writer, SubscriptionPeriodUnit value, JsonSerializerOptions options) @@ -2085,7 +2085,7 @@ public override void Write(Utf8JsonWriter writer, SubscriptionPeriodUnit value, internal static string ToRawString(SubscriptionPeriodUnit value) => _toString[value]; internal static SubscriptionPeriodUnit FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionPeriodUnit value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : SubscriptionPeriodUnit.Unknown; } public static class SubscriptionPeriodUnitExtensions @@ -2153,7 +2153,7 @@ public override SubscriptionReplacementModeAndroid Read(ref Utf8JsonReader reade { var raw = reader.GetString(); if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value; - throw new JsonException($"Unknown SubscriptionReplacementModeAndroid value: {raw}"); + return SubscriptionReplacementModeAndroid.UnknownReplacementMode; } public override void Write(Utf8JsonWriter writer, SubscriptionReplacementModeAndroid value, JsonSerializerOptions options) @@ -2163,7 +2163,7 @@ public override void Write(Utf8JsonWriter writer, SubscriptionReplacementModeAnd internal static string ToRawString(SubscriptionReplacementModeAndroid value) => _toString[value]; internal static SubscriptionReplacementModeAndroid FromRawString(string value) => - _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionReplacementModeAndroid value: {value}"); + _fromString.TryGetValue(value, out var v) ? v : SubscriptionReplacementModeAndroid.UnknownReplacementMode; } public static class SubscriptionReplacementModeAndroidExtensions @@ -2374,6 +2374,7 @@ public sealed record AdvancedCommerceInfoIOS /// or visionOS 2.4+). /// [JsonPropertyName("period")] + [JsonConverter(typeof(SubscriptionPeriodValueIOSNullableJsonConverter))] public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking [JsonPropertyName("requestReferenceId")] @@ -2839,6 +2840,29 @@ public sealed record IapkitProductClientPayload public required double Version { get; init; } } +public sealed class IapkitProductClientPayloadNullableJsonConverter : JsonConverter +{ + public override IapkitProductClientPayload? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + try + { + return document.RootElement.Deserialize(options); + } + catch (JsonException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, IapkitProductClientPayload? value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} + /// /// Result from showing Play billing in-app messages (Android) /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 @@ -3515,6 +3539,7 @@ public sealed record RequestVerifyPurchaseWithIapkitResult /// Apple or Google receipt is valid, and a payload exists for that product. /// [JsonPropertyName("clientPayload")] + [JsonConverter(typeof(IapkitProductClientPayloadNullableJsonConverter))] public IapkitProductClientPayload? ClientPayload { get; init; } /// /// Available in OpenIAP Spec 3.2.0 / openiap-apple 3.2.0 / openiap-google 3.3.0. @@ -3679,6 +3704,29 @@ public sealed record SubscriptionPeriodValueIOS public required int Value { get; init; } } +public sealed class SubscriptionPeriodValueIOSNullableJsonConverter : JsonConverter +{ + public override SubscriptionPeriodValueIOS? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + try + { + return document.RootElement.Deserialize(options); + } + catch (JsonException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, SubscriptionPeriodValueIOS? value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} + public sealed record SubscriptionPricingTermsIOS { [JsonPropertyName("billingDisplayPrice")] @@ -3936,6 +3984,29 @@ public sealed record DeveloperBillingOptionParamsAndroid public string? ExternalTransactionToken { get; init; } } +public sealed class DeveloperBillingOptionParamsAndroidNullableJsonConverter : JsonConverter +{ + public override DeveloperBillingOptionParamsAndroid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + try + { + return document.RootElement.Deserialize(options); + } + catch (JsonException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, DeveloperBillingOptionParamsAndroid? value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} + public sealed record DiscountOfferInputIOS { /// Discount identifier @@ -4132,6 +4203,7 @@ public sealed record RequestPurchaseAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] + [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -4258,6 +4330,7 @@ public sealed record RequestSubscriptionAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] + [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 5ed78d22d..f1f7e3463 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -392,7 +392,7 @@ public enum class ErrorCode(val rawValue: String) { "duplicate-purchase" -> ErrorCode.DuplicatePurchase "DUPLICATE_PURCHASE" -> ErrorCode.DuplicatePurchase "DuplicatePurchase" -> ErrorCode.DuplicatePurchase - else -> throw IllegalArgumentException("Unknown ErrorCode value: $value") + else -> ErrorCode.Unknown } } @@ -687,7 +687,7 @@ public enum class IapkitPurchaseState(val rawValue: String) { "UNKNOWN" -> IapkitPurchaseState.Unknown "inauthentic" -> IapkitPurchaseState.Inauthentic "INAUTHENTIC" -> IapkitPurchaseState.Inauthentic - else -> throw IllegalArgumentException("Unknown IapkitPurchaseState value: $value") + else -> IapkitPurchaseState.Unknown } } @@ -736,7 +736,7 @@ public enum class IapStore(val rawValue: String) { "amazon" -> IapStore.Amazon "AMAZON" -> IapStore.Amazon "Amazon" -> IapStore.Amazon - else -> throw IllegalArgumentException("Unknown IapStore value: $value") + else -> IapStore.Unknown } } @@ -764,7 +764,7 @@ public enum class InAppMessageCategoryAndroid(val rawValue: String) { "UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID" -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId "transactional" -> InAppMessageCategoryAndroid.Transactional "TRANSACTIONAL" -> InAppMessageCategoryAndroid.Transactional - else -> throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid value: $value") + else -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } } @@ -835,7 +835,7 @@ public enum class PaymentMode(val rawValue: String) { "unknown" -> PaymentMode.Unknown "UNKNOWN" -> PaymentMode.Unknown "Unknown" -> PaymentMode.Unknown - else -> throw IllegalArgumentException("Unknown PaymentMode value: $value") + else -> PaymentMode.Unknown } } @@ -926,7 +926,7 @@ public enum class ProductStatusAndroid(val rawValue: String) { "NO_OFFERS_AVAILABLE" -> ProductStatusAndroid.NoOffersAvailable "unknown" -> ProductStatusAndroid.Unknown "UNKNOWN" -> ProductStatusAndroid.Unknown - else -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value") + else -> ProductStatusAndroid.Unknown } } @@ -1009,7 +1009,7 @@ public enum class PurchaseState(val rawValue: String) { "unknown" -> PurchaseState.Unknown "UNKNOWN" -> PurchaseState.Unknown "Unknown" -> PurchaseState.Unknown - else -> throw IllegalArgumentException("Unknown PurchaseState value: $value") + else -> PurchaseState.Unknown } } @@ -1089,7 +1089,7 @@ public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) { "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront "UP_FRONT" -> SubscriptionBillingPlanTypeIOS.UpFront "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront - else -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value") + else -> SubscriptionBillingPlanTypeIOS.Unknown } } @@ -1181,7 +1181,7 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { "unknown" -> SubscriptionPeriodUnit.Unknown "UNKNOWN" -> SubscriptionPeriodUnit.Unknown "Unknown" -> SubscriptionPeriodUnit.Unknown - else -> throw IllegalArgumentException("Unknown SubscriptionPeriodUnit value: $value") + else -> SubscriptionPeriodUnit.Unknown } } @@ -1239,7 +1239,7 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { "DEFERRED" -> SubscriptionReplacementModeAndroid.Deferred "keep-existing" -> SubscriptionReplacementModeAndroid.KeepExisting "KEEP_EXISTING" -> SubscriptionReplacementModeAndroid.KeepExisting - else -> throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid value: $value") + else -> SubscriptionReplacementModeAndroid.UnknownReplacementMode } } @@ -1426,7 +1426,7 @@ public data class AdvancedCommerceInfoIOS( displayName = json["displayName"] as? String, estimatedTax = json["estimatedTax"] as? String, items = (json["items"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AdvancedCommerceItemIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for AdvancedCommerceItemIOS") } ?: emptyList(), - period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) }, + period = (json["period"] as? Map)?.let { runCatching { SubscriptionPeriodValueIOS.fromJson(it) }.getOrNull() }, requestReferenceId = json["requestReferenceId"] as? String, taxCode = json["taxCode"] as? String, taxExclusivePrice = json["taxExclusivePrice"] as? String, @@ -2260,7 +2260,7 @@ public data class IapkitProductClientPayload( fun fromJson(json: Map): IapkitProductClientPayload { return IapkitProductClientPayload( body = json["body"] as? String ?: "", - format = (json["format"] as? String)?.let { IapkitClientPayloadFormat.fromJson(it) } ?: IapkitClientPayloadFormat.Toml, + format = (json["format"] as? String)?.let { IapkitClientPayloadFormat.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for IapkitClientPayloadFormat"), updatedAt = (json["updatedAt"] as? Number)?.toDouble() ?: 0.0, version = (json["version"] as? Number)?.toDouble() ?: 0.0, ) @@ -2543,7 +2543,7 @@ public data class ProductAndroid( nameAndroid = json["nameAndroid"] as? String ?: "", platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, price = (json["price"] as? Number)?.toDouble(), - productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { runCatching { ProductStatusAndroid.fromJson(it) }.getOrNull() ?: ProductStatusAndroid.Unknown }, + productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, @@ -2683,7 +2683,7 @@ public data class ProductSubscriptionAndroid( nameAndroid = json["nameAndroid"] as? String ?: "", platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, price = (json["price"] as? Number)?.toDouble(), - productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { runCatching { ProductStatusAndroid.fromJson(it) }.getOrNull() ?: ProductStatusAndroid.Unknown }, + productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(), title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, @@ -2886,11 +2886,11 @@ public data class PurchaseAndroid( packageNameAndroid = json["packageNameAndroid"] as? String, pendingPurchaseUpdateAndroid = (json["pendingPurchaseUpdateAndroid"] as? Map)?.let { PendingPurchaseUpdateAndroid.fromJson(it) }, productId = json["productId"] as? String ?: "", - purchaseState = runCatching { (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } }.getOrNull() ?: PurchaseState.Unknown, + purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Unknown, purchaseToken = json["purchaseToken"] as? String, quantity = (json["quantity"] as? Number)?.toInt() ?: 0, signatureAndroid = json["signatureAndroid"] as? String, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, transactionDate = (json["transactionDate"] as? Number)?.toDouble() ?: 0.0, transactionId = json["transactionId"] as? String, userIdAmazon = json["userIdAmazon"] as? String, @@ -2967,7 +2967,7 @@ public data class PurchaseError( companion object { fun fromJson(json: Map): PurchaseError { return PurchaseError( - code = runCatching { (json["code"] as? String)?.let { ErrorCode.fromJson(it) } }.getOrNull() ?: ErrorCode.Unknown, + code = (json["code"] as? String)?.let { ErrorCode.fromJson(it) } ?: ErrorCode.Unknown, debugMessage = json["debugMessage"] as? String, isEmptyProductList = json["isEmptyProductList"] as? Boolean, message = json["message"] as? String ?: "", @@ -3089,7 +3089,7 @@ public data class PurchaseIOS( advancedCommerceInfoIOS = (json["advancedCommerceInfoIOS"] as? Map)?.let { AdvancedCommerceInfoIOS.fromJson(it) }, appAccountToken = json["appAccountToken"] as? String, appBundleIdIOS = json["appBundleIdIOS"] as? String, - billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown }, + billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }, bundleOriginalTransactionIdIOS = json["bundleOriginalTransactionIdIOS"] as? String, bundleProductIdIOS = json["bundleProductIdIOS"] as? String, bundleSubscriptionGroupIdIOS = json["bundleSubscriptionGroupIdIOS"] as? String, @@ -3111,7 +3111,7 @@ public data class PurchaseIOS( ownershipTypeIOS = json["ownershipTypeIOS"] as? String, previousOriginalTransactionIdIOS = json["previousOriginalTransactionIdIOS"] as? String, productId = json["productId"] as? String ?: "", - purchaseState = runCatching { (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } }.getOrNull() ?: PurchaseState.Unknown, + purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Unknown, purchaseToken = json["purchaseToken"] as? String, quantity = (json["quantity"] as? Number)?.toInt() ?: 0, quantityIOS = (json["quantityIOS"] as? Number)?.toInt(), @@ -3121,7 +3121,7 @@ public data class PurchaseIOS( revocationDateIOS = (json["revocationDateIOS"] as? Number)?.toDouble(), revocationReasonIOS = json["revocationReasonIOS"] as? String, revocationTypeIOS = json["revocationTypeIOS"] as? String, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, storefrontCountryCodeIOS = json["storefrontCountryCodeIOS"] as? String, subscriptionGroupIdIOS = json["subscriptionGroupIdIOS"] as? String, transactionDate = (json["transactionDate"] as? Number)?.toDouble() ?: 0.0, @@ -3237,7 +3237,7 @@ public data class RenewalCommitmentInfoIOS( return RenewalCommitmentInfoIOS( commitmentAutoRenewProductId = json["commitmentAutoRenewProductId"] as? String ?: "", commitmentAutoRenewStatus = json["commitmentAutoRenewStatus"] as? Boolean ?: false, - commitmentRenewalBillingPlanType = runCatching { (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown, + commitmentRenewalBillingPlanType = (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown, commitmentRenewalDate = (json["commitmentRenewalDate"] as? Number)?.toDouble() ?: 0.0, commitmentRenewalPrice = (json["commitmentRenewalPrice"] as? Number)?.toDouble() ?: 0.0, ) @@ -3342,7 +3342,7 @@ public data class RenewalInfoIOS( jsonRepresentation = json["jsonRepresentation"] as? String, pendingUpgradeProductId = json["pendingUpgradeProductId"] as? String, priceIncreaseStatus = json["priceIncreaseStatus"] as? String, - renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown }, + renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }, renewalDate = (json["renewalDate"] as? Number)?.toDouble(), renewalOfferId = json["renewalOfferId"] as? String, renewalOfferType = json["renewalOfferType"] as? String, @@ -3492,9 +3492,9 @@ public data class RequestVerifyPurchaseWithIapkitResult( fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitResult { return RequestVerifyPurchaseWithIapkitResult( isValid = json["isValid"] as? Boolean ?: false, - state = runCatching { (json["state"] as? String)?.let { IapkitPurchaseState.fromJson(it) } }.getOrNull() ?: IapkitPurchaseState.Unknown, - store = runCatching { (json["store"] as? String)?.let { IapStore.fromJson(it) } }.getOrNull() ?: IapStore.Unknown, - clientPayload = (json["clientPayload"] as? Map)?.let { IapkitProductClientPayload.fromJson(it) }, + state = (json["state"] as? String)?.let { IapkitPurchaseState.fromJson(it) } ?: IapkitPurchaseState.Unknown, + store = (json["store"] as? String)?.let { IapStore.fromJson(it) } ?: IapStore.Unknown, + clientPayload = (json["clientPayload"] as? Map)?.let { runCatching { IapkitProductClientPayload.fromJson(it) }.getOrNull() }, productId = json["productId"] as? String, environment = json["environment"] as? String, ) @@ -3650,7 +3650,7 @@ public data class SubscriptionOffer( numberOfPeriodsIOS = (json["numberOfPeriodsIOS"] as? Number)?.toInt(), offerTagsAndroid = (json["offerTagsAndroid"] as? List<*>)?.mapNotNull { it as? String }, offerTokenAndroid = json["offerTokenAndroid"] as? String, - paymentMode = (json["paymentMode"] as? String)?.let { runCatching { PaymentMode.fromJson(it) }.getOrNull() ?: PaymentMode.Unknown }, + paymentMode = (json["paymentMode"] as? String)?.let { PaymentMode.fromJson(it) }, period = (json["period"] as? Map)?.let { SubscriptionPeriod.fromJson(it) }, periodCount = (json["periodCount"] as? Number)?.toInt(), price = (json["price"] as? Number)?.toDouble() ?: 0.0, @@ -3703,7 +3703,7 @@ public data class SubscriptionPeriod( companion object { fun fromJson(json: Map): SubscriptionPeriod { return SubscriptionPeriod( - unit = runCatching { (json["unit"] as? String)?.let { SubscriptionPeriodUnit.fromJson(it) } }.getOrNull() ?: SubscriptionPeriodUnit.Unknown, + unit = (json["unit"] as? String)?.let { SubscriptionPeriodUnit.fromJson(it) } ?: SubscriptionPeriodUnit.Unknown, value = (json["value"] as? Number)?.toInt() ?: 0, ) } @@ -3751,7 +3751,7 @@ public data class SubscriptionPricingTermsIOS( return SubscriptionPricingTermsIOS( billingDisplayPrice = json["billingDisplayPrice"] as? String ?: "", billingPeriod = (json["billingPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"), - billingPlanType = runCatching { (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown, + billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown, billingPrice = (json["billingPrice"] as? Number)?.toDouble() ?: 0.0, commitmentInfo = (json["commitmentInfo"] as? Map)?.let { SubscriptionCommitmentInfoIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionCommitmentInfoIOS"), subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, @@ -4239,7 +4239,7 @@ public data class DeveloperBillingOptionParamsAndroid( ) { companion object { fun fromJson(json: Map): DeveloperBillingOptionParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String val launchMode = (json["launchMode"] as? String)?.let { DeveloperBillingLaunchModeAndroid.fromJson(it) } val linkUri = json["linkUri"] as? String @@ -4358,7 +4358,7 @@ public data class InAppMessageParamsAndroid( companion object { fun fromJson(json: Map): InAppMessageParamsAndroid { return InAppMessageParamsAndroid( - categories = (json["categories"] as? List<*>)?.mapNotNull { runCatching { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } }.getOrNull() ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), + categories = (json["categories"] as? List<*>)?.mapNotNull { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), ) } } @@ -4614,7 +4614,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4846,7 +4846,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4931,7 +4931,7 @@ public data class RequestSubscriptionIosProps( val advancedCommerceData = json["advancedCommerceData"] as? String val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean val appAccountToken = json["appAccountToken"] as? String - val billingPlanType = (json["billingPlanType"] as? String)?.let { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown } + val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } val compactJWS = json["compactJWS"] as? String val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } val quantity = (json["quantity"] as? Number)?.toInt() @@ -5203,7 +5203,7 @@ public data class SubscriptionProductReplacementParamsAndroid( companion object { fun fromJson(json: Map): SubscriptionProductReplacementParamsAndroid? { val oldProductId = json["oldProductId"] as? String - val replacementMode = runCatching { (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } }.getOrNull() ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode + val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode if (oldProductId == null) return null return SubscriptionProductReplacementParamsAndroid( oldProductId = oldProductId, diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 13e69dde8..3cd4c0c61 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -329,7 +329,7 @@ enum ErrorCode { case 'duplicate-purchase': return ErrorCode.DuplicatePurchase; } - throw ArgumentError('Unknown ErrorCode value: $value'); + return ErrorCode.Unknown; } String toJson() => value; @@ -584,7 +584,7 @@ enum IapkitPurchaseState { case 'inauthentic': return IapkitPurchaseState.Inauthentic; } - throw ArgumentError('Unknown IapkitPurchaseState value: $value'); + return IapkitPurchaseState.Unknown; } String toJson() => value; @@ -635,7 +635,7 @@ enum IapStore { case 'amazon': return IapStore.Amazon; } - throw ArgumentError('Unknown IapStore value: $value'); + return IapStore.Unknown; } String toJson() => value; @@ -661,7 +661,7 @@ enum InAppMessageCategoryAndroid { case 'transactional': return InAppMessageCategoryAndroid.Transactional; } - throw ArgumentError('Unknown InAppMessageCategoryAndroid value: $value'); + return InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; } String toJson() => value; @@ -720,7 +720,7 @@ enum PaymentMode { case 'unknown': return PaymentMode.Unknown; } - throw ArgumentError('Unknown PaymentMode value: $value'); + return PaymentMode.Unknown; } String toJson() => value; @@ -806,7 +806,7 @@ enum ProductStatusAndroid { case 'unknown': return ProductStatusAndroid.Unknown; } - throw ArgumentError('Unknown ProductStatusAndroid value: $value'); + return ProductStatusAndroid.Unknown; } String toJson() => value; @@ -886,7 +886,7 @@ enum PurchaseState { case 'unknown': return PurchaseState.Unknown; } - throw ArgumentError('Unknown PurchaseState value: $value'); + return PurchaseState.Unknown; } String toJson() => value; @@ -960,7 +960,7 @@ enum SubscriptionBillingPlanTypeIOS { case 'up-front': return SubscriptionBillingPlanTypeIOS.UpFront; } - throw ArgumentError('Unknown SubscriptionBillingPlanTypeIOS value: $value'); + return SubscriptionBillingPlanTypeIOS.Unknown; } String toJson() => value; @@ -1047,7 +1047,7 @@ enum SubscriptionPeriodUnit { case 'unknown': return SubscriptionPeriodUnit.Unknown; } - throw ArgumentError('Unknown SubscriptionPeriodUnit value: $value'); + return SubscriptionPeriodUnit.Unknown; } String toJson() => value; @@ -1093,7 +1093,7 @@ enum SubscriptionReplacementModeAndroid { case 'keep-existing': return SubscriptionReplacementModeAndroid.KeepExisting; } - throw ArgumentError('Unknown SubscriptionReplacementModeAndroid value: $value'); + return SubscriptionReplacementModeAndroid.UnknownReplacementMode; } String toJson() => value; @@ -1264,7 +1264,7 @@ class AdvancedCommerceInfoIOS { displayName: json['displayName'] as String?, estimatedTax: json['estimatedTax'] as String?, items: (json['items'] as List).map((e) => AdvancedCommerceItemIOS.fromJson(e as Map)).toList(), - period: json['period'] != null ? SubscriptionPeriodValueIOS.fromJson(json['period'] as Map) : null, + period: json['period'] is Map ? SubscriptionPeriodValueIOS._tryFromJson(json['period'] as Map) : null, requestReferenceId: json['requestReferenceId'] as String?, taxCode: json['taxCode'] as String?, taxExclusivePrice: json['taxExclusivePrice'] as String?, @@ -2092,6 +2092,16 @@ class IapkitProductClientPayload { ); } + static IapkitProductClientPayload? _tryFromJson(Map json) { + try { + return IapkitProductClientPayload.fromJson(json); + } on ArgumentError { + return null; + } on TypeError { + return null; + } + } + Map toJson() { return { '__typename': 'IapkitProductClientPayload', @@ -3374,7 +3384,7 @@ class RequestVerifyPurchaseWithIapkitResult { factory RequestVerifyPurchaseWithIapkitResult.fromJson(Map json) { return RequestVerifyPurchaseWithIapkitResult( - clientPayload: json['clientPayload'] != null ? IapkitProductClientPayload.fromJson(json['clientPayload'] as Map) : null, + clientPayload: json['clientPayload'] is Map ? IapkitProductClientPayload._tryFromJson(json['clientPayload'] as Map) : null, environment: json['environment'] as String?, isValid: json['isValid'] as bool, productId: json['productId'] as String?, @@ -3600,6 +3610,16 @@ class SubscriptionPeriodValueIOS { ); } + static SubscriptionPeriodValueIOS? _tryFromJson(Map json) { + try { + return SubscriptionPeriodValueIOS.fromJson(json); + } on ArgumentError { + return null; + } on TypeError { + return null; + } + } + Map toJson() { return { '__typename': 'SubscriptionPeriodValueIOS', @@ -4126,6 +4146,16 @@ class DeveloperBillingOptionParamsAndroid { ); } + static DeveloperBillingOptionParamsAndroid? _tryFromJson(Map json) { + try { + return DeveloperBillingOptionParamsAndroid.fromJson(json); + } on ArgumentError { + return null; + } on TypeError { + return null; + } + } + Map toJson() { return { 'billingProgram': billingProgram.toJson(), @@ -4466,7 +4496,7 @@ class RequestPurchaseAndroidProps { factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, @@ -4669,7 +4699,7 @@ class RequestSubscriptionAndroidProps { factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 097d3c06d..2e5b03b11 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -470,8 +470,6 @@ class AdvancedCommerceInfoIOS: if data.has("period") and data["period"] != null: if data["period"] is Dictionary: obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) - else: - obj.period = data["period"] if data.has("requestReferenceId") and data["requestReferenceId"] != null: obj.request_reference_id = data["requestReferenceId"] if data.has("taxCode") and data["taxCode"] != null: @@ -1259,6 +1257,8 @@ class IapkitProductClientPayload: var updated_at: float = 0.0 static func from_dict(data: Dictionary) -> IapkitProductClientPayload: + if not data.has("format") or not data["format"] is String or not IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING.has(data["format"]): + return null var obj = IapkitProductClientPayload.new() if data.has("format") and data["format"] != null: var enum_str = data["format"] @@ -1522,8 +1522,10 @@ class ProductAndroid: var enum_str = data["productStatusAndroid"] if enum_str is String: obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN) - else: + elif enum_str is int: obj.product_status_android = enum_str + else: + obj.product_status_android = ProductStatusAndroid.UNKNOWN if data.has("discountOffers") and data["discountOffers"] != null: if data["discountOffers"] is Array: var arr: Array[DiscountOffer] = [] @@ -1778,8 +1780,10 @@ class ProductSubscriptionAndroid: var enum_str = data["productStatusAndroid"] if enum_str is String: obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN) - else: + elif enum_str is int: obj.product_status_android = enum_str + else: + obj.product_status_android = ProductStatusAndroid.UNKNOWN if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: if data["subscriptionOffers"] is Array: var arr: Array[SubscriptionOffer] = [] @@ -2098,16 +2102,20 @@ class PurchaseAndroid: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("quantity") and data["quantity"] != null: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] if enum_str is String: obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.purchase_state = enum_str + else: + obj.purchase_state = PurchaseState.UNKNOWN if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2208,8 +2216,10 @@ class PurchaseError: var enum_str = data["code"] if enum_str is String: obj.code = ERROR_CODE_FROM_STRING.get(enum_str, ErrorCode.UNKNOWN) - else: + elif enum_str is int: obj.code = enum_str + else: + obj.code = ErrorCode.UNKNOWN if data.has("message") and data["message"] != null: obj.message = data["message"] if data.has("productId") and data["productId"] != null: @@ -2340,16 +2350,20 @@ class PurchaseIOS: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("quantity") and data["quantity"] != null: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] if enum_str is String: obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.purchase_state = enum_str + else: + obj.purchase_state = PurchaseState.UNKNOWN if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2412,8 +2426,10 @@ class PurchaseIOS: var enum_str = data["billingPlanTypeIOS"] if enum_str is String: obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.billing_plan_type_ios = enum_str + else: + obj.billing_plan_type_ios = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null: if data["commitmentInfoIOS"] is Dictionary: obj.commitment_info_ios = TransactionCommitmentInfoIOS.from_dict(data["commitmentInfoIOS"]) @@ -2590,8 +2606,10 @@ class RenewalCommitmentInfoIOS: var enum_str = data["commitmentRenewalBillingPlanType"] if enum_str is String: obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.commitment_renewal_billing_plan_type = enum_str + else: + obj.commitment_renewal_billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null: obj.commitment_renewal_date = data["commitmentRenewalDate"] if data.has("commitmentRenewalPrice") and data["commitmentRenewalPrice"] != null: @@ -2672,8 +2690,10 @@ class RenewalInfoIOS: var enum_str = data["renewalBillingPlanType"] if enum_str is String: obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.renewal_billing_plan_type = enum_str + else: + obj.renewal_billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("commitmentInfo") and data["commitmentInfo"] != null: if data["commitmentInfo"] is Dictionary: obj.commitment_info = RenewalCommitmentInfoIOS.from_dict(data["commitmentInfo"]) @@ -2772,8 +2792,10 @@ class RequestVerifyPurchaseWithIapkitResult: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("environment") and data["environment"] != null: obj.environment = data["environment"] if data.has("isValid") and data["isValid"] != null: @@ -2782,15 +2804,15 @@ class RequestVerifyPurchaseWithIapkitResult: var enum_str = data["state"] if enum_str is String: obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING.get(enum_str, IapkitPurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.state = enum_str + else: + obj.state = IapkitPurchaseState.UNKNOWN if data.has("productId") and data["productId"] != null: obj.product_id = data["productId"] if data.has("clientPayload") and data["clientPayload"] != null: if data["clientPayload"] is Dictionary: obj.client_payload = IapkitProductClientPayload.from_dict(data["clientPayload"]) - else: - obj.client_payload = data["clientPayload"] return obj func to_dict() -> Dictionary: @@ -2910,8 +2932,10 @@ class SubscriptionOffer: var enum_str = data["paymentMode"] if enum_str is String: obj.payment_mode = PAYMENT_MODE_FROM_STRING.get(enum_str, PaymentMode.UNKNOWN) - else: + elif enum_str is int: obj.payment_mode = enum_str + else: + obj.payment_mode = PaymentMode.UNKNOWN if data.has("keyIdentifierIOS") and data["keyIdentifierIOS"] != null: obj.key_identifier_ios = data["keyIdentifierIOS"] if data.has("nonceIOS") and data["nonceIOS"] != null: @@ -3009,8 +3033,10 @@ class SubscriptionPeriod: var enum_str = data["unit"] if enum_str is String: obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.get(enum_str, SubscriptionPeriodUnit.UNKNOWN) - else: + elif enum_str is int: obj.unit = enum_str + else: + obj.unit = SubscriptionPeriodUnit.UNKNOWN if data.has("value") and data["value"] != null: obj.value = data["value"] return obj @@ -3029,6 +3055,8 @@ class SubscriptionPeriodValueIOS: var value: int = 0 static func from_dict(data: Dictionary) -> SubscriptionPeriodValueIOS: + if not data.has("unit") or not data["unit"] is String or not SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(data["unit"]): + return null var obj = SubscriptionPeriodValueIOS.new() if data.has("unit") and data["unit"] != null: var enum_str = data["unit"] @@ -3070,8 +3098,10 @@ class SubscriptionPricingTermsIOS: var enum_str = data["billingPlanType"] if enum_str is String: obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.billing_plan_type = enum_str + else: + obj.billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("billingPrice") and data["billingPrice"] != null: obj.billing_price = data["billingPrice"] if data.has("commitmentInfo") and data["commitmentInfo"] != null: @@ -3566,6 +3596,8 @@ class DeveloperBillingOptionParamsAndroid: var external_transaction_token: Variant = null static func from_dict(data: Dictionary) -> DeveloperBillingOptionParamsAndroid: + if not data.has("billingProgram") or not data["billingProgram"] is String or not BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"]): + return null var obj = DeveloperBillingOptionParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3697,8 +3729,8 @@ class InAppMessageParamsAndroid: if data["categories"] is Array: var arr: Array[InAppMessageCategoryAndroid] = [] for item in data["categories"]: - if item is String and IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.has(item): - arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING[item]) + if item is String: + arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.get(item, InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID)) elif item is int: arr.append(item) obj.categories = arr @@ -3904,16 +3936,20 @@ class PurchaseInput: var enum_str = data["store"] if enum_str is String: obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - else: + elif enum_str is int: obj.store = enum_str + else: + obj.store = IapStore.UNKNOWN if data.has("quantity") and data["quantity"] != null: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] if enum_str is String: obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - else: + elif enum_str is int: obj.purchase_state = enum_str + else: + obj.purchase_state = PurchaseState.UNKNOWN if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] return obj @@ -4024,8 +4060,6 @@ class RequestPurchaseAndroidProps: if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) - else: - obj.developer_billing_option = data["developerBillingOption"] return obj func to_dict() -> Dictionary: @@ -4251,8 +4285,6 @@ class RequestSubscriptionAndroidProps: if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) - else: - obj.developer_billing_option = data["developerBillingOption"] return obj func to_dict() -> Dictionary: @@ -4336,8 +4368,10 @@ class RequestSubscriptionIosProps: var enum_str = data["billingPlanType"] if enum_str is String: obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + elif enum_str is int: obj.billing_plan_type = enum_str + else: + obj.billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN if data.has("compactJWS") and data["compactJWS"] != null: obj.compact_jws = data["compactJWS"] if data.has("advancedCommerceData") and data["advancedCommerceData"] != null: @@ -4559,10 +4593,12 @@ class SubscriptionProductReplacementParamsAndroid: obj.old_product_id = data["oldProductId"] if data.has("replacementMode") and data["replacementMode"] != null: var enum_str = data["replacementMode"] - if enum_str is String and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING.has(enum_str): - obj.replacement_mode = SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING[enum_str] - else: + if enum_str is String: + obj.replacement_mode = SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING.get(enum_str, SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE) + elif enum_str is int: obj.replacement_mode = enum_str + else: + obj.replacement_mode = SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE return obj func to_dict() -> Dictionary: From ac45ced30b76e0516627e50f5d238404bd7f3980 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 01:04:01 +0900 Subject: [PATCH 02/12] fix: address decoder review findings --- .../flutter_inapp_purchase/lib/types.dart | 14 +- .../Example/tests/test_envelope_parsing.gd | 47 ++ .../Example/tests/test_types_only.gd | 56 +- .../godot-iap/addons/godot-iap/godot_iap.gd | 23 +- libraries/godot-iap/addons/godot-iap/types.gd | 581 ++++++++++++++---- libraries/godot-iap/android/build.gradle.kts | 1 + .../java/dev/hyo/godotiap/GodotIapHelper.kt | 40 +- .../dev/hyo/godotiap/GodotIapHelperTest.kt | 69 +++ .../kotlin/io/github/hyochan/kmpiap/Helper.kt | 2 +- .../SubscriptionReplacementResolutionTest.kt | 36 ++ .../io/github/hyochan/kmpiap/openiap/Types.kt | 72 +-- libraries/maui-iap/src/OpenIap.Maui/KitApi.cs | 16 +- libraries/maui-iap/src/OpenIap.Maui/Types.cs | 25 - .../OpenIap.Maui.Tests/KitApiClientTests.cs | 37 +- .../docs/src/pages/docs/kit-compatibility.tsx | 12 +- .../src/main/java/dev/hyo/openiap/Types.kt | 72 +-- .../SubscriptionReplacementModeAndroidExt.kt | 3 +- .../SubscriptionReplacementModeTest.kt | 46 +- packages/gql/codegen/plugins/base-plugin.ts | 35 +- packages/gql/codegen/plugins/csharp.ts | 56 +- packages/gql/codegen/plugins/dart.ts | 2 +- packages/gql/codegen/plugins/gdscript.ts | 231 +++++-- packages/gql/codegen/plugins/kotlin.ts | 17 +- .../src/enum-decoder-compatibility.test.ts | 248 +++++++- packages/gql/src/generated-gdscript.test.ts | 27 +- packages/gql/src/generated/Types.cs | 25 - packages/gql/src/generated/Types.kt | 72 +-- packages/gql/src/generated/types.dart | 14 +- packages/gql/src/generated/types.gd | 581 ++++++++++++++---- 29 files changed, 1948 insertions(+), 512 deletions(-) diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 3cd4c0c61..a50a2c5f8 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -4146,16 +4146,6 @@ class DeveloperBillingOptionParamsAndroid { ); } - static DeveloperBillingOptionParamsAndroid? _tryFromJson(Map json) { - try { - return DeveloperBillingOptionParamsAndroid.fromJson(json); - } on ArgumentError { - return null; - } on TypeError { - return null; - } - } - Map toJson() { return { 'billingProgram': billingProgram.toJson(), @@ -4496,7 +4486,7 @@ class RequestPurchaseAndroidProps { factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, @@ -4699,7 +4689,7 @@ class RequestSubscriptionAndroidProps { factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, diff --git a/libraries/godot-iap/Example/tests/test_envelope_parsing.gd b/libraries/godot-iap/Example/tests/test_envelope_parsing.gd index ee9f8d3ef..f8c6bc177 100644 --- a/libraries/godot-iap/Example/tests/test_envelope_parsing.gd +++ b/libraries/godot-iap/Example/tests/test_envelope_parsing.gd @@ -182,6 +182,7 @@ func _run_all_tests() -> void: test_request_purchase_unsupported_platform() test_ios_request_purchase_requires_sku() test_android_purchase_uses_canonical_native_wire() + test_android_purchase_rejects_invalid_optional_request_fields() test_removed_purchase_inputs_are_rejected() test_ambiguous_purchase_branches_are_rejected() await test_android_finish_transaction_envelopes() @@ -755,6 +756,51 @@ func test_android_purchase_uses_canonical_native_wire() -> void: _assert_equal(sent.get("offerToken"), "canonical-offer-token", "One-time offerToken should use its canonical key") _assert_false(sent.has("offerTokenArr"), "Canonical calls should never emit legacy offerTokenArr") _assert_false(sent.has("replacementMode"), "Missing replacement options should remain absent") + _assert_false( + sent.has("subscriptionProductReplacementParams"), + "Missing product replacement params should remain absent" + ) + _assert_false( + sent.has("developerBillingOption"), + "Missing developer billing options should remain absent" + ) + _uninstall_fake() + + +func test_android_purchase_rejects_invalid_optional_request_fields() -> void: + var fake = _install_android_fake() + var null_replacement = GodotIapPlugin._request_purchase_raw({ + "type": "subs", + "requestSubscription": {"google": { + "skus": ["subscription"], + "subscriptionProductReplacementParams": null, + }}, + }) + _assert_equal(null_replacement.get("success"), false, "Explicit null replacement params should fail") + _assert_equal(fake.last_method, "", "Null replacement params must not reach the native plugin") + + var null_billing = GodotIapPlugin._request_purchase_raw({ + "type": "in-app", + "requestPurchase": {"google": { + "skus": ["coins"], + "developerBillingOption": null, + }}, + }) + _assert_equal(null_billing.get("success"), false, "Explicit null developer billing options should fail") + _assert_equal(fake.last_method, "", "Null developer billing options must not reach the native plugin") + + var in_app_replacement = GodotIapPlugin._request_purchase_raw({ + "type": "in-app", + "requestPurchase": {"google": { + "skus": ["coins"], + "subscriptionProductReplacementParams": { + "oldProductId": "legacy", + "replacementMode": "without-proration", + }, + }}, + }) + _assert_equal(in_app_replacement.get("success"), false, "In-app replacement params should fail") + _assert_equal(fake.last_method, "", "In-app replacement params must not reach the native plugin") _uninstall_fake() @@ -1074,6 +1120,7 @@ func test_ios_immediate_payload_envelope() -> void: "title": "iOS Product", "description": "A product", "type": "in-app", + "typeIOS": "consumable", "platform": "ios", "displayPrice": "$1.99", "currency": "USD", diff --git a/libraries/godot-iap/Example/tests/test_types_only.gd b/libraries/godot-iap/Example/tests/test_types_only.gd index 86893f9bc..8a6f80d96 100644 --- a/libraries/godot-iap/Example/tests/test_types_only.gd +++ b/libraries/godot-iap/Example/tests/test_types_only.gd @@ -50,6 +50,7 @@ func _run_all_tests() -> void: # Enum tests _test_enums() + _test_enum_list_decoding() # New types tests (v1.3.12) _test_discount_offer() @@ -111,6 +112,7 @@ func _test_product_request() -> void: func _test_product_nested_arrays() -> void: print("Testing nested product arrays...") + var valid_period = {"unit": "month", "value": 1} var offer_data = { "id": "intro", "displayPrice": "Free", @@ -124,11 +126,18 @@ func _test_product_nested_arrays() -> void: "description": "Premium subscription", "type": "subs", "platform": "ios", - "subscriptionOffers": [offer_data, "invalid", null], + "typeIOS": "auto-renewable-subscription", + "subscriptionOffers": [offer_data], "pricingTermsIOS": [{ "billingDisplayPrice": "$9.99", + "billingPeriod": valid_period, "billingPlanType": "monthly", "billingPrice": 9.99, + "commitmentInfo": { + "displayPrice": "$119.88", + "period": valid_period, + "price": 119.88 + }, "subscriptionOffers": [offer_data] }] }) @@ -141,6 +150,34 @@ func _test_product_nested_arrays() -> void: "Nested scalar arrays should remain typed" ) + var invalid_offer_product = Types.ProductIOS.from_dict({ + "type": "subs", + "platform": "ios", + "typeIOS": "auto-renewable-subscription", + "subscriptionOffers": [ + {"id": "future", "displayPrice": "$1.99", "price": 1.99, "type": "future-offer"} + ] + }, false) + _assert_equal(invalid_offer_product, null, "Unreadable non-null offer rows should reject the parent") + + var invalid_pricing_product = Types.ProductIOS.from_dict({ + "type": "subs", + "platform": "ios", + "typeIOS": "auto-renewable-subscription", + "pricingTermsIOS": [{ + "billingDisplayPrice": "$19.99", + "billingPeriod": {"unit": "future-period", "value": 1}, + "billingPlanType": "monthly", + "billingPrice": 19.99, + "commitmentInfo": { + "displayPrice": "$239.88", + "period": valid_period, + "price": 239.88 + } + }] + }, false) + _assert_equal(invalid_pricing_product, null, "Unreadable transitive pricing rows should reject the parent") + var android_product = Types.ProductSubscriptionAndroid.from_dict({ "id": "premium.android", "title": "Premium", @@ -384,6 +421,23 @@ func _test_enums() -> void: _assert_equal(Types.ErrorCode.UNKNOWN, 0, "ErrorCode.UNKNOWN should be 0") +func _test_enum_list_decoding() -> void: + print("Testing enum list decoding...") + var nullable_strict_values: Array[Variant] = [null] + _assert_equal(nullable_strict_values[0], null, "Nullable enum lists should accept unreadable values as null") + var decoded_request_values: Array[Variant] = [] + decoded_request_values.append(null) + nullable_strict_values = decoded_request_values + _assert_equal(nullable_strict_values.size(), 1, "Nullable enum request arrays should remain assignable") + var params = Types.InAppMessageParamsAndroid.from_dict({"categories": [999]}) + _assert_equal(params.categories.size(), 1, "Unknown neutral enum list values should be retained") + _assert_equal( + params.categories[0], + Types.InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID, + "Unknown neutral enum list integers should use the neutral member" + ) + + # ============================================ # DiscountOffer Tests (v1.3.12) # ============================================ diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index 73f25afd6..753fd48df 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -588,10 +588,27 @@ func _request_purchase_raw(args: Dictionary) -> Dictionary: ) var offer_token = google_props.get("offerToken", "") var subscription_offers = google_props.get("subscriptionOffers", []) + var has_replacement_params = google_props.has("subscriptionProductReplacementParams") var replacement_params = google_props.get("subscriptionProductReplacementParams", null) + if has_replacement_params and replacement_params == null: + return _purchase_failure( + "developer-error", + "Invalid request: subscriptionProductReplacementParams must not be null" + ) + if has_replacement_params and purchase_type != "subs": + return _purchase_failure( + "developer-error", + "Invalid request: subscriptionProductReplacementParams requires type `subs`" + ) if typeof(replacement_params) == TYPE_OBJECT and replacement_params.has_method("to_dict"): replacement_params = replacement_params.to_dict() + var has_developer_billing_option = google_props.has("developerBillingOption") var developer_billing_option = google_props.get("developerBillingOption", null) + if has_developer_billing_option and developer_billing_option == null: + return _purchase_failure( + "developer-error", + "Invalid request: developerBillingOption must not be null" + ) if typeof(developer_billing_option) == TYPE_OBJECT and developer_billing_option.has_method("to_dict"): developer_billing_option = developer_billing_option.to_dict() var params = { @@ -603,9 +620,11 @@ func _request_purchase_raw(args: Dictionary) -> Dictionary: "subscriptionOffers": subscription_offers, "purchaseToken": google_props.get("purchaseToken", ""), "originalExternalTransactionId": google_props.get("originalExternalTransactionId", ""), - "subscriptionProductReplacementParams": replacement_params, - "developerBillingOption": developer_billing_option, } + if has_replacement_params: + params["subscriptionProductReplacementParams"] = replacement_params + if has_developer_billing_option: + params["developerBillingOption"] = developer_billing_option if purchase_type == "in-app" and not str(offer_token).is_empty(): params["offerToken"] = str(offer_token) var params_json = JSON.stringify(params) diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 2e5b03b11..5515aaff2 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -466,10 +466,13 @@ class AdvancedCommerceInfoIOS: arr.append(AdvancedCommerceItemIOS.from_dict(item)) elif item is AdvancedCommerceItemIOS: arr.append(item) + else: + push_error("Invalid AdvancedCommerceItemIOS list value for items") + return null obj.items = arr if data.has("period") and data["period"] != null: if data["period"] is Dictionary: - obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) + obj.period = SubscriptionPeriodValueIOS.from_dict_or_null(data["period"]) if data.has("requestReferenceId") and data["requestReferenceId"] != null: obj.request_reference_id = data["requestReferenceId"] if data.has("taxCode") and data["taxCode"] != null: @@ -559,6 +562,9 @@ class AdvancedCommerceItemIOS: arr.append(AdvancedCommerceRefundIOS.from_dict(item)) elif item is AdvancedCommerceRefundIOS: arr.append(item) + else: + push_error("Invalid AdvancedCommerceRefundIOS list value for refunds") + return null obj.refunds = arr if data.has("revocationDate") and data["revocationDate"] != null: obj.revocation_date = data["revocationDate"] @@ -712,7 +718,11 @@ class BillingProgramAvailabilityResultAndroid: ## Whether external-link payment is available for Billing Choice. Populated only for available BILLING_CHOICE results. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. var is_external_link_available: Variant = null - static func from_dict(data: Dictionary) -> BillingProgramAvailabilityResultAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> BillingProgramAvailabilityResultAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid BillingProgramAvailabilityResultAndroid.billingProgram enum value") + return null var obj = BillingProgramAvailabilityResultAndroid.new() if data.has("isAvailable") and data["isAvailable"] != null: obj.is_available = data["isAvailable"] @@ -755,7 +765,11 @@ class BillingProgramReportingDetailsAndroid: ## External transaction token used to report transactions made outside of Google Play Billing. Do not cache it for a later redirect session. For External Offer, the same token may report multiple purchases made during the session that generated it. var external_transaction_token: String = "" - static func from_dict(data: Dictionary) -> BillingProgramReportingDetailsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> BillingProgramReportingDetailsAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid BillingProgramReportingDetailsAndroid.billingProgram enum value") + return null var obj = BillingProgramReportingDetailsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -869,7 +883,7 @@ class DeveloperProvidedBillingDetailsAndroid: ## Products selected for the developer billing flow. var products: Array[DeveloperProvidedBillingProductAndroid] = [] - static func from_dict(data: Dictionary) -> DeveloperProvidedBillingDetailsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> DeveloperProvidedBillingDetailsAndroid: var obj = DeveloperProvidedBillingDetailsAndroid.new() if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: obj.external_transaction_token = data["externalTransactionToken"] @@ -882,9 +896,16 @@ class DeveloperProvidedBillingDetailsAndroid: var arr: Array[DeveloperProvidedBillingProductAndroid] = [] for item in data["products"]: if item is Dictionary: - arr.append(DeveloperProvidedBillingProductAndroid.from_dict(item)) + var decoded_developer_provided_billing_product_android = DeveloperProvidedBillingProductAndroid.from_dict(item, report_errors) + if decoded_developer_provided_billing_product_android == null: + return null + arr.append(decoded_developer_provided_billing_product_android) elif item is DeveloperProvidedBillingProductAndroid: arr.append(item) + else: + if report_errors: + push_error("Invalid DeveloperProvidedBillingProductAndroid list value for products") + return null obj.products = arr return obj @@ -917,7 +938,11 @@ class DeveloperProvidedBillingProductAndroid: ## Subscription offer token, when applicable. var offer_token: Variant = null - static func from_dict(data: Dictionary) -> DeveloperProvidedBillingProductAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> DeveloperProvidedBillingProductAndroid: + if not data.has("type") or not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid DeveloperProvidedBillingProductAndroid.type enum value") + return null var obj = DeveloperProvidedBillingProductAndroid.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1026,7 +1051,11 @@ class DiscountOffer: ## [Android] Purchase option ID for this offer. Used to identify which purchase option the user selected. Available in Google Play Billing Library 8.0+ var purchase_option_id_android: Variant = null - static func from_dict(data: Dictionary) -> DiscountOffer: + static func from_dict(data: Dictionary, report_errors: bool = true) -> DiscountOffer: + if not data.has("type") or not ((data["type"] is String and DISCOUNT_OFFER_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and DISCOUNT_OFFER_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid DiscountOffer.type enum value") + return null var obj = DiscountOffer.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1223,7 +1252,11 @@ class ExternalPurchaseNoticeResultIOS: ## 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. var external_purchase_token: Variant = null - static func from_dict(data: Dictionary) -> ExternalPurchaseNoticeResultIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ExternalPurchaseNoticeResultIOS: + if not data.has("result") or not ((data["result"] is String and EXTERNAL_PURCHASE_NOTICE_ACTION_FROM_STRING.has(data["result"])) or (data["result"] is int and EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES.has(data["result"]))): + if report_errors: + push_error("Invalid ExternalPurchaseNoticeResultIOS.result enum value") + return null var obj = ExternalPurchaseNoticeResultIOS.new() if data.has("result") and data["result"] != null: var enum_str = data["result"] @@ -1256,8 +1289,10 @@ class IapkitProductClientPayload: var version: float = 0.0 var updated_at: float = 0.0 - static func from_dict(data: Dictionary) -> IapkitProductClientPayload: - if not data.has("format") or not data["format"] is String or not IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING.has(data["format"]): + static func from_dict(data: Dictionary, report_errors: bool = true) -> IapkitProductClientPayload: + if not data.has("format") or not ((data["format"] is String and IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING.has(data["format"])) or (data["format"] is int and IAPKIT_CLIENT_PAYLOAD_FORMAT_VALUES.has(data["format"]))): + if report_errors: + push_error("Invalid IapkitProductClientPayload.format enum value") return null var obj = IapkitProductClientPayload.new() if data.has("format") and data["format"] != null: @@ -1274,6 +1309,9 @@ class IapkitProductClientPayload: obj.updated_at = data["updatedAt"] return obj + static func from_dict_or_null(data: Dictionary) -> Variant: + return from_dict(data, false) + func to_dict() -> Dictionary: var dict = {} if IAPKIT_CLIENT_PAYLOAD_FORMAT_VALUES.has(format): @@ -1292,7 +1330,11 @@ class InAppMessageResultAndroid: ## Purchase token returned when a subscription status changed. var purchase_token: Variant = null - static func from_dict(data: Dictionary) -> InAppMessageResultAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> InAppMessageResultAndroid: + if not data.has("responseCode") or not ((data["responseCode"] is String and IN_APP_MESSAGE_RESPONSE_CODE_ANDROID_FROM_STRING.has(data["responseCode"])) or (data["responseCode"] is int and IN_APP_MESSAGE_RESPONSE_CODE_ANDROID_VALUES.has(data["responseCode"]))): + if report_errors: + push_error("Invalid InAppMessageResultAndroid.responseCode enum value") + return null var obj = InAppMessageResultAndroid.new() if data.has("responseCode") and data["responseCode"] != null: var enum_str = data["responseCode"] @@ -1450,6 +1492,9 @@ class PricingPhasesAndroid: arr.append(PricingPhaseAndroid.from_dict(item)) elif item is PricingPhaseAndroid: arr.append(item) + else: + push_error("Invalid PricingPhaseAndroid list value for pricingPhaseList") + return null obj.pricing_phase_list = arr return obj @@ -1486,7 +1531,15 @@ class ProductAndroid: ## Standardized subscription offers. Cross-platform type with Android-specific fields using suffix. @see https://openiap.dev/docs/types/subscription-offer var subscription_offers: Array[SubscriptionOffer] = [] - static func from_dict(data: Dictionary) -> ProductAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductAndroid: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductAndroid.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductAndroid.platform enum value") + return null var obj = ProductAndroid.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1531,18 +1584,32 @@ class ProductAndroid: var arr: Array[DiscountOffer] = [] for item in data["discountOffers"]: if item is Dictionary: - arr.append(DiscountOffer.from_dict(item)) + var decoded_discount_offer = DiscountOffer.from_dict(item, report_errors) + if decoded_discount_offer == null: + return null + arr.append(decoded_discount_offer) elif item is DiscountOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid DiscountOffer list value for discountOffers") + return null obj.discount_offers = arr if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: if data["subscriptionOffers"] is Array: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr return obj @@ -1615,7 +1682,19 @@ class ProductIOS: ## iOS 26.4+ subscription pricing terms, including billing plan metadata for monthly subscriptions with a 12-month commitment. var pricing_terms_ios: Array[SubscriptionPricingTermsIOS] = [] - static func from_dict(data: Dictionary) -> ProductIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductIOS: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductIOS.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductIOS.platform enum value") + return null + if not data.has("typeIOS") or not ((data["typeIOS"] is String and PRODUCT_TYPE_IOS_FROM_STRING.has(data["typeIOS"])) or (data["typeIOS"] is int and PRODUCT_TYPE_IOS_VALUES.has(data["typeIOS"]))): + if report_errors: + push_error("Invalid ProductIOS.typeIOS enum value") + return null var obj = ProductIOS.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1662,18 +1741,32 @@ class ProductIOS: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null: if data["pricingTermsIOS"] is Array: var arr: Array[SubscriptionPricingTermsIOS] = [] for item in data["pricingTermsIOS"]: if item is Dictionary: - arr.append(SubscriptionPricingTermsIOS.from_dict(item)) + var decoded_subscription_pricing_terms_ios = SubscriptionPricingTermsIOS.from_dict(item, report_errors) + if decoded_subscription_pricing_terms_ios == null: + return null + arr.append(decoded_subscription_pricing_terms_ios) elif item is SubscriptionPricingTermsIOS: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionPricingTermsIOS list value for pricingTermsIOS") + return null obj.pricing_terms_ios = arr return obj @@ -1744,7 +1837,15 @@ class ProductSubscriptionAndroid: ## Standardized subscription offers. Cross-platform type with Android-specific fields using suffix. @see https://openiap.dev/docs/types/subscription-offer var subscription_offers: Array[SubscriptionOffer] = [] - static func from_dict(data: Dictionary) -> ProductSubscriptionAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductSubscriptionAndroid: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductSubscriptionAndroid.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductSubscriptionAndroid.platform enum value") + return null var obj = ProductSubscriptionAndroid.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1789,9 +1890,16 @@ class ProductSubscriptionAndroid: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr return obj @@ -1859,13 +1967,29 @@ class ProductSubscriptionIOS: var subscription_group_id_ios: Variant = null var introductory_price_ios: Variant = null var introductory_price_as_amount_ios: Variant = null - var introductory_price_payment_mode_ios: PaymentModeIOS + var introductory_price_payment_mode_ios: PaymentModeIOS = PaymentModeIOS.EMPTY var introductory_price_number_of_periods_ios: Variant = null var introductory_price_subscription_period_ios: Variant = null var subscription_period_number_ios: Variant = null var subscription_period_unit_ios: Variant = null - static func from_dict(data: Dictionary) -> ProductSubscriptionIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductSubscriptionIOS: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.platform enum value") + return null + if not data.has("typeIOS") or not ((data["typeIOS"] is String and PRODUCT_TYPE_IOS_FROM_STRING.has(data["typeIOS"])) or (data["typeIOS"] is int and PRODUCT_TYPE_IOS_VALUES.has(data["typeIOS"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.typeIOS enum value") + return null + if data.has("introductoryPricePaymentModeIOS") and data["introductoryPricePaymentModeIOS"] != null and not ((data["introductoryPricePaymentModeIOS"] is String and PAYMENT_MODE_IOS_FROM_STRING.has(data["introductoryPricePaymentModeIOS"])) or (data["introductoryPricePaymentModeIOS"] is int and PAYMENT_MODE_IOS_VALUES.has(data["introductoryPricePaymentModeIOS"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.introductoryPricePaymentModeIOS enum value") + return null var obj = ProductSubscriptionIOS.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1912,18 +2036,32 @@ class ProductSubscriptionIOS: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null: if data["pricingTermsIOS"] is Array: var arr: Array[SubscriptionPricingTermsIOS] = [] for item in data["pricingTermsIOS"]: if item is Dictionary: - arr.append(SubscriptionPricingTermsIOS.from_dict(item)) + var decoded_subscription_pricing_terms_ios = SubscriptionPricingTermsIOS.from_dict(item, report_errors) + if decoded_subscription_pricing_terms_ios == null: + return null + arr.append(decoded_subscription_pricing_terms_ios) elif item is SubscriptionPricingTermsIOS: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionPricingTermsIOS list value for pricingTermsIOS") + return null obj.pricing_terms_ios = arr if data.has("bundledSubscriptionsIOS") and data["bundledSubscriptionsIOS"] != null: if data["bundledSubscriptionsIOS"] is Array: @@ -1933,6 +2071,9 @@ class ProductSubscriptionIOS: arr.append(BundledSubscriptionIOS.from_dict(item)) elif item is BundledSubscriptionIOS: arr.append(item) + else: + push_error("Invalid BundledSubscriptionIOS list value for bundledSubscriptionsIOS") + return null obj.bundled_subscriptions_ios = arr if data.has("subscriptionGroupIdIOS") and data["subscriptionGroupIdIOS"] != null: obj.subscription_group_id_ios = data["subscriptionGroupIdIOS"] @@ -1944,24 +2085,36 @@ class ProductSubscriptionIOS: var enum_str = data["introductoryPricePaymentModeIOS"] if enum_str is String and PAYMENT_MODE_IOS_FROM_STRING.has(enum_str): obj.introductory_price_payment_mode_ios = PAYMENT_MODE_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and PAYMENT_MODE_IOS_VALUES.has(enum_str): obj.introductory_price_payment_mode_ios = enum_str + elif not enum_str is String: + obj.introductory_price_payment_mode_ios = PaymentModeIOS.EMPTY + else: + push_error("Unknown PaymentModeIOS value: %s" % enum_str) if data.has("introductoryPriceNumberOfPeriodsIOS") and data["introductoryPriceNumberOfPeriodsIOS"] != null: obj.introductory_price_number_of_periods_ios = data["introductoryPriceNumberOfPeriodsIOS"] if data.has("introductoryPriceSubscriptionPeriodIOS") and data["introductoryPriceSubscriptionPeriodIOS"] != null: var enum_str = data["introductoryPriceSubscriptionPeriodIOS"] if enum_str is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(enum_str): obj.introductory_price_subscription_period_ios = SUBSCRIPTION_PERIOD_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(enum_str): obj.introductory_price_subscription_period_ios = enum_str + elif not enum_str is String: + obj.introductory_price_subscription_period_ios = SubscriptionPeriodIOS.EMPTY + else: + push_error("Unknown SubscriptionPeriodIOS value: %s" % enum_str) if data.has("subscriptionPeriodNumberIOS") and data["subscriptionPeriodNumberIOS"] != null: obj.subscription_period_number_ios = data["subscriptionPeriodNumberIOS"] if data.has("subscriptionPeriodUnitIOS") and data["subscriptionPeriodUnitIOS"] != null: var enum_str = data["subscriptionPeriodUnitIOS"] if enum_str is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(enum_str): obj.subscription_period_unit_ios = SUBSCRIPTION_PERIOD_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(enum_str): obj.subscription_period_unit_ios = enum_str + elif not enum_str is String: + obj.subscription_period_unit_ios = SubscriptionPeriodIOS.EMPTY + else: + push_error("Unknown SubscriptionPeriodIOS value: %s" % enum_str) return obj func to_dict() -> Dictionary: @@ -2057,9 +2210,9 @@ class PurchaseAndroid: var transaction_date: float = 0.0 var purchase_token: Variant = null ## Store where purchase was made - var store: IapStore + var store: IapStore = IapStore.UNKNOWN var quantity: int = 0 - var purchase_state: PurchaseState + var purchase_state: PurchaseState = PurchaseState.UNKNOWN var is_auto_renewing: bool = false var current_plan_id: Variant = null var data_android: Variant = null @@ -2200,7 +2353,7 @@ class PurchaseAndroid: return dict class PurchaseError: - var code: ErrorCode + var code: ErrorCode = ErrorCode.UNKNOWN var message: String = "" var product_id: Variant = null var debug_message: Variant = null @@ -2280,9 +2433,9 @@ class PurchaseIOS: var transaction_date: float = 0.0 var purchase_token: Variant = null ## Store where purchase was made - var store: IapStore + var store: IapStore = IapStore.UNKNOWN var quantity: int = 0 - var purchase_state: PurchaseState + var purchase_state: PurchaseState = PurchaseState.UNKNOWN var is_auto_renewing: bool = false var current_plan_id: Variant = null var transaction_id: String = "" @@ -2592,7 +2745,7 @@ class RefundResultIOS: class RenewalCommitmentInfoIOS: var commitment_auto_renew_product_id: String = "" var commitment_auto_renew_status: bool = false - var commitment_renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS + var commitment_renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS = SubscriptionBillingPlanTypeIOS.UNKNOWN var commitment_renewal_date: float = 0.0 var commitment_renewal_price: float = 0.0 @@ -2774,13 +2927,13 @@ class RentalDetailsAndroid: return dict class RequestVerifyPurchaseWithIapkitResult: - var store: IapStore + var store: IapStore = IapStore.UNKNOWN ## Available in OpenIAP Spec 3.2.0 / openiap-apple 3.2.0 / openiap-google 3.3.0. Amazon RVS environment selected by IAPKit. Present as `Sandbox` or `Production` on handled Amazon verification results. Deliberately String, not an enum: the value space belongs to IAPKit and the stores behind it, and Apple's App Store Server alone also names `Xcode` and `LocalTesting`. SDKs must forward this value opaquely. Never reject a verification because the environment is unrecognised — that fails a purchase the store already confirmed. var environment: Variant = null ## True when the purchase is valid and actionable. Only entitled, pending-acknowledgment, or ready-to-consume return true. Callers must still match productId and use the platform plus app-owned product type to choose the fulfillment path. var is_valid: bool = false ## The current state of the purchase. - var state: IapkitPurchaseState + var state: IapkitPurchaseState = IapkitPurchaseState.UNKNOWN ## Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. Store-verified product identifier when the provider returns one. var product_id: Variant = null ## Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. Public product payload when includeClientPayload was requested, the Apple or Google receipt is valid, and a payload exists for that product. @@ -2812,7 +2965,7 @@ class RequestVerifyPurchaseWithIapkitResult: obj.product_id = data["productId"] if data.has("clientPayload") and data["clientPayload"] != null: if data["clientPayload"] is Dictionary: - obj.client_payload = IapkitProductClientPayload.from_dict(data["clientPayload"]) + obj.client_payload = IapkitProductClientPayload.from_dict_or_null(data["clientPayload"]) return obj func to_dict() -> Dictionary: @@ -2841,15 +2994,26 @@ class SubscriptionCommitmentInfoIOS: var period: SubscriptionPeriodValueIOS var price: float = 0.0 - static func from_dict(data: Dictionary) -> SubscriptionCommitmentInfoIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionCommitmentInfoIOS: var obj = SubscriptionCommitmentInfoIOS.new() if data.has("displayPrice") and data["displayPrice"] != null: obj.display_price = data["displayPrice"] if data.has("period") and data["period"] != null: if data["period"] is Dictionary: - obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) - else: - obj.period = data["period"] + var decoded_period = SubscriptionPeriodValueIOS.from_dict(data["period"], report_errors) + if decoded_period == null: + if report_errors: + push_error("Invalid required SubscriptionPeriodValueIOS value for period") + return null + obj.period = decoded_period + else: + if report_errors: + push_error("Expected period to be a SubscriptionPeriodValueIOS dictionary") + return null + else: + if report_errors: + push_error("Missing required period value") + return null if data.has("price") and data["price"] != null: obj.price = data["price"] return obj @@ -2905,7 +3069,11 @@ class SubscriptionOffer: ## [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+ var installment_plan_details_android: InstallmentPlanDetailsAndroid - static func from_dict(data: Dictionary) -> SubscriptionOffer: + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionOffer: + if not data.has("type") or not ((data["type"] is String and DISCOUNT_OFFER_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and DISCOUNT_OFFER_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid SubscriptionOffer.type enum value") + return null var obj = SubscriptionOffer.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -3023,7 +3191,7 @@ class SubscriptionOffer: ## Subscription period value combining unit and count. class SubscriptionPeriod: ## The period unit (day, week, month, year) - var unit: SubscriptionPeriodUnit + var unit: SubscriptionPeriodUnit = SubscriptionPeriodUnit.UNKNOWN ## The number of units (e.g., 1 for monthly, 3 for quarterly) var value: int = 0 @@ -3051,23 +3219,32 @@ class SubscriptionPeriod: return dict class SubscriptionPeriodValueIOS: - var unit: SubscriptionPeriodIOS + var unit: SubscriptionPeriodIOS = SubscriptionPeriodIOS.EMPTY var value: int = 0 - static func from_dict(data: Dictionary) -> SubscriptionPeriodValueIOS: - if not data.has("unit") or not data["unit"] is String or not SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(data["unit"]): + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionPeriodValueIOS: + if data.has("unit") and data["unit"] != null and not ((data["unit"] is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(data["unit"])) or (data["unit"] is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(data["unit"]))): + if report_errors: + push_error("Invalid SubscriptionPeriodValueIOS.unit enum value") return null var obj = SubscriptionPeriodValueIOS.new() if data.has("unit") and data["unit"] != null: var enum_str = data["unit"] if enum_str is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(enum_str): obj.unit = SUBSCRIPTION_PERIOD_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(enum_str): obj.unit = enum_str + elif not enum_str is String: + obj.unit = SubscriptionPeriodIOS.EMPTY + else: + push_error("Unknown SubscriptionPeriodIOS value: %s" % enum_str) if data.has("value") and data["value"] != null: obj.value = data["value"] return obj + static func from_dict_or_null(data: Dictionary) -> Variant: + return from_dict(data, false) + func to_dict() -> Dictionary: var dict = {} if SUBSCRIPTION_PERIOD_IOS_VALUES.has(unit): @@ -3080,20 +3257,31 @@ class SubscriptionPeriodValueIOS: class SubscriptionPricingTermsIOS: var billing_display_price: String = "" var billing_period: SubscriptionPeriodValueIOS - var billing_plan_type: SubscriptionBillingPlanTypeIOS + var billing_plan_type: SubscriptionBillingPlanTypeIOS = SubscriptionBillingPlanTypeIOS.UNKNOWN var billing_price: float = 0.0 var commitment_info: SubscriptionCommitmentInfoIOS var subscription_offers: Array[SubscriptionOffer] = [] - static func from_dict(data: Dictionary) -> SubscriptionPricingTermsIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionPricingTermsIOS: var obj = SubscriptionPricingTermsIOS.new() if data.has("billingDisplayPrice") and data["billingDisplayPrice"] != null: obj.billing_display_price = data["billingDisplayPrice"] if data.has("billingPeriod") and data["billingPeriod"] != null: if data["billingPeriod"] is Dictionary: - obj.billing_period = SubscriptionPeriodValueIOS.from_dict(data["billingPeriod"]) - else: - obj.billing_period = data["billingPeriod"] + var decoded_billing_period = SubscriptionPeriodValueIOS.from_dict(data["billingPeriod"], report_errors) + if decoded_billing_period == null: + if report_errors: + push_error("Invalid required SubscriptionPeriodValueIOS value for billingPeriod") + return null + obj.billing_period = decoded_billing_period + else: + if report_errors: + push_error("Expected billingPeriod to be a SubscriptionPeriodValueIOS dictionary") + return null + else: + if report_errors: + push_error("Missing required billingPeriod value") + return null if data.has("billingPlanType") and data["billingPlanType"] != null: var enum_str = data["billingPlanType"] if enum_str is String: @@ -3106,17 +3294,35 @@ class SubscriptionPricingTermsIOS: obj.billing_price = data["billingPrice"] if data.has("commitmentInfo") and data["commitmentInfo"] != null: if data["commitmentInfo"] is Dictionary: - obj.commitment_info = SubscriptionCommitmentInfoIOS.from_dict(data["commitmentInfo"]) - else: - obj.commitment_info = data["commitmentInfo"] + var decoded_commitment_info = SubscriptionCommitmentInfoIOS.from_dict(data["commitmentInfo"], report_errors) + if decoded_commitment_info == null: + if report_errors: + push_error("Invalid required SubscriptionCommitmentInfoIOS value for commitmentInfo") + return null + obj.commitment_info = decoded_commitment_info + else: + if report_errors: + push_error("Expected commitmentInfo to be a SubscriptionCommitmentInfoIOS dictionary") + return null + else: + if report_errors: + push_error("Missing required commitmentInfo value") + return null if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: if data["subscriptionOffers"] is Array: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr return obj @@ -3209,7 +3415,7 @@ class UserChoiceBillingDetails: ## Structured product details selected in the user-choice flow, including the product type and offer token. Legacy payloads may omit this field; use products as the product-ID fallback. Available in OpenIAP Spec 2.3.0 / openiap-google 2.3.1 (requires Play Billing 9.1+). var product_details_android: Array[DeveloperProvidedBillingProductAndroid] = [] - static func from_dict(data: Dictionary) -> UserChoiceBillingDetails: + static func from_dict(data: Dictionary, report_errors: bool = true) -> UserChoiceBillingDetails: var obj = UserChoiceBillingDetails.new() if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: obj.external_transaction_token = data["externalTransactionToken"] @@ -3227,9 +3433,16 @@ class UserChoiceBillingDetails: var arr: Array[DeveloperProvidedBillingProductAndroid] = [] for item in data["productDetailsAndroid"]: if item is Dictionary: - arr.append(DeveloperProvidedBillingProductAndroid.from_dict(item)) + var decoded_developer_provided_billing_product_android = DeveloperProvidedBillingProductAndroid.from_dict(item, report_errors) + if decoded_developer_provided_billing_product_android == null: + return null + arr.append(decoded_developer_provided_billing_product_android) elif item is DeveloperProvidedBillingProductAndroid: arr.append(item) + else: + if report_errors: + push_error("Invalid DeveloperProvidedBillingProductAndroid list value for productDetailsAndroid") + return null obj.product_details_android = arr return obj @@ -3446,7 +3659,11 @@ class VerifyPurchaseWithProviderResult: ## Error details if verification failed var errors: Array[VerifyPurchaseWithProviderError] = [] - static func from_dict(data: Dictionary) -> VerifyPurchaseWithProviderResult: + static func from_dict(data: Dictionary, report_errors: bool = true) -> VerifyPurchaseWithProviderResult: + if not data.has("provider") or not ((data["provider"] is String and PURCHASE_VERIFICATION_PROVIDER_FROM_STRING.has(data["provider"])) or (data["provider"] is int and PURCHASE_VERIFICATION_PROVIDER_VALUES.has(data["provider"]))): + if report_errors: + push_error("Invalid VerifyPurchaseWithProviderResult.provider enum value") + return null var obj = VerifyPurchaseWithProviderResult.new() if data.has("provider") and data["provider"] != null: var enum_str = data["provider"] @@ -3467,6 +3684,9 @@ class VerifyPurchaseWithProviderResult: arr.append(VerifyPurchaseWithProviderError.from_dict(item)) elif item is VerifyPurchaseWithProviderError: arr.append(item) + else: + push_error("Invalid VerifyPurchaseWithProviderError list value for errors") + return null obj.errors = arr return obj @@ -3539,7 +3759,11 @@ class BillingProgramInformationDialogParamsAndroid: ## External transaction token returned by the Billing Choice reporting-details flow. var external_transaction_token: String = "" - static func from_dict(data: Dictionary) -> BillingProgramInformationDialogParamsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> BillingProgramInformationDialogParamsAndroid: + if data.has("billingProgram") and data["billingProgram"] != null and not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid BillingProgramInformationDialogParamsAndroid.billingProgram enum value") + return null var obj = BillingProgramInformationDialogParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3595,8 +3819,10 @@ class DeveloperBillingOptionParamsAndroid: ## A pre-generated external transaction token for a Billing Choice external-link flow. Omit it when Google Play should provide the token in the callback. var external_transaction_token: Variant = null - static func from_dict(data: Dictionary) -> DeveloperBillingOptionParamsAndroid: - if not data.has("billingProgram") or not data["billingProgram"] is String or not BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"]): + static func from_dict(data: Dictionary, report_errors: bool = true) -> DeveloperBillingOptionParamsAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid DeveloperBillingOptionParamsAndroid.billingProgram enum value") return null var obj = DeveloperBillingOptionParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: @@ -3684,7 +3910,15 @@ class GetBillingChoiceInfoParamsAndroid: ## BCP 47 locale tag. If omitted, Play Billing uses the user's default locale. var user_locale: Variant = null - static func from_dict(data: Dictionary) -> GetBillingChoiceInfoParamsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> GetBillingChoiceInfoParamsAndroid: + if data.has("billingProgram") and data["billingProgram"] != null and not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid GetBillingChoiceInfoParamsAndroid.billingProgram enum value") + return null + if data.has("playBillingChoiceImageLayout") and data["playBillingChoiceImageLayout"] != null and not ((data["playBillingChoiceImageLayout"] is String and BILLING_CHOICE_IMAGE_LAYOUT_ANDROID_FROM_STRING.has(data["playBillingChoiceImageLayout"])) or (data["playBillingChoiceImageLayout"] is int and BILLING_CHOICE_IMAGE_LAYOUT_ANDROID_VALUES.has(data["playBillingChoiceImageLayout"]))): + if report_errors: + push_error("Invalid GetBillingChoiceInfoParamsAndroid.playBillingChoiceImageLayout enum value") + return null var obj = GetBillingChoiceInfoParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3731,8 +3965,10 @@ class InAppMessageParamsAndroid: for item in data["categories"]: if item is String: arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.get(item, InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID)) - elif item is int: + elif item is int and IN_APP_MESSAGE_CATEGORY_ANDROID_VALUES.has(item): arr.append(item) + elif item is int: + arr.append(InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID) obj.categories = arr return obj @@ -3798,7 +4034,19 @@ class LaunchExternalLinkParamsAndroid: ## External transaction token for a developer-rendered Billing Choice external-link flow. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). Generate it with createBillingProgramReportingDetailsAndroid. var external_transaction_token: Variant = null - static func from_dict(data: Dictionary) -> LaunchExternalLinkParamsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> LaunchExternalLinkParamsAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid LaunchExternalLinkParamsAndroid.billingProgram enum value") + return null + if not data.has("launchMode") or not ((data["launchMode"] is String and EXTERNAL_LINK_LAUNCH_MODE_ANDROID_FROM_STRING.has(data["launchMode"])) or (data["launchMode"] is int and EXTERNAL_LINK_LAUNCH_MODE_ANDROID_VALUES.has(data["launchMode"]))): + if report_errors: + push_error("Invalid LaunchExternalLinkParamsAndroid.launchMode enum value") + return null + if not data.has("linkType") or not ((data["linkType"] is String and EXTERNAL_LINK_TYPE_ANDROID_FROM_STRING.has(data["linkType"])) or (data["linkType"] is int and EXTERNAL_LINK_TYPE_ANDROID_VALUES.has(data["linkType"]))): + if report_errors: + push_error("Invalid LaunchExternalLinkParamsAndroid.linkType enum value") + return null var obj = LaunchExternalLinkParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3912,7 +4160,7 @@ class PurchaseInput: ## Store where purchase was made var store: Variant = null var quantity: int = 0 - var purchase_state: PurchaseState + var purchase_state: PurchaseState = PurchaseState.UNKNOWN var is_auto_renewing: bool = false static func from_dict(data: Dictionary) -> PurchaseInput: @@ -4059,7 +4307,14 @@ class RequestPurchaseAndroidProps: obj.offer_token = data["offerToken"] if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: - obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + var decoded_developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + if decoded_developer_billing_option == null: + push_error("Invalid input DeveloperBillingOptionParamsAndroid value for developerBillingOption") + return null + obj.developer_billing_option = decoded_developer_billing_option + else: + push_error("Expected developerBillingOption to be a DeveloperBillingOptionParamsAndroid dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4107,9 +4362,14 @@ class RequestPurchaseIosProps: obj.quantity = data["quantity"] if data.has("withOffer") and data["withOffer"] != null: if data["withOffer"] is Dictionary: - obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) - else: - obj.with_offer = data["withOffer"] + var decoded_with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) + if decoded_with_offer == null: + push_error("Invalid input DiscountOfferInputIOS value for withOffer") + return null + obj.with_offer = decoded_with_offer + else: + push_error("Expected withOffer to be a DiscountOfferInputIOS dictionary") + return null if data.has("advancedCommerceData") and data["advancedCommerceData"] != null: obj.advanced_commerce_data = data["advancedCommerceData"] return obj @@ -4205,14 +4465,24 @@ class RequestPurchasePropsByPlatforms: var obj = RequestPurchasePropsByPlatforms.new() if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = RequestPurchaseIosProps.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = RequestPurchaseIosProps.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input RequestPurchaseIosProps value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a RequestPurchaseIosProps dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = RequestPurchaseAndroidProps.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = RequestPurchaseAndroidProps.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input RequestPurchaseAndroidProps value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a RequestPurchaseAndroidProps dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4276,15 +4546,30 @@ class RequestSubscriptionAndroidProps: arr.append(AndroidSubscriptionOfferInput.from_dict(item)) elif item is AndroidSubscriptionOfferInput: arr.append(item) + else: + push_error("Invalid AndroidSubscriptionOfferInput list value for subscriptionOffers") + return null obj.subscription_offers = arr if data.has("subscriptionProductReplacementParams") and data["subscriptionProductReplacementParams"] != null: if data["subscriptionProductReplacementParams"] is Dictionary: - obj.subscription_product_replacement_params = SubscriptionProductReplacementParamsAndroid.from_dict(data["subscriptionProductReplacementParams"]) - else: - obj.subscription_product_replacement_params = data["subscriptionProductReplacementParams"] + var decoded_subscription_product_replacement_params = SubscriptionProductReplacementParamsAndroid.from_dict(data["subscriptionProductReplacementParams"]) + if decoded_subscription_product_replacement_params == null: + push_error("Invalid input SubscriptionProductReplacementParamsAndroid value for subscriptionProductReplacementParams") + return null + obj.subscription_product_replacement_params = decoded_subscription_product_replacement_params + else: + push_error("Expected subscriptionProductReplacementParams to be a SubscriptionProductReplacementParamsAndroid dictionary") + return null if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: - obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + var decoded_developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + if decoded_developer_billing_option == null: + push_error("Invalid input DeveloperBillingOptionParamsAndroid value for developerBillingOption") + return null + obj.developer_billing_option = decoded_developer_billing_option + else: + push_error("Expected developerBillingOption to be a DeveloperBillingOptionParamsAndroid dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4351,19 +4636,34 @@ class RequestSubscriptionIosProps: obj.quantity = data["quantity"] if data.has("withOffer") and data["withOffer"] != null: if data["withOffer"] is Dictionary: - obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) - else: - obj.with_offer = data["withOffer"] + var decoded_with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) + if decoded_with_offer == null: + push_error("Invalid input DiscountOfferInputIOS value for withOffer") + return null + obj.with_offer = decoded_with_offer + else: + push_error("Expected withOffer to be a DiscountOfferInputIOS dictionary") + return null if data.has("winBackOffer") and data["winBackOffer"] != null: if data["winBackOffer"] is Dictionary: - obj.win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"]) - else: - obj.win_back_offer = data["winBackOffer"] + var decoded_win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"]) + if decoded_win_back_offer == null: + push_error("Invalid input WinBackOfferInputIOS value for winBackOffer") + return null + obj.win_back_offer = decoded_win_back_offer + else: + push_error("Expected winBackOffer to be a WinBackOfferInputIOS dictionary") + return null if data.has("promotionalOfferJWS") and data["promotionalOfferJWS"] != null: if data["promotionalOfferJWS"] is Dictionary: - obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"]) - else: - obj.promotional_offer_jws = data["promotionalOfferJWS"] + var decoded_promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"]) + if decoded_promotional_offer_jws == null: + push_error("Invalid input PromotionalOfferJWSInputIOS value for promotionalOfferJWS") + return null + obj.promotional_offer_jws = decoded_promotional_offer_jws + else: + push_error("Expected promotionalOfferJWS to be a PromotionalOfferJWSInputIOS dictionary") + return null if data.has("billingPlanType") and data["billingPlanType"] != null: var enum_str = data["billingPlanType"] if enum_str is String: @@ -4425,14 +4725,24 @@ class RequestSubscriptionPropsByPlatforms: var obj = RequestSubscriptionPropsByPlatforms.new() if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = RequestSubscriptionIosProps.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = RequestSubscriptionIosProps.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input RequestSubscriptionIosProps value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a RequestSubscriptionIosProps dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = RequestSubscriptionAndroidProps.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = RequestSubscriptionAndroidProps.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input RequestSubscriptionAndroidProps value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a RequestSubscriptionAndroidProps dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4540,19 +4850,34 @@ class RequestVerifyPurchaseWithIapkitProps: obj.include_client_payload = data["includeClientPayload"] if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = RequestVerifyPurchaseWithIapkitAppleProps.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = RequestVerifyPurchaseWithIapkitAppleProps.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitAppleProps value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a RequestVerifyPurchaseWithIapkitAppleProps dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = RequestVerifyPurchaseWithIapkitGoogleProps.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = RequestVerifyPurchaseWithIapkitGoogleProps.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitGoogleProps value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a RequestVerifyPurchaseWithIapkitGoogleProps dictionary") + return null if data.has("amazon") and data["amazon"] != null: if data["amazon"] is Dictionary: - obj.amazon = RequestVerifyPurchaseWithIapkitAmazonProps.from_dict(data["amazon"]) - else: - obj.amazon = data["amazon"] + var decoded_amazon = RequestVerifyPurchaseWithIapkitAmazonProps.from_dict(data["amazon"]) + if decoded_amazon == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitAmazonProps value for amazon") + return null + obj.amazon = decoded_amazon + else: + push_error("Expected amazon to be a RequestVerifyPurchaseWithIapkitAmazonProps dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4585,7 +4910,7 @@ class SubscriptionProductReplacementParamsAndroid: ## The old product ID that needs to be replaced var old_product_id: String = "" ## The replacement mode for this product change - var replacement_mode: SubscriptionReplacementModeAndroid + var replacement_mode: SubscriptionReplacementModeAndroid = SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE static func from_dict(data: Dictionary) -> SubscriptionProductReplacementParamsAndroid: var obj = SubscriptionProductReplacementParamsAndroid.new() @@ -4712,19 +5037,34 @@ class VerifyPurchaseProps: var obj = VerifyPurchaseProps.new() if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = VerifyPurchaseAppleOptions.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = VerifyPurchaseAppleOptions.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input VerifyPurchaseAppleOptions value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a VerifyPurchaseAppleOptions dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = VerifyPurchaseGoogleOptions.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = VerifyPurchaseGoogleOptions.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input VerifyPurchaseGoogleOptions value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a VerifyPurchaseGoogleOptions dictionary") + return null if data.has("horizon") and data["horizon"] != null: if data["horizon"] is Dictionary: - obj.horizon = VerifyPurchaseHorizonOptions.from_dict(data["horizon"]) - else: - obj.horizon = data["horizon"] + var decoded_horizon = VerifyPurchaseHorizonOptions.from_dict(data["horizon"]) + if decoded_horizon == null: + push_error("Invalid input VerifyPurchaseHorizonOptions value for horizon") + return null + obj.horizon = decoded_horizon + else: + push_error("Expected horizon to be a VerifyPurchaseHorizonOptions dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4750,7 +5090,11 @@ class VerifyPurchaseWithProviderProps: var provider: PurchaseVerificationProvider var iapkit: RequestVerifyPurchaseWithIapkitProps - static func from_dict(data: Dictionary) -> VerifyPurchaseWithProviderProps: + static func from_dict(data: Dictionary, report_errors: bool = true) -> VerifyPurchaseWithProviderProps: + if not data.has("provider") or not ((data["provider"] is String and PURCHASE_VERIFICATION_PROVIDER_FROM_STRING.has(data["provider"])) or (data["provider"] is int and PURCHASE_VERIFICATION_PROVIDER_VALUES.has(data["provider"]))): + if report_errors: + push_error("Invalid VerifyPurchaseWithProviderProps.provider enum value") + return null var obj = VerifyPurchaseWithProviderProps.new() if data.has("provider") and data["provider"] != null: var enum_str = data["provider"] @@ -4760,9 +5104,14 @@ class VerifyPurchaseWithProviderProps: obj.provider = enum_str if data.has("iapkit") and data["iapkit"] != null: if data["iapkit"] is Dictionary: - obj.iapkit = RequestVerifyPurchaseWithIapkitProps.from_dict(data["iapkit"]) - else: - obj.iapkit = data["iapkit"] + var decoded_iapkit = RequestVerifyPurchaseWithIapkitProps.from_dict(data["iapkit"]) + if decoded_iapkit == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitProps value for iapkit") + return null + obj.iapkit = decoded_iapkit + else: + push_error("Expected iapkit to be a RequestVerifyPurchaseWithIapkitProps dictionary") + return null return obj func to_dict() -> Dictionary: diff --git a/libraries/godot-iap/android/build.gradle.kts b/libraries/godot-iap/android/build.gradle.kts index 4874c5e4b..edf9de2b8 100644 --- a/libraries/godot-iap/android/build.gradle.kts +++ b/libraries/godot-iap/android/build.gradle.kts @@ -99,6 +99,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$googleCoroutinesVersion") testImplementation("junit:junit:4.13.2") + testImplementation("org.json:json:20260719") } // Copy the built AAR to the addons directory for Godot diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt index 321cbb219..67d808984 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt @@ -117,22 +117,23 @@ internal object GodotIapHelper { // Parse subscriptionProductReplacementParams (8.1.0+) val subscriptionProductReplacementParams = if (json.has("subscriptionProductReplacementParams")) { val paramsObj = json.optJSONObject("subscriptionProductReplacementParams") - if (paramsObj != null) { - val oldProductId = paramsObj.optString("oldProductId").takeIf { it.isNotEmpty() } - val mode = paramsObj.optString("replacementMode").takeIf { it.isNotEmpty() } - val parsedMode = parseSubscriptionReplacementMode(mode) - if (oldProductId != null && parsedMode != null) { - SubscriptionProductReplacementParamsAndroid( - oldProductId = oldProductId, - replacementMode = parsedMode - ) - } else null - } else null + ?: throw IllegalArgumentException("subscriptionProductReplacementParams must be an object") + val oldProductId = paramsObj.optString("oldProductId").takeIf { it.isNotEmpty() } + ?: throw IllegalArgumentException("subscriptionProductReplacementParams.oldProductId is required") + val mode = paramsObj.optString("replacementMode").takeIf { it.isNotEmpty() } + val parsedMode = parseSubscriptionReplacementMode(mode) + ?: throw IllegalArgumentException("A concrete subscription replacement mode is required") + SubscriptionProductReplacementParamsAndroid( + oldProductId = oldProductId, + replacementMode = parsedMode + ) } else null - val developerBillingOption = json.optJSONObject("developerBillingOption")?.let { - DeveloperBillingOptionParamsAndroid.fromJson(jsonObjectToMap(it)) - } + val developerBillingOption = if (json.has("developerBillingOption")) { + val option = json.optJSONObject("developerBillingOption") + ?: throw IllegalArgumentException("developerBillingOption must be an object") + DeveloperBillingOptionParamsAndroid.fromJson(jsonObjectToMap(option)) + } else null val offerToken = json.optStringOrNull("offerToken") @@ -152,6 +153,15 @@ internal object GodotIapHelper { } } + val productType = parseProductQueryType( + rawType = type, + defaultType = ProductQueryType.InApp, + allowAll = false, + ) + if (subscriptionProductReplacementParams != null && productType != ProductQueryType.Subs) { + throw IllegalArgumentException("subscriptionProductReplacementParams requires type `subs`") + } + return RequestPurchaseParams( type = type, skus = skus, @@ -173,12 +183,12 @@ internal object GodotIapHelper { */ private fun parseSubscriptionReplacementMode(mode: String?): SubscriptionReplacementModeAndroid? { return when (mode?.lowercase(Locale.US)?.replace("-", "_")) { - "unknown_replacement_mode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode "with_time_proration" -> SubscriptionReplacementModeAndroid.WithTimeProration "charge_prorated_price" -> SubscriptionReplacementModeAndroid.ChargeProratedPrice "without_proration" -> SubscriptionReplacementModeAndroid.WithoutProration "charge_full_price" -> SubscriptionReplacementModeAndroid.ChargeFullPrice "deferred" -> SubscriptionReplacementModeAndroid.Deferred + "keep_existing" -> SubscriptionReplacementModeAndroid.KeepExisting else -> null } } diff --git a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt index e8b09c013..b9b06fe37 100644 --- a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt +++ b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt @@ -1,6 +1,8 @@ package dev.hyo.godotiap +import dev.hyo.openiap.BillingProgramAndroid import dev.hyo.openiap.ProductQueryType +import dev.hyo.openiap.SubscriptionReplacementModeAndroid import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Test @@ -40,4 +42,71 @@ class GodotIapHelperTest { } } + @Test + fun `subscription replacement modes require complete concrete params`() { + val invalidParams = listOf( + """{"type":"subs","subscriptionProductReplacementParams":null}""", + """{"type":"subs","subscriptionProductReplacementParams":{}}""", + """{"type":"subs","subscriptionProductReplacementParams":{"oldProductId":"old"}}""", + """{"type":"subs","subscriptionProductReplacementParams":{"oldProductId":"old","replacementMode":"future-mode"}}""", + """{"type":"subs","subscriptionProductReplacementParams":{"oldProductId":"old","replacementMode":"unknown-replacement-mode"}}""", + ) + + invalidParams.forEach { json -> + assertThrows(IllegalArgumentException::class.java) { + GodotIapHelper.parseRequestPurchaseParams(json) + } + } + } + + @Test + fun `all concrete subscription replacement modes parse`() { + val mappings = mapOf( + "with-time-proration" to SubscriptionReplacementModeAndroid.WithTimeProration, + "charge-prorated-price" to SubscriptionReplacementModeAndroid.ChargeProratedPrice, + "charge-full-price" to SubscriptionReplacementModeAndroid.ChargeFullPrice, + "without-proration" to SubscriptionReplacementModeAndroid.WithoutProration, + "deferred" to SubscriptionReplacementModeAndroid.Deferred, + "keep-existing" to SubscriptionReplacementModeAndroid.KeepExisting, + ) + + mappings.forEach { (rawMode, expected) -> + val params = GodotIapHelper.parseRequestPurchaseParams( + """{"type":"subs","subscriptionProductReplacementParams":{"oldProductId":"old","replacementMode":"$rawMode"}}""", + ) + assertEquals(expected, params.subscriptionProductReplacementParams?.replacementMode) + } + } + + @Test + fun `native purchase boundary rejects replacement params for in-app products`() { + assertThrows(IllegalArgumentException::class.java) { + GodotIapHelper.parseRequestPurchaseParams( + """{"type":"in-app","skus":["coins"],"subscriptionProductReplacementParams":{"oldProductId":"old","replacementMode":"without-proration"}}""", + ) + } + } + + @Test + fun `developer billing option is either absent or valid`() { + listOf( + """{"developerBillingOption":null}""", + """{"developerBillingOption":"billing-choice"}""", + """{"developerBillingOption":{}}""", + """{"developerBillingOption":{"billingProgram":"future-program"}}""", + ).forEach { json -> + assertThrows(IllegalArgumentException::class.java) { + GodotIapHelper.parseRequestPurchaseParams(json) + } + } + + val absent = GodotIapHelper.parseRequestPurchaseParams("{}") + assertEquals(null, absent.developerBillingOption) + + val valid = GodotIapHelper.parseRequestPurchaseParams( + """{"developerBillingOption":{"billingProgram":"billing-choice"}}""", + ) + assertEquals(BillingProgramAndroid.BillingChoice, valid.developerBillingOption?.billingProgram) + } + } diff --git a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/Helper.kt b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/Helper.kt index 8c0f85eb6..1b027fff8 100644 --- a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/Helper.kt +++ b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/Helper.kt @@ -306,7 +306,7 @@ internal fun Int.toOpenIapSubResponseCode(): SubResponseCodeAndroid? = when (thi */ internal fun mapReplacementMode(mode: SubscriptionReplacementModeAndroid): Int? = when (mode) { SubscriptionReplacementModeAndroid.UnknownReplacementMode -> - BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode.UNKNOWN_REPLACEMENT_MODE + throw IllegalArgumentException("A concrete subscription replacement mode is required.") SubscriptionReplacementModeAndroid.WithTimeProration -> BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode.WITH_TIME_PRORATION SubscriptionReplacementModeAndroid.ChargeProratedPrice -> diff --git a/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/SubscriptionReplacementResolutionTest.kt b/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/SubscriptionReplacementResolutionTest.kt index 017ffe40c..84b595777 100644 --- a/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/SubscriptionReplacementResolutionTest.kt +++ b/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/SubscriptionReplacementResolutionTest.kt @@ -1,7 +1,11 @@ package io.github.hyochan.kmpiap +import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode +import io.github.hyochan.kmpiap.openiap.SubscriptionProductReplacementParamsAndroid +import io.github.hyochan.kmpiap.openiap.SubscriptionReplacementModeAndroid import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull class SubscriptionReplacementResolutionTest { @@ -28,4 +32,36 @@ class SubscriptionReplacementResolutionTest { ) ) } + + @Test + fun `concrete product replacement modes map to native values`() { + val mappings = mapOf( + SubscriptionReplacementModeAndroid.WithTimeProration to ReplacementMode.WITH_TIME_PRORATION, + SubscriptionReplacementModeAndroid.ChargeProratedPrice to ReplacementMode.CHARGE_PRORATED_PRICE, + SubscriptionReplacementModeAndroid.ChargeFullPrice to ReplacementMode.CHARGE_FULL_PRICE, + SubscriptionReplacementModeAndroid.WithoutProration to ReplacementMode.WITHOUT_PRORATION, + SubscriptionReplacementModeAndroid.Deferred to ReplacementMode.DEFERRED, + SubscriptionReplacementModeAndroid.KeepExisting to ReplacementMode.KEEP_EXISTING, + ) + + mappings.forEach { (mode, nativeValue) -> + assertEquals(nativeValue, mapReplacementMode(mode)) + } + } + + @Test + fun `unknown and missing product replacement modes are rejected`() { + val inputs = listOf( + mapOf("oldProductId" to "old.product", "replacementMode" to "future-mode"), + mapOf("oldProductId" to "old.product"), + ) + + inputs.forEach { input -> + val params = requireNotNull(SubscriptionProductReplacementParamsAndroid.fromJson(input)) + assertEquals(SubscriptionReplacementModeAndroid.UnknownReplacementMode, params.replacementMode) + assertFailsWith { + mapReplacementMode(params.replacementMode) + } + } + } } 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 ea8beda7b..798a26ae2 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 @@ -1668,7 +1668,7 @@ public data class BillingProgramAvailabilityResultAndroid( companion object { fun fromJson(json: Map): BillingProgramAvailabilityResultAndroid { return BillingProgramAvailabilityResultAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified, + billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid"), choiceScreenType = (json["choiceScreenType"] as? String)?.let { BillingChoiceScreenTypeAndroid.fromJson(it) }, isAvailable = json["isAvailable"] as? Boolean ?: false, isExternalLinkAvailable = json["isExternalLinkAvailable"] as? Boolean, @@ -1706,7 +1706,7 @@ public data class BillingProgramReportingDetailsAndroid( companion object { fun fromJson(json: Map): BillingProgramReportingDetailsAndroid { return BillingProgramReportingDetailsAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified, + billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid"), externalTransactionToken = json["externalTransactionToken"] as? String ?: "", ) } @@ -1873,7 +1873,7 @@ public data class DeveloperProvidedBillingProductAndroid( return DeveloperProvidedBillingProductAndroid( id = json["id"] as? String ?: "", offerToken = json["offerToken"] as? String, - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductType"), ) } } @@ -2058,7 +2058,7 @@ public data class DiscountOffer( price = (json["price"] as? Number)?.toDouble() ?: 0.0, purchaseOptionIdAndroid = json["purchaseOptionIdAndroid"] as? String, rentalDetailsAndroid = (json["rentalDetailsAndroid"] as? Map)?.let { RentalDetailsAndroid.fromJson(it) }, - type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory, + type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for DiscountOfferType"), validTimeWindowAndroid = (json["validTimeWindowAndroid"] as? Map)?.let { ValidTimeWindowAndroid.fromJson(it) }, ) } @@ -2226,7 +2226,7 @@ public data class ExternalPurchaseNoticeResultIOS( return ExternalPurchaseNoticeResultIOS( error = json["error"] as? String, externalPurchaseToken = json["externalPurchaseToken"] as? String, - result = (json["result"] as? String)?.let { ExternalPurchaseNoticeAction.fromJson(it) } ?: ExternalPurchaseNoticeAction.Continue, + result = (json["result"] as? String)?.let { ExternalPurchaseNoticeAction.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalPurchaseNoticeAction"), ) } } @@ -2298,7 +2298,7 @@ public data class InAppMessageResultAndroid( fun fromJson(json: Map): InAppMessageResultAndroid { return InAppMessageResultAndroid( purchaseToken = json["purchaseToken"] as? String, - responseCode = (json["responseCode"] as? String)?.let { InAppMessageResponseCodeAndroid.fromJson(it) } ?: InAppMessageResponseCodeAndroid.NoActionNeeded, + responseCode = (json["responseCode"] as? String)?.let { InAppMessageResponseCodeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for InAppMessageResponseCodeAndroid"), ) } } @@ -2543,7 +2543,7 @@ public data class ProductAndroid( displayPrice = json["displayPrice"] as? String ?: "", id = json["id"] as? String ?: "", nameAndroid = json["nameAndroid"] as? String ?: "", - platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, + platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Android, price = (json["price"] as? Number)?.toDouble(), productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, @@ -2619,7 +2619,7 @@ public data class ProductIOS( subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, - typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: ProductTypeIOS.Consumable, + typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductTypeIOS"), ) } } @@ -2683,12 +2683,12 @@ public data class ProductSubscriptionAndroid( displayPrice = json["displayPrice"] as? String ?: "", id = json["id"] as? String ?: "", nameAndroid = json["nameAndroid"] as? String ?: "", - platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, + platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Android, price = (json["price"] as? Number)?.toDouble(), productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(), title = json["title"] as? String ?: "", - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, ) } } @@ -2781,8 +2781,8 @@ public data class ProductSubscriptionIOS( subscriptionPeriodNumberIOS = json["subscriptionPeriodNumberIOS"] as? String, subscriptionPeriodUnitIOS = (json["subscriptionPeriodUnitIOS"] as? String)?.let { SubscriptionPeriodIOS.fromJson(it) }, title = json["title"] as? String ?: "", - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, - typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: ProductTypeIOS.Consumable, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, + typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductTypeIOS"), ) } } @@ -3659,7 +3659,7 @@ public data class SubscriptionOffer( pricingPhasesAndroid = (json["pricingPhasesAndroid"] as? Map)?.let { PricingPhasesAndroid.fromJson(it) }, signatureIOS = json["signatureIOS"] as? String, timestampIOS = (json["timestampIOS"] as? Number)?.toDouble(), - type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory, + type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for DiscountOfferType"), ) } } @@ -4109,7 +4109,7 @@ public data class VerifyPurchaseWithProviderResult( return VerifyPurchaseWithProviderResult( errors = (json["errors"] as? List<*>)?.mapNotNull { (it as? Map)?.let { VerifyPurchaseWithProviderError.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for VerifyPurchaseWithProviderError") }, iapkit = (json["iapkit"] as? Map)?.let { RequestVerifyPurchaseWithIapkitResult.fromJson(it) }, - provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: PurchaseVerificationProvider.Iapkit, + provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider"), ) } } @@ -4443,10 +4443,10 @@ public data class LaunchExternalLinkParamsAndroid( ) { companion object { fun fromJson(json: Map): LaunchExternalLinkParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String - val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: ExternalLinkLaunchModeAndroid.Unspecified - val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: ExternalLinkTypeAndroid.Unspecified + val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkLaunchModeAndroid") + val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkTypeAndroid") val linkUri = json["linkUri"] as? String if (linkUri == null) return null return LaunchExternalLinkParamsAndroid( @@ -4616,7 +4616,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4681,7 +4681,7 @@ public data class RequestPurchaseIosProps( val appAccountToken = json["appAccountToken"] as? String val quantity = (json["quantity"] as? Number)?.toInt() val sku = json["sku"] as? String - val withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } + val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null return RequestPurchaseIosProps( advancedCommerceData = advancedCommerceData, @@ -4787,8 +4787,8 @@ public data class RequestPurchasePropsByPlatforms( companion object { fun fromJson(json: Map): RequestPurchasePropsByPlatforms { return RequestPurchasePropsByPlatforms( - apple = (json["apple"] as? Map)?.let { RequestPurchaseIosProps.fromJson(it) }, - google = (json["google"] as? Map)?.let { RequestPurchaseAndroidProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestPurchaseIosProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestPurchaseIosProps") }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestPurchaseAndroidProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestPurchaseAndroidProps") }, ) } } @@ -4848,7 +4848,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4856,7 +4856,7 @@ public data class RequestSubscriptionAndroidProps( val purchaseToken = json["purchaseToken"] as? String val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } val subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } } - val subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } + val subscriptionProductReplacementParams = json["subscriptionProductReplacementParams"]?.let { value -> (value as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for SubscriptionProductReplacementParamsAndroid") } if (skus == null) return null return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, @@ -4935,11 +4935,11 @@ public data class RequestSubscriptionIosProps( val appAccountToken = json["appAccountToken"] as? String val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } val compactJWS = json["compactJWS"] as? String - val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } + val promotionalOfferJWS = json["promotionalOfferJWS"]?.let { value -> (value as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for PromotionalOfferJWSInputIOS") } val quantity = (json["quantity"] as? Number)?.toInt() val sku = json["sku"] as? String - val winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } - val withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } + val winBackOffer = json["winBackOffer"]?.let { value -> (value as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for WinBackOfferInputIOS") } + val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null return RequestSubscriptionIosProps( advancedCommerceData = advancedCommerceData, @@ -4992,8 +4992,8 @@ public data class RequestSubscriptionPropsByPlatforms( companion object { fun fromJson(json: Map): RequestSubscriptionPropsByPlatforms { return RequestSubscriptionPropsByPlatforms( - apple = (json["apple"] as? Map)?.let { RequestSubscriptionIosProps.fromJson(it) }, - google = (json["google"] as? Map)?.let { RequestSubscriptionAndroidProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestSubscriptionIosProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestSubscriptionIosProps") }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestSubscriptionAndroidProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestSubscriptionAndroidProps") }, ) } } @@ -5167,11 +5167,11 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( - amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, + amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, apiKey = json["apiKey"] as? String, - apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAppleProps") }, baseUrl = json["baseUrl"] as? String, - google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitGoogleProps") }, includeClientPayload = json["includeClientPayload"] as? Boolean, ) } @@ -5369,9 +5369,9 @@ public data class VerifyPurchaseProps( companion object { fun fromJson(json: Map): VerifyPurchaseProps { return VerifyPurchaseProps( - apple = (json["apple"] as? Map)?.let { VerifyPurchaseAppleOptions.fromJson(it) }, - google = (json["google"] as? Map)?.let { VerifyPurchaseGoogleOptions.fromJson(it) }, - horizon = (json["horizon"] as? Map)?.let { VerifyPurchaseHorizonOptions.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { VerifyPurchaseAppleOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseAppleOptions") }, + google = json["google"]?.let { value -> (value as? Map)?.let { VerifyPurchaseGoogleOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseGoogleOptions") }, + horizon = json["horizon"]?.let { value -> (value as? Map)?.let { VerifyPurchaseHorizonOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseHorizonOptions") }, ) } } @@ -5389,8 +5389,8 @@ public data class VerifyPurchaseWithProviderProps( ) { companion object { fun fromJson(json: Map): VerifyPurchaseWithProviderProps? { - val iapkit = (json["iapkit"] as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } - val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: PurchaseVerificationProvider.Iapkit + val iapkit = json["iapkit"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitProps") } + val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider") return VerifyPurchaseWithProviderProps( iapkit = iapkit, provider = provider, diff --git a/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs b/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs index 7189716ad..44e3939b1 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net.Http; using System.Text; using System.Text.Json; @@ -54,7 +55,9 @@ internal sealed class TolerantJsonObjectConverter : JsonConverter public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var document = JsonDocument.ParseValue(ref reader); - return TolerantJson.Deserialize(document.RootElement, options); + var value = TolerantJson.Deserialize(document.RootElement, options); + if (value is null) Trace.TraceWarning("Skipped unreadable {0} object.", typeof(T).Name); + return value; } public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) @@ -73,10 +76,19 @@ public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConv } var values = new List(); + var index = 0; foreach (var element in document.RootElement.EnumerateArray()) { var value = TolerantJson.Deserialize(element, options); - if (value is not null) values.Add(value); + if (value is not null) + { + values.Add(value); + } + else + { + Trace.TraceWarning("Skipped unreadable {0} row at index {1}.", typeof(T).Name, index); + } + index++; } return values; } diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index 36c18b74d..4fd848499 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -3984,29 +3984,6 @@ public sealed record DeveloperBillingOptionParamsAndroid public string? ExternalTransactionToken { get; init; } } -public sealed class DeveloperBillingOptionParamsAndroidNullableJsonConverter : JsonConverter -{ - public override DeveloperBillingOptionParamsAndroid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var document = JsonDocument.ParseValue(ref reader); - try - { - return document.RootElement.Deserialize(options); - } - catch (JsonException) - { - return null; - } - catch (InvalidOperationException) - { - return null; - } - } - - public override void Write(Utf8JsonWriter writer, DeveloperBillingOptionParamsAndroid? value, JsonSerializerOptions options) => - JsonSerializer.Serialize(writer, value, options); -} - public sealed record DiscountOfferInputIOS { /// Discount identifier @@ -4203,7 +4180,6 @@ public sealed record RequestPurchaseAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] - [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -4330,7 +4306,6 @@ public sealed record RequestSubscriptionAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] - [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } diff --git a/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs b/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs index 6dcf151fc..637959cec 100644 --- a/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs +++ b/libraries/maui-iap/tests/OpenIap.Maui.Tests/KitApiClientTests.cs @@ -3,6 +3,8 @@ // (same fake-HttpMessageHandler technique), plus coverage for the remaining // endpoints and the KitApiError paths. +using System.Diagnostics; +using System.IO; using System.Net; using System.Text.Json; using System.Text.Json.Nodes; @@ -165,13 +167,28 @@ public async Task Products_DropUnreadableProductsAndOffersIndependently() """); using var httpClient = new HttpClient(handler); var client = CreateClient(httpClient); + using var diagnostics = new StringWriter(); + using var listener = new TextWriterTraceListener(diagnostics); + Trace.Listeners.Add(listener); - var response = await client.ProductsAsync(); + KitProductsResponse response; + try + { + response = await client.ProductsAsync(); + Trace.Flush(); + } + finally + { + Trace.Listeners.Remove(listener); + } var product = Assert.Single(response.Products); Assert.Equal("premium.monthly", product.ProductId); var offer = Assert.Single(Assert.IsAssignableFrom>(product.Offers)); Assert.Equal("intro", offer.Id); + var warnings = diagnostics.ToString(); + Assert.Contains("Skipped unreadable KitProduct row at index 0.", warnings, StringComparison.Ordinal); + Assert.Contains("Skipped unreadable KitProductOffer row at index 0.", warnings, StringComparison.Ordinal); } [Fact] @@ -300,11 +317,27 @@ public async Task Status_DropsSubscriptionWithUnknownPlatform() """); using var httpClient = new HttpClient(handler); var client = CreateClient(httpClient); + using var diagnostics = new StringWriter(); + using var listener = new TextWriterTraceListener(diagnostics); + Trace.Listeners.Add(listener); - var response = await client.StatusAsync("user-1"); + StatusResponse response; + try + { + response = await client.StatusAsync("user-1"); + Trace.Flush(); + } + finally + { + Trace.Listeners.Remove(listener); + } Assert.True(response.Active); Assert.Null(response.Subscription); + Assert.Contains( + "Skipped unreadable KitSubscription object.", + diagnostics.ToString(), + StringComparison.Ordinal); } [Fact] diff --git a/packages/docs/src/pages/docs/kit-compatibility.tsx b/packages/docs/src/pages/docs/kit-compatibility.tsx index 2bbaaee98..05b0aced2 100644 --- a/packages/docs/src/pages/docs/kit-compatibility.tsx +++ b/packages/docs/src/pages/docs/kit-compatibility.tsx @@ -94,8 +94,9 @@ function KitCompatibility() {
  • clientPayload — a payload this build cannot read, - including one using a format added later, is dropped and the result - still returns. + including one using a format added later, never rejects the + verification result. Strongly typed SDKs drop the optional payload; + JavaScript Kit clients preserve its format and body as opaque data.
  • environment — forwarded as an opaque string. It is{' '} @@ -153,9 +154,10 @@ X-OpenIAP-Spec: 3.2.0`} the emitted one cannot diverge.
  • - Each SDK has tests that feed its parser values from a hypothetical - future IAPKit — an unknown state, an unknown client-payload format, - an unrecognised environment — and assert the receipt survives. + SDK tests feed parsers values from a hypothetical future IAPKit — an + unknown state, an unknown client-payload format, an unrecognised + environment — and assert the receipt survives with the payload + either dropped or preserved opaquely according to the SDK contract.
  • The entitlement decision is pinned by an exhaustive table, so a new 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 e702b3c99..f59999575 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 @@ -1720,7 +1720,7 @@ public data class BillingProgramAvailabilityResultAndroid( companion object { fun fromJson(json: Map): BillingProgramAvailabilityResultAndroid { return BillingProgramAvailabilityResultAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified, + billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid"), choiceScreenType = (json["choiceScreenType"] as? String)?.let { BillingChoiceScreenTypeAndroid.fromJson(it) }, isAvailable = json["isAvailable"] as? Boolean ?: false, isExternalLinkAvailable = json["isExternalLinkAvailable"] as? Boolean, @@ -1758,7 +1758,7 @@ public data class BillingProgramReportingDetailsAndroid( companion object { fun fromJson(json: Map): BillingProgramReportingDetailsAndroid { return BillingProgramReportingDetailsAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified, + billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid"), externalTransactionToken = json["externalTransactionToken"] as? String ?: "", ) } @@ -1925,7 +1925,7 @@ public data class DeveloperProvidedBillingProductAndroid( return DeveloperProvidedBillingProductAndroid( id = json["id"] as? String ?: "", offerToken = json["offerToken"] as? String, - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductType"), ) } } @@ -2110,7 +2110,7 @@ public data class DiscountOffer( price = (json["price"] as? Number)?.toDouble() ?: 0.0, purchaseOptionIdAndroid = json["purchaseOptionIdAndroid"] as? String, rentalDetailsAndroid = (json["rentalDetailsAndroid"] as? Map)?.let { RentalDetailsAndroid.fromJson(it) }, - type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory, + type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for DiscountOfferType"), validTimeWindowAndroid = (json["validTimeWindowAndroid"] as? Map)?.let { ValidTimeWindowAndroid.fromJson(it) }, ) } @@ -2278,7 +2278,7 @@ public data class ExternalPurchaseNoticeResultIOS( return ExternalPurchaseNoticeResultIOS( error = json["error"] as? String, externalPurchaseToken = json["externalPurchaseToken"] as? String, - result = (json["result"] as? String)?.let { ExternalPurchaseNoticeAction.fromJson(it) } ?: ExternalPurchaseNoticeAction.Continue, + result = (json["result"] as? String)?.let { ExternalPurchaseNoticeAction.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalPurchaseNoticeAction"), ) } } @@ -2350,7 +2350,7 @@ public data class InAppMessageResultAndroid( fun fromJson(json: Map): InAppMessageResultAndroid { return InAppMessageResultAndroid( purchaseToken = json["purchaseToken"] as? String, - responseCode = (json["responseCode"] as? String)?.let { InAppMessageResponseCodeAndroid.fromJson(it) } ?: InAppMessageResponseCodeAndroid.NoActionNeeded, + responseCode = (json["responseCode"] as? String)?.let { InAppMessageResponseCodeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for InAppMessageResponseCodeAndroid"), ) } } @@ -2595,7 +2595,7 @@ public data class ProductAndroid( displayPrice = json["displayPrice"] as? String ?: "", id = json["id"] as? String ?: "", nameAndroid = json["nameAndroid"] as? String ?: "", - platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, + platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Android, price = (json["price"] as? Number)?.toDouble(), productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, @@ -2671,7 +2671,7 @@ public data class ProductIOS( subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, - typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: ProductTypeIOS.Consumable, + typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductTypeIOS"), ) } } @@ -2735,12 +2735,12 @@ public data class ProductSubscriptionAndroid( displayPrice = json["displayPrice"] as? String ?: "", id = json["id"] as? String ?: "", nameAndroid = json["nameAndroid"] as? String ?: "", - platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, + platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Android, price = (json["price"] as? Number)?.toDouble(), productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(), title = json["title"] as? String ?: "", - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, ) } } @@ -2833,8 +2833,8 @@ public data class ProductSubscriptionIOS( subscriptionPeriodNumberIOS = json["subscriptionPeriodNumberIOS"] as? String, subscriptionPeriodUnitIOS = (json["subscriptionPeriodUnitIOS"] as? String)?.let { SubscriptionPeriodIOS.fromJson(it) }, title = json["title"] as? String ?: "", - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, - typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: ProductTypeIOS.Consumable, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, + typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductTypeIOS"), ) } } @@ -3711,7 +3711,7 @@ public data class SubscriptionOffer( pricingPhasesAndroid = (json["pricingPhasesAndroid"] as? Map)?.let { PricingPhasesAndroid.fromJson(it) }, signatureIOS = json["signatureIOS"] as? String, timestampIOS = (json["timestampIOS"] as? Number)?.toDouble(), - type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory, + type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for DiscountOfferType"), ) } } @@ -4161,7 +4161,7 @@ public data class VerifyPurchaseWithProviderResult( return VerifyPurchaseWithProviderResult( errors = (json["errors"] as? List<*>)?.mapNotNull { (it as? Map)?.let { VerifyPurchaseWithProviderError.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for VerifyPurchaseWithProviderError") }, iapkit = (json["iapkit"] as? Map)?.let { RequestVerifyPurchaseWithIapkitResult.fromJson(it) }, - provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: PurchaseVerificationProvider.Iapkit, + provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider"), ) } } @@ -4495,10 +4495,10 @@ public data class LaunchExternalLinkParamsAndroid( ) { companion object { fun fromJson(json: Map): LaunchExternalLinkParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String - val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: ExternalLinkLaunchModeAndroid.Unspecified - val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: ExternalLinkTypeAndroid.Unspecified + val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkLaunchModeAndroid") + val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkTypeAndroid") val linkUri = json["linkUri"] as? String if (linkUri == null) return null return LaunchExternalLinkParamsAndroid( @@ -4668,7 +4668,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4733,7 +4733,7 @@ public data class RequestPurchaseIosProps( val appAccountToken = json["appAccountToken"] as? String val quantity = (json["quantity"] as? Number)?.toInt() val sku = json["sku"] as? String - val withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } + val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null return RequestPurchaseIosProps( advancedCommerceData = advancedCommerceData, @@ -4839,8 +4839,8 @@ public data class RequestPurchasePropsByPlatforms( companion object { fun fromJson(json: Map): RequestPurchasePropsByPlatforms { return RequestPurchasePropsByPlatforms( - apple = (json["apple"] as? Map)?.let { RequestPurchaseIosProps.fromJson(it) }, - google = (json["google"] as? Map)?.let { RequestPurchaseAndroidProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestPurchaseIosProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestPurchaseIosProps") }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestPurchaseAndroidProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestPurchaseAndroidProps") }, ) } } @@ -4900,7 +4900,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4908,7 +4908,7 @@ public data class RequestSubscriptionAndroidProps( val purchaseToken = json["purchaseToken"] as? String val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } val subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } } - val subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } + val subscriptionProductReplacementParams = json["subscriptionProductReplacementParams"]?.let { value -> (value as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for SubscriptionProductReplacementParamsAndroid") } if (skus == null) return null return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, @@ -4987,11 +4987,11 @@ public data class RequestSubscriptionIosProps( val appAccountToken = json["appAccountToken"] as? String val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } val compactJWS = json["compactJWS"] as? String - val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } + val promotionalOfferJWS = json["promotionalOfferJWS"]?.let { value -> (value as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for PromotionalOfferJWSInputIOS") } val quantity = (json["quantity"] as? Number)?.toInt() val sku = json["sku"] as? String - val winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } - val withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } + val winBackOffer = json["winBackOffer"]?.let { value -> (value as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for WinBackOfferInputIOS") } + val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null return RequestSubscriptionIosProps( advancedCommerceData = advancedCommerceData, @@ -5044,8 +5044,8 @@ public data class RequestSubscriptionPropsByPlatforms( companion object { fun fromJson(json: Map): RequestSubscriptionPropsByPlatforms { return RequestSubscriptionPropsByPlatforms( - apple = (json["apple"] as? Map)?.let { RequestSubscriptionIosProps.fromJson(it) }, - google = (json["google"] as? Map)?.let { RequestSubscriptionAndroidProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestSubscriptionIosProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestSubscriptionIosProps") }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestSubscriptionAndroidProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestSubscriptionAndroidProps") }, ) } } @@ -5219,11 +5219,11 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( - amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, + amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, apiKey = json["apiKey"] as? String, - apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAppleProps") }, baseUrl = json["baseUrl"] as? String, - google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitGoogleProps") }, includeClientPayload = json["includeClientPayload"] as? Boolean, ) } @@ -5421,9 +5421,9 @@ public data class VerifyPurchaseProps( companion object { fun fromJson(json: Map): VerifyPurchaseProps { return VerifyPurchaseProps( - apple = (json["apple"] as? Map)?.let { VerifyPurchaseAppleOptions.fromJson(it) }, - google = (json["google"] as? Map)?.let { VerifyPurchaseGoogleOptions.fromJson(it) }, - horizon = (json["horizon"] as? Map)?.let { VerifyPurchaseHorizonOptions.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { VerifyPurchaseAppleOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseAppleOptions") }, + google = json["google"]?.let { value -> (value as? Map)?.let { VerifyPurchaseGoogleOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseGoogleOptions") }, + horizon = json["horizon"]?.let { value -> (value as? Map)?.let { VerifyPurchaseHorizonOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseHorizonOptions") }, ) } } @@ -5441,8 +5441,8 @@ public data class VerifyPurchaseWithProviderProps( ) { companion object { fun fromJson(json: Map): VerifyPurchaseWithProviderProps? { - val iapkit = (json["iapkit"] as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } - val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: PurchaseVerificationProvider.Iapkit + val iapkit = json["iapkit"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitProps") } + val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider") return VerifyPurchaseWithProviderProps( iapkit = iapkit, provider = provider, diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/SubscriptionReplacementModeAndroidExt.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/SubscriptionReplacementModeAndroidExt.kt index 6fec98511..146b3bd5f 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/SubscriptionReplacementModeAndroidExt.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/SubscriptionReplacementModeAndroidExt.kt @@ -18,7 +18,8 @@ import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams.Subs */ internal fun SubscriptionReplacementModeAndroid.toReplacementModeConstant(): Int { return when (this) { - SubscriptionReplacementModeAndroid.UnknownReplacementMode -> ReplacementMode.UNKNOWN_REPLACEMENT_MODE + SubscriptionReplacementModeAndroid.UnknownReplacementMode -> + throw IllegalArgumentException("A concrete subscription replacement mode is required.") SubscriptionReplacementModeAndroid.WithTimeProration -> ReplacementMode.WITH_TIME_PRORATION SubscriptionReplacementModeAndroid.ChargeProratedPrice -> ReplacementMode.CHARGE_PRORATED_PRICE SubscriptionReplacementModeAndroid.WithoutProration -> ReplacementMode.WITHOUT_PRORATION diff --git a/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt b/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt index 861f95df4..170d55a58 100644 --- a/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt +++ b/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt @@ -2,6 +2,7 @@ package dev.hyo.openiap import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Test /** @@ -20,11 +21,10 @@ import org.junit.Test class SubscriptionReplacementModeTest { @Test - fun `UnknownReplacementMode maps to native UNKNOWN_REPLACEMENT_MODE`() { - assertEquals( - ReplacementMode.UNKNOWN_REPLACEMENT_MODE, + fun `UnknownReplacementMode is rejected before purchase`() { + assertThrows(IllegalArgumentException::class.java) { SubscriptionReplacementModeAndroid.UnknownReplacementMode.toReplacementModeConstant() - ) + } } @Test @@ -77,15 +77,19 @@ class SubscriptionReplacementModeTest { @Test fun `all enum values are covered by toReplacementModeConstant`() { - assertEquals(7, SubscriptionReplacementModeAndroid.entries.size) - for (mode in SubscriptionReplacementModeAndroid.entries) { + val concreteModes = SubscriptionReplacementModeAndroid.entries - + SubscriptionReplacementModeAndroid.UnknownReplacementMode + assertEquals(6, concreteModes.size) + for (mode in concreteModes) { mode.toReplacementModeConstant() } } @Test fun `subscription replacement params use the native Billing builder`() { - for (mode in SubscriptionReplacementModeAndroid.entries) { + val concreteModes = SubscriptionReplacementModeAndroid.entries - + SubscriptionReplacementModeAndroid.UnknownReplacementMode + for (mode in concreteModes) { val params = SubscriptionProductReplacementParamsAndroid( oldProductId = "old.product", replacementMode = mode, @@ -96,6 +100,34 @@ class SubscriptionReplacementModeTest { } } + @Test + fun `subscription replacement params reject unknown mode`() { + val params = SubscriptionProductReplacementParamsAndroid( + oldProductId = "old.product", + replacementMode = SubscriptionReplacementModeAndroid.UnknownReplacementMode, + ) + + assertThrows(IllegalArgumentException::class.java) { + params.toBillingSubscriptionProductReplacementParams() + } + } + + @Test + fun `decoded unknown and missing replacement modes are rejected`() { + val inputs = listOf( + mapOf("oldProductId" to "old.product", "replacementMode" to "future-mode"), + mapOf("oldProductId" to "old.product"), + ) + + for (input in inputs) { + val params = requireNotNull(SubscriptionProductReplacementParamsAndroid.fromJson(input)) + assertEquals(SubscriptionReplacementModeAndroid.UnknownReplacementMode, params.replacementMode) + assertThrows(IllegalArgumentException::class.java) { + params.toBillingSubscriptionProductReplacementParams() + } + } + } + @Test fun `SubscriptionReplacementModeAndroid fromJson parses correctly`() { assertEquals(SubscriptionReplacementModeAndroid.UnknownReplacementMode, SubscriptionReplacementModeAndroid.fromJson("unknown-replacement-mode")) diff --git a/packages/gql/codegen/plugins/base-plugin.ts b/packages/gql/codegen/plugins/base-plugin.ts index e701e3887..b21570645 100644 --- a/packages/gql/codegen/plugins/base-plugin.ts +++ b/packages/gql/codegen/plugins/base-plugin.ts @@ -243,22 +243,45 @@ export abstract class CodegenPlugin { return irEnum.values.find((value) => value.name.toLowerCase().startsWith('unknown')) ?? null; } - protected typeHasRequiredEnumWithoutUnknown(typeName: string, schema: IRSchema): boolean { + protected typeHasRequiredEnumWithoutUnknown(typeName: string, schema: IRSchema, visited = new Set()): boolean { + if (visited.has(typeName)) return false; const container = [...schema.objects, ...schema.inputs].find((candidate) => candidate.name === typeName); + const isInputContainer = schema.inputs.some((candidate) => candidate.name === typeName); + const nestedVisited = new Set(visited).add(typeName); return ( container?.fields.some((field) => { - if (field.type.kind !== 'enum' || field.type.nullable || !field.type.name) return false; - const irEnum = schema.enums.find((candidate) => candidate.name === field.type.name); - return irEnum !== undefined && this.enumUnknownValue(irEnum) === null; + if (field.type.kind === 'list') { + const element = field.type.elementType; + if (!element || (!isInputContainer && element.nullable) || !element.name) return false; + if (element.kind === 'enum') { + const irEnum = schema.enums.find((candidate) => candidate.name === element.name); + return irEnum !== undefined && this.enumUnknownValue(irEnum) === null; + } + if (!['object', 'input'].includes(element.kind)) return false; + return this.typeHasRequiredEnumWithoutUnknown(element.name, schema, nestedVisited); + } + if (field.type.nullable || !field.type.name) return false; + if (field.type.kind === 'enum') { + const irEnum = schema.enums.find((candidate) => candidate.name === field.type.name); + return irEnum !== undefined && this.enumUnknownValue(irEnum) === null; + } + if (!['object', 'input'].includes(field.type.kind)) return false; + return this.typeHasRequiredEnumWithoutUnknown(field.type.name, schema, nestedVisited); }) ?? false ); } protected typeNeedsTolerantNullableDecoder(typeName: string, schema: IRSchema): boolean { if (!this.typeHasRequiredEnumWithoutUnknown(typeName, schema)) return false; - return [...schema.objects, ...schema.inputs].some((container) => + if (!schema.objects.some((container) => container.name === typeName)) return false; + return schema.objects.some((container) => container.fields.some( - (field) => field.type.nullable && ['object', 'input'].includes(field.type.kind) && field.type.name === typeName, + (field) => + (field.type.nullable && field.type.kind === 'object' && field.type.name === typeName) || + (field.type.kind === 'list' && + field.type.elementType?.nullable === true && + field.type.elementType.kind === 'object' && + field.type.elementType.name === typeName), ), ); } diff --git a/packages/gql/codegen/plugins/csharp.ts b/packages/gql/codegen/plugins/csharp.ts index 9de9612da..3ceb5442f 100644 --- a/packages/gql/codegen/plugins/csharp.ts +++ b/packages/gql/codegen/plugins/csharp.ts @@ -522,9 +522,16 @@ export class CSharpPlugin extends CodegenPlugin { if ( field.type.nullable && ['object', 'input'].includes(field.type.kind) && - this.typeHasRequiredEnumWithoutUnknown(field.type.name!, this.schema) + this.typeNeedsTolerantNullableDecoder(field.type.name!, this.schema) ) { this.emit(` [JsonConverter(typeof(${field.type.name}NullableJsonConverter))]`); + } else if ( + field.type.kind === 'list' && + field.type.elementType?.nullable === true && + field.type.elementType.kind === 'object' && + this.typeNeedsTolerantNullableDecoder(field.type.elementType.name!, this.schema) + ) { + this.emit(` [JsonConverter(typeof(${field.type.elementType.name}NullableElementListJsonConverter))]`); } // Required vs. nullable — non-nullable scalars/objects get the C# @@ -664,6 +671,53 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(' JsonSerializer.Serialize(writer, value, options);'); this.emit('}'); this.emit(''); + + const needsListConverter = this.schema.objects.some((container) => + container.fields.some( + (field) => + field.type.kind === 'list' && + field.type.elementType?.nullable === true && + field.type.elementType.kind === 'object' && + field.type.elementType.name === typeName, + ), + ); + if (!needsListConverter) return; + this.emit(`public sealed class ${typeName}NullableElementListJsonConverter : JsonConverter>`); + this.emit('{'); + this.emit( + ` public override IReadOnlyList<${typeName}?> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)`, + ); + this.emit(' {'); + this.emit(' using var document = JsonDocument.ParseValue(ref reader);'); + this.emit(' if (document.RootElement.ValueKind != JsonValueKind.Array)'); + this.emit(' {'); + this.emit(` throw new JsonException("Expected an array of ${typeName} values.");`); + this.emit(' }'); + this.emit(` var values = new List<${typeName}?>();`); + this.emit(' foreach (var element in document.RootElement.EnumerateArray())'); + this.emit(' {'); + this.emit(' try'); + this.emit(' {'); + this.emit(` values.Add(element.Deserialize<${typeName}>(options));`); + this.emit(' }'); + this.emit(' catch (JsonException)'); + this.emit(' {'); + this.emit(' values.Add(null);'); + this.emit(' }'); + this.emit(' catch (InvalidOperationException)'); + this.emit(' {'); + this.emit(' values.Add(null);'); + this.emit(' }'); + this.emit(' }'); + this.emit(' return values;'); + this.emit(' }'); + this.emit(''); + this.emit( + ` public override void Write(Utf8JsonWriter writer, IReadOnlyList<${typeName}?> value, JsonSerializerOptions options) =>`, + ); + this.emit(' JsonSerializer.Serialize(writer, value, options);'); + this.emit('}'); + this.emit(''); } private generateCustomInput(irInput: IRInput): void { diff --git a/packages/gql/codegen/plugins/dart.ts b/packages/gql/codegen/plugins/dart.ts index b58d93dec..5b03564ef 100644 --- a/packages/gql/codegen/plugins/dart.ts +++ b/packages/gql/codegen/plugins/dart.ts @@ -831,7 +831,7 @@ export class DartPlugin extends CodegenPlugin { } if (['object', 'input', 'interface', 'union'].includes(type.kind)) { - if (type.nullable && this.typeHasRequiredEnumWithoutUnknown(type.name!, this.schema)) { + if (type.nullable && this.typeNeedsTolerantNullableDecoder(type.name!, this.schema)) { return `${sourceExpr} is Map ? ${type.name}._tryFromJson(${sourceExpr} as Map) : null`; } return type.nullable diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts index 1031dab8b..73a15333d 100644 --- a/packages/gql/codegen/plugins/gdscript.ts +++ b/packages/gql/codegen/plugins/gdscript.ts @@ -41,7 +41,9 @@ export class GDScriptPlugin extends CodegenPlugin { mapType(type: IRType): string { if (type.kind === 'list') { - const elementType = this.mapType(type.elementType!); + const element = type.elementType!; + const isNullableEnum = element.nullable && (element.kind === 'enum' || this.enumNames.has(element.name!)); + const elementType = isNullableEnum ? 'Variant' : this.mapType(element); return `Array[${elementType}]`; } if (type.kind === 'scalar') { @@ -86,9 +88,9 @@ export class GDScriptPlugin extends CodegenPlugin { return null; } - // Enums - no default, they should be set explicitly + // Neutral enum members keep missing values from becoming an unrelated case. if (type.kind === 'enum' || this.enumNames.has(type.name!)) { - return null; + return this.getEnumUnknownFallback(type.name!) ?? this.getEnumEmptyFallback(type.name!); } // Scalars get type-appropriate defaults @@ -320,6 +322,12 @@ export class GDScriptPlugin extends CodegenPlugin { return unknown ? `${typeName}.${this.enumValueCase(unknown.name)}` : null; } + private getEnumEmptyFallback(typeName: string): string | null { + const irEnum = this.schema.enums.find((candidate) => candidate.name === typeName); + const empty = irEnum?.values.find((value) => value.name.toLowerCase() === 'empty'); + return empty ? `${typeName}.${this.enumValueCase(empty.name)}` : null; + } + private emitEnumFromDictAssignment(indent: string, target: string, typeName: string, sourceExpression: string): void { const enumReverseLookup = toConstantCase(typeName) + '_FROM_STRING'; const fallback = this.getEnumUnknownFallback(typeName); @@ -335,6 +343,20 @@ export class GDScriptPlugin extends CodegenPlugin { return; } + const emptyFallback = this.getEnumEmptyFallback(typeName); + if (emptyFallback) { + const enumValues = toConstantCase(typeName) + '_VALUES'; + this.emit(`${indent}if enum_str is String and ${enumReverseLookup}.has(enum_str):`); + this.emit(`${indent}\t${target} = ${enumReverseLookup}[enum_str]`); + this.emit(`${indent}elif enum_str is int and ${enumValues}.has(enum_str):`); + this.emit(`${indent}\t${target} = enum_str`); + this.emit(`${indent}elif not enum_str is String:`); + this.emit(`${indent}\t${target} = ${emptyFallback}`); + this.emit(`${indent}else:`); + this.emit(`${indent}\tpush_error("Unknown ${typeName} value: %s" % enum_str)`); + return; + } + this.emit(`${indent}if enum_str is String and ${enumReverseLookup}.has(enum_str):`); this.emit(`${indent}\t${target} = ${enumReverseLookup}[enum_str]`); this.emit(`${indent}else:`); @@ -404,9 +426,11 @@ export class GDScriptPlugin extends CodegenPlugin { // from_dict method this.emit(''); - this.emit(`\tstatic func from_dict(data: Dictionary) -> ${irObject.name}:`); - if (this.typeNeedsTolerantNullableDecoder(irObject.name, this.schema)) { - this.emitRequiredEnumGuards(fields); + const hasStrictRequiredEnum = this.typeHasRequiredEnumWithoutUnknown(irObject.name, this.schema); + const decoderOptions = hasStrictRequiredEnum ? ', report_errors: bool = true' : ''; + this.emit(`\tstatic func from_dict(data: Dictionary${decoderOptions}) -> ${irObject.name}:`); + if (hasStrictRequiredEnum) { + this.emitRequiredEnumGuards(irObject.name, fields); } this.emit(`\t\tvar obj = ${irObject.name}.new()`); for (const field of fields) { @@ -415,6 +439,12 @@ export class GDScriptPlugin extends CodegenPlugin { } this.emit('\t\treturn obj'); + if (this.typeNeedsGdscriptTolerantDecoder(irObject.name)) { + this.emit(''); + this.emit(`\tstatic func from_dict_or_null(data: Dictionary) -> Variant:`); + this.emit('\t\treturn from_dict(data, false)'); + } + // to_dict method this.emit(''); this.emit('\tfunc to_dict() -> Dictionary:'); @@ -438,13 +468,7 @@ export class GDScriptPlugin extends CodegenPlugin { if (type.kind === 'list') { this.generateListFromDictAssignment(type, graphqlName, fieldName); } else if (this.isObjectOrInput(type)) { - const typeName = type.name!; - this.emit(`\t\t\tif data["${graphqlName}"] is Dictionary:`); - this.emit(`\t\t\t\tobj.${fieldName} = ${typeName}.from_dict(data["${graphqlName}"])`); - if (!type.nullable || !this.typeHasRequiredEnumWithoutUnknown(typeName, this.schema)) { - this.emit(`\t\t\telse:`); - this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`); - } + this.generateNestedFromDictAssignment(type, graphqlName, fieldName); } else if (type.kind === 'enum') { this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`); } else { @@ -452,32 +476,131 @@ export class GDScriptPlugin extends CodegenPlugin { } } - private generateListFromDictAssignment(type: IRType, graphqlName: string, fieldName: string, indent = '\t\t\t'): void { + private generateNestedFromDictAssignment(type: IRType, graphqlName: string, fieldName: string): void { + const typeName = type.name!; + const needsStrictValidation = this.typeHasRequiredEnumWithoutUnknown(typeName, this.schema); + const isRequestInput = this.inputNames.has(typeName); + const decoder = type.nullable && needsStrictValidation && !isRequestInput ? 'from_dict_or_null' : 'from_dict'; + this.emit(`\t\t\tif data["${graphqlName}"] is Dictionary:`); + if (isRequestInput) { + const decodedName = `decoded_${fieldName}`; + this.emit(`\t\t\t\tvar ${decodedName} = ${typeName}.from_dict(data["${graphqlName}"])`); + this.emit(`\t\t\t\tif ${decodedName} == null:`); + this.emit(`\t\t\t\t\tpush_error("Invalid input ${typeName} value for ${graphqlName}")`); + this.emit('\t\t\t\t\treturn null'); + this.emit(`\t\t\t\tobj.${fieldName} = ${decodedName}`); + this.emit('\t\t\telse:'); + this.emit(`\t\t\t\tpush_error("Expected ${graphqlName} to be a ${typeName} dictionary")`); + this.emit('\t\t\t\treturn null'); + return; + } + if (!type.nullable && needsStrictValidation) { + const decodedName = `decoded_${fieldName}`; + this.emit(`\t\t\t\tvar ${decodedName} = ${typeName}.${decoder}(data["${graphqlName}"], report_errors)`); + this.emit(`\t\t\t\tif ${decodedName} == null:`); + this.emit('\t\t\t\t\tif report_errors:'); + this.emit(`\t\t\t\t\t\tpush_error("Invalid required ${typeName} value for ${graphqlName}")`); + this.emit('\t\t\t\t\treturn null'); + this.emit(`\t\t\t\tobj.${fieldName} = ${decodedName}`); + this.emit('\t\t\telse:'); + this.emit('\t\t\t\tif report_errors:'); + this.emit(`\t\t\t\t\tpush_error("Expected ${graphqlName} to be a ${typeName} dictionary")`); + this.emit('\t\t\t\treturn null'); + this.emit('\t\telse:'); + this.emit('\t\t\tif report_errors:'); + this.emit(`\t\t\t\tpush_error("Missing required ${graphqlName} value")`); + this.emit('\t\t\treturn null'); + return; + } + + this.emit(`\t\t\t\tobj.${fieldName} = ${typeName}.${decoder}(data["${graphqlName}"])`); + if (!type.nullable || !needsStrictValidation) { + this.emit('\t\t\telse:'); + this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`); + } + } + + private generateListFromDictAssignment( + type: IRType, + graphqlName: string, + fieldName: string, + indent = '\t\t\t', + reportErrorsExpression: string | null = 'report_errors', + isRequestContext = false, + ): void { const elementType = type.elementType!; const elementTypeName = elementType.name!; const gdElementType = this.mapType(elementType); + const isEnumElement = elementType.kind === 'enum' || this.enumNames.has(elementTypeName); + const listElementType = elementType.nullable && isEnumElement ? 'Variant' : gdElementType; const listIndent = `${indent}\t`; const itemIndent = `${listIndent}\t`; this.emit(`${indent}if data["${graphqlName}"] is Array:`); - this.emit(`${listIndent}var arr: Array[${gdElementType}] = []`); + this.emit(`${listIndent}var arr: Array[${listElementType}] = []`); this.emit(`${listIndent}for item in data["${graphqlName}"]:`); if (this.isObjectOrInput(elementType)) { + const needsStrictValidation = this.typeHasRequiredEnumWithoutUnknown(elementTypeName, this.schema); this.emit(`${itemIndent}if item is Dictionary:`); - this.emit(`${itemIndent}\tarr.append(${elementTypeName}.from_dict(item))`); + if (needsStrictValidation) { + const decodedName = `decoded_${this.getGdscriptFieldName(elementTypeName)}`; + const isRequestInput = this.inputNames.has(elementTypeName); + if (elementType.nullable && !isRequestInput) { + this.emit(`${itemIndent}\tvar ${decodedName} = ${elementTypeName}.from_dict_or_null(item)`); + this.emit(`${itemIndent}\tarr.append(${decodedName})`); + } else { + const decoderArgs = isRequestInput || reportErrorsExpression === null ? 'item' : `item, ${reportErrorsExpression}`; + this.emit(`${itemIndent}\tvar ${decodedName} = ${elementTypeName}.from_dict(${decoderArgs})`); + this.emit(`${itemIndent}\tif ${decodedName} == null:`); + this.emit(`${itemIndent}\t\treturn null`); + this.emit(`${itemIndent}\tarr.append(${decodedName})`); + } + } else { + this.emit(`${itemIndent}\tarr.append(${elementTypeName}.from_dict(item))`); + } this.emit(`${itemIndent}elif item is ${elementTypeName}:`); this.emit(`${itemIndent}\tarr.append(item)`); - } else if (elementType.kind === 'enum' || this.enumNames.has(elementTypeName)) { + this.emit(`${itemIndent}else:`); + if (elementType.nullable && !isRequestContext) { + this.emit(`${itemIndent}\tarr.append(null)`); + } else { + this.emitListDecodeFailure( + itemIndent, + elementTypeName, + graphqlName, + needsStrictValidation ? reportErrorsExpression : null, + ); + } + } else if (isEnumElement) { const enumReverseLookup = toConstantCase(elementTypeName) + '_FROM_STRING'; + const enumValues = toConstantCase(elementTypeName) + '_VALUES'; const fallback = this.getEnumUnknownFallback(elementTypeName); if (fallback) { - this.emit(`${itemIndent}if item is String:`); + if (elementType.nullable) { + this.emit(`${itemIndent}if item == null:`); + this.emit(`${itemIndent}\tarr.append(null)`); + } + this.emit(`${itemIndent}${elementType.nullable ? 'elif' : 'if'} item is String:`); this.emit(`${itemIndent}\tarr.append(${enumReverseLookup}.get(item, ${fallback}))`); + this.emit(`${itemIndent}elif item is int and ${enumValues}.has(item):`); + this.emit(`${itemIndent}\tarr.append(item)`); + this.emit(`${itemIndent}elif item is int:`); + this.emit(`${itemIndent}\tarr.append(${fallback})`); } else { - this.emit(`${itemIndent}if item is String and ${enumReverseLookup}.has(item):`); + if (elementType.nullable) { + this.emit(`${itemIndent}if item == null:`); + this.emit(`${itemIndent}\tarr.append(null)`); + } + this.emit(`${itemIndent}${elementType.nullable ? 'elif' : 'if'} item is String and ${enumReverseLookup}.has(item):`); this.emit(`${itemIndent}\tarr.append(${enumReverseLookup}[item])`); + this.emit(`${itemIndent}elif item is int and ${enumValues}.has(item):`); + this.emit(`${itemIndent}\tarr.append(item)`); + this.emit(`${itemIndent}else:`); + if (elementType.nullable && !isRequestContext) { + this.emit(`${itemIndent}\tarr.append(null)`); + } else { + this.emitListDecodeFailure(itemIndent, elementTypeName, graphqlName, reportErrorsExpression); + } } - this.emit(`${itemIndent}elif item is int:`); - this.emit(`${itemIndent}\tarr.append(item)`); } else { switch (gdElementType) { case 'String': @@ -509,6 +632,21 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`${listIndent}obj.${fieldName} = arr`); } + private emitListDecodeFailure( + itemIndent: string, + elementTypeName: string, + graphqlName: string, + reportErrorsExpression: string | null, + ): void { + if (reportErrorsExpression === null) { + this.emit(`${itemIndent}\tpush_error("Invalid ${elementTypeName} list value for ${graphqlName}")`); + } else { + this.emit(`${itemIndent}\tif ${reportErrorsExpression}:`); + this.emit(`${itemIndent}\t\tpush_error("Invalid ${elementTypeName} list value for ${graphqlName}")`); + } + this.emit(`${itemIndent}\treturn null`); + } + private generateToDictField(field: IRField, fieldName: string): void { const graphqlName = field.name; const type = field.type; @@ -631,9 +769,11 @@ export class GDScriptPlugin extends CodegenPlugin { // from_dict method this.emit(''); - this.emit(`\tstatic func from_dict(data: Dictionary) -> ${irInput.name}:`); - if (this.typeNeedsTolerantNullableDecoder(irInput.name, this.schema)) { - this.emitRequiredEnumGuards(fields); + const hasStrictRequiredEnum = this.typeHasRequiredEnumWithoutUnknown(irInput.name, this.schema); + const decoderOptions = hasStrictRequiredEnum ? ', report_errors: bool = true' : ''; + this.emit(`\tstatic func from_dict(data: Dictionary${decoderOptions}) -> ${irInput.name}:`); + if (hasStrictRequiredEnum) { + this.emitRequiredEnumGuards(irInput.name, fields); } this.emit(`\t\tvar obj = ${irInput.name}.new()`); for (const field of fields) { @@ -656,24 +796,45 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(''); } - private emitRequiredEnumGuards(fields: IRField[]): void { + private emitRequiredEnumGuards(containerName: string, fields: IRField[]): void { for (const field of fields) { if (field.type.kind !== 'enum' || field.type.nullable || !field.type.name) continue; const irEnum = this.schema.enums.find((candidate) => candidate.name === field.type.name); if (!irEnum || this.enumUnknownValue(irEnum)) continue; const source = `data["${field.name}"]`; const lookup = `${toConstantCase(field.type.name)}_FROM_STRING`; + const values = `${toConstantCase(field.type.name)}_VALUES`; + const validValue = `((${source} is String and ${lookup}.has(${source})) or (${source} is int and ${values}.has(${source})))`; + const hasEmptyFallback = irEnum.values.some((value) => value.name.toLowerCase() === 'empty'); if (field.defaultValue === undefined) { - this.emit(`\t\tif not data.has("${field.name}") or not ${source} is String or not ${lookup}.has(${source}):`); + const invalid = hasEmptyFallback + ? `data.has("${field.name}") and ${source} != null and not ${validValue}` + : `not data.has("${field.name}") or not ${validValue}`; + this.emit(`\t\tif ${invalid}:`); } else { - this.emit( - `\t\tif data.has("${field.name}") and ${source} != null and (not ${source} is String or not ${lookup}.has(${source})):`, - ); + this.emit(`\t\tif data.has("${field.name}") and ${source} != null and not ${validValue}:`); } + this.emit('\t\t\tif report_errors:'); + this.emit(`\t\t\t\tpush_error("Invalid ${containerName}.${field.name} enum value")`); this.emit('\t\t\treturn null'); } } + private typeNeedsGdscriptTolerantDecoder(typeName: string): boolean { + if (!this.typeHasRequiredEnumWithoutUnknown(typeName, this.schema)) return false; + if (!this.objectNames.has(typeName)) return false; + if (this.typeNeedsTolerantNullableDecoder(typeName, this.schema)) return true; + return [...this.schema.objects, ...this.schema.inputs].some((container) => + container.fields.some( + (field) => + field.type.kind === 'list' && + field.type.elementType?.nullable === true && + field.type.elementType?.kind === 'object' && + field.type.elementType?.name === typeName, + ), + ); + } + /** * RequestPurchaseProps is a tagged union even though GraphQL input objects * cannot express one-of semantics. Reject ambiguous or mismatched @@ -762,15 +923,9 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`\t\tif data.has("${graphqlName}") and data["${graphqlName}"] != null:`); if (type.kind === 'list') { - this.generateListFromDictAssignment(type, graphqlName, fieldName); + this.generateListFromDictAssignment(type, graphqlName, fieldName, '\t\t\t', 'report_errors', true); } else if (this.isObjectOrInput(type)) { - const typeName = type.name!; - this.emit(`\t\t\tif data["${graphqlName}"] is Dictionary:`); - this.emit(`\t\t\t\tobj.${fieldName} = ${typeName}.from_dict(data["${graphqlName}"])`); - if (!type.nullable || !this.typeHasRequiredEnumWithoutUnknown(typeName, this.schema)) { - this.emit(`\t\t\telse:`); - this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`); - } + this.generateNestedFromDictAssignment(type, graphqlName, fieldName); } else if (type.kind === 'enum') { this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`); } else { @@ -858,7 +1013,7 @@ export class GDScriptPlugin extends CodegenPlugin { const argSnakeName = this.escapeKeyword(toSnakeCase(arg.name)); this.emit(`\t\t\t\tif data.has("${arg.name}") and data["${arg.name}"] != null:`); if (arg.type.kind === 'list') { - this.generateListFromDictAssignment(arg.type, arg.name, argSnakeName, '\t\t\t\t\t'); + this.generateListFromDictAssignment(arg.type, arg.name, argSnakeName, '\t\t\t\t\t', null, true); } else if (arg.type.kind === 'enum') { this.emitEnumFromDictAssignment('\t\t\t\t\t', `obj.${argSnakeName}`, arg.type.name!, `data["${arg.name}"]`); } else { diff --git a/packages/gql/codegen/plugins/kotlin.ts b/packages/gql/codegen/plugins/kotlin.ts index 7ec0de31f..ab008e54c 100644 --- a/packages/gql/codegen/plugins/kotlin.ts +++ b/packages/gql/codegen/plugins/kotlin.ts @@ -330,7 +330,7 @@ export class KotlinPlugin extends CodegenPlugin { this.emit(` fun fromJson(json: Map): ${irObject.name} {`); this.emit(` return ${irObject.name}(`); - const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irObject.name, this.schema); + const rejectMissingStrictEnums = this.typeHasRequiredEnumWithoutUnknown(irObject.name, this.schema); for (const field of sortedFields) { const propertyName = this.escapeKeyword(this.fieldNameCase(field.name)); @@ -339,7 +339,7 @@ export class KotlinPlugin extends CodegenPlugin { `json["${field.name}"]`, false, false, - undefined, + this.buildDefaultValueExpression(field), rejectMissingStrictEnums, ); this.emit(` ${propertyName} = ${expression},`); @@ -436,14 +436,14 @@ export class KotlinPlugin extends CodegenPlugin { this.emit(' companion object {'); this.emit(` fun fromJson(json: Map): ${irObject.name} {`); this.emit(` return ${irObject.name}(`); - const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irObject.name, this.schema); + const rejectMissingStrictEnums = this.typeHasRequiredEnumWithoutUnknown(irObject.name, this.schema); for (const value of [...primaryFields, ...extraFields]) { const expression = this.buildFromJsonExpression( value.type, `json["${value.name}"]`, false, false, - undefined, + this.buildDefaultValueExpression(value), rejectMissingStrictEnums, ); this.emit(` ${value.name} = ${expression},`); @@ -506,7 +506,7 @@ export class KotlinPlugin extends CodegenPlugin { // Sort fields alphabetically for Kotlin const sortedFields = [...irInput.fields].sort((a, b) => a.name.localeCompare(b.name)); - const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irInput.name, this.schema); + const rejectMissingStrictEnums = this.typeHasRequiredEnumWithoutUnknown(irInput.name, this.schema); this.generateDocComment(irInput.description); this.generateDeprecationAnnotation(irInput.description); @@ -727,7 +727,7 @@ export class KotlinPlugin extends CodegenPlugin { } private generateStandardInput(irInput: IRInput): void { - const rejectMissingStrictEnums = this.typeNeedsTolerantNullableDecoder(irInput.name, this.schema); + const rejectMissingStrictEnums = this.typeHasRequiredEnumWithoutUnknown(irInput.name, this.schema); this.generateDocComment(irInput.description); this.generateDeprecationAnnotation(irInput.description); this.emit(`public data class ${irInput.name}(`); @@ -1146,8 +1146,11 @@ export class KotlinPlugin extends CodegenPlugin { if (['object', 'input', 'interface', 'union'].includes(type.kind)) { const callTarget = type.name!; + if (type.kind === 'input' && type.nullable) { + return `${sourceExpr}?.let { value -> (value as? Map)?.let { ${callTarget}.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for ${callTarget}") }`; + } if (type.nullable || forNullableFromJson) { - if (this.typeHasRequiredEnumWithoutUnknown(callTarget, this.schema)) { + if (this.typeNeedsTolerantNullableDecoder(callTarget, this.schema)) { return `(${sourceExpr} as? Map)?.let { runCatching { ${callTarget}.fromJson(it) }.getOrNull() }`; } return `(${sourceExpr} as? Map)?.let { ${callTarget}.fromJson(it) }`; diff --git a/packages/gql/src/enum-decoder-compatibility.test.ts b/packages/gql/src/enum-decoder-compatibility.test.ts index e86d21fb9..78a4e236c 100644 --- a/packages/gql/src/enum-decoder-compatibility.test.ts +++ b/packages/gql/src/enum-decoder-compatibility.test.ts @@ -23,13 +23,22 @@ const strictFormat: IREnum = { ], }; +const platform: IREnum = { + name: 'Platform', + isErrorCode: false, + values: [ + { name: 'Ios', rawValue: 'ios', legacyAliases: [] }, + { name: 'Android', rawValue: 'android', legacyAliases: [] }, + ], +}; + function field(name: string, type: IRType): IRField { return { name, type, isOverride: false }; } function decoderSchema(): IRSchema { return { - enums: [tolerantStatus, strictFormat], + enums: [tolerantStatus, strictFormat, platform], interfaces: [], objects: [ { @@ -55,15 +64,192 @@ function decoderSchema(): IRSchema { nullable: false, }), field('payload', { kind: 'object', name: 'Payload', nullable: true }), + field('transitive', { kind: 'object', name: 'TransitiveEnvelope', nullable: true }), + field('deep', { kind: 'object', name: 'DeepEnvelope', nullable: true }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'RequiredEnvelope', + fields: [ + field('payload', { + kind: 'object', + name: 'Payload', + nullable: false, + }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'TransitiveEnvelope', + fields: [ + field('requiredEnvelope', { + kind: 'object', + name: 'RequiredEnvelope', + nullable: false, + }), ], interfaces: [], unions: [], isResultUnion: false, }, + { + name: 'PayloadListEnvelope', + fields: [ + field('payloads', { + kind: 'list', + nullable: false, + elementType: { + kind: 'object', + name: 'Payload', + nullable: false, + }, + }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'ListOnlyPayload', + fields: [ + field('format', { + kind: 'enum', + name: 'StrictFormat', + nullable: false, + }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'NullablePayloadListEnvelope', + fields: [ + field('payloads', { + kind: 'list', + nullable: false, + elementType: { + kind: 'object', + name: 'ListOnlyPayload', + nullable: true, + }, + }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'DeepPayload', + fields: [ + field('format', { + kind: 'enum', + name: 'StrictFormat', + nullable: false, + }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'DeepEnvelope', + fields: [ + field('payload', { + kind: 'object', + name: 'DeepPayload', + nullable: false, + }), + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + { + name: 'AndroidProduct', + fields: [ + { + ...field('platform', { + kind: 'enum', + name: 'Platform', + nullable: false, + }), + defaultValue: 'Android', + }, + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + ], + inputs: [ + { + name: 'InputPayload', + fields: [ + field('format', { + kind: 'enum', + name: 'StrictFormat', + nullable: false, + }), + ], + hasRequiredFields: true, + isCustomType: false, + }, + { + name: 'RequestInput', + fields: [ + field('option', { + kind: 'input', + name: 'InputPayload', + nullable: true, + }), + field('formats', { + kind: 'list', + nullable: true, + elementType: { + kind: 'enum', + name: 'StrictFormat', + nullable: true, + }, + }), + ], + hasRequiredFields: false, + isCustomType: false, + }, ], - inputs: [], unions: [], - operations: [], + operations: [ + { + kind: 'Mutation', + name: 'Mutation', + fields: [ + { + name: 'submit', + args: [ + { + name: 'options', + type: { + kind: 'list', + nullable: false, + elementType: { + kind: 'input', + name: 'InputPayload', + nullable: false, + }, + }, + }, + ], + returnType: { kind: 'scalar', name: 'Boolean', nullable: false }, + resolvedReturnType: { kind: 'scalar', name: 'Boolean', nullable: false }, + }, + ], + }, + ], }; } @@ -77,9 +263,22 @@ describe('unknown enum decoder compatibility', () => { expect(output).toContain( 'format = (json["format"] as? String)?.let { StrictFormat.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for StrictFormat")', ); + expect(output).toContain('payload = (json["payload"] as? Map)?.let { runCatching { Payload.fromJson(it) }.getOrNull() }'); expect(output).toContain( - 'payload = (json["payload"] as? Map)?.let { runCatching { Payload.fromJson(it) }.getOrNull() }', + 'payload = (json["payload"] as? Map)?.let { Payload.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for Payload")', ); + expect(output).toContain('transitive = (json["transitive"] as? Map)?.let { runCatching { TransitiveEnvelope.fromJson(it) }.getOrNull() }'); + expect(output).toContain('deep = (json["deep"] as? Map)?.let { runCatching { DeepEnvelope.fromJson(it) }.getOrNull() }'); + expect(output).toContain( + 'payloads = (json["payloads"] as? List<*>)?.map { (it as? Map)?.let { runCatching { ListOnlyPayload.fromJson(it) }.getOrNull() } } ?: emptyList()', + ); + expect(output).toContain( + 'format = (json["format"] as? String)?.let { StrictFormat.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for StrictFormat")', + ); + expect(output).toContain( + 'option = json["option"]?.let { value -> (value as? Map)?.let { InputPayload.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for InputPayload") }', + ); + expect(output).toContain('platform = (json["platform"] as? String)?.let { Platform.fromJson(it) } ?: Platform.Android'); }); it('degrades neutral Dart enums and drops an unreadable optional payload', () => { @@ -89,6 +288,12 @@ describe('unknown enum decoder compatibility', () => { expect(output).not.toContain('Unknown TolerantStatus value'); expect(output).toContain("throw ArgumentError('Unknown StrictFormat value: $value');"); expect(output).toContain('static Payload? _tryFromJson(Map json)'); + expect(output).toContain('static TransitiveEnvelope? _tryFromJson(Map json)'); + expect(output).toContain('static ListOnlyPayload? _tryFromJson(Map json)'); + expect(output).toContain( + "payloads: (json['payloads'] as List).map((e) => e is Map ? ListOnlyPayload._tryFromJson(e as Map) : null).toList()", + ); + expect(output).not.toContain('static InputPayload? _tryFromJson'); expect(output).toContain('} on ArgumentError {'); expect(output).toContain('} on TypeError {'); expect(output).toContain( @@ -103,6 +308,12 @@ describe('unknown enum decoder compatibility', () => { expect(output).not.toContain('Unknown TolerantStatus value'); expect(output).toContain('throw new JsonException($"Unknown StrictFormat value: {raw}");'); expect(output).toContain('public sealed class PayloadNullableJsonConverter : JsonConverter'); + expect(output).toContain('public sealed class TransitiveEnvelopeNullableJsonConverter : JsonConverter'); + expect(output).toContain( + 'public sealed class ListOnlyPayloadNullableElementListJsonConverter : JsonConverter>', + ); + expect(output).toContain('[JsonConverter(typeof(ListOnlyPayloadNullableElementListJsonConverter))]'); + expect(output).not.toContain('InputPayloadNullableJsonConverter'); expect(output).toContain('[JsonConverter(typeof(PayloadNullableJsonConverter))]'); expect(output).toContain('return document.RootElement.Deserialize(options);'); expect(output).toContain('catch (JsonException)'); @@ -113,12 +324,35 @@ describe('unknown enum decoder compatibility', () => { expect(output).toContain('obj.status = TOLERANT_STATUS_FROM_STRING.get(enum_str, TolerantStatus.UNKNOWN)'); expect(output).toContain('obj.status = TolerantStatus.UNKNOWN'); + expect(output).toContain('static func from_dict(data: Dictionary, report_errors: bool = true) -> Payload:'); + expect(output).toContain('push_error("Invalid Payload.format enum value")'); + expect(output).toContain('\t\t\treturn null'); expect(output).toContain( - 'if not data.has("format") or not data["format"] is String or not STRICT_FORMAT_FROM_STRING.has(data["format"]):', + 'static func from_dict_or_null(data: Dictionary) -> Variant:\n\t\treturn from_dict(data, false)', + ); + expect(output).toContain('if data["payload"] is Dictionary:\n\t\t\t\tobj.payload = Payload.from_dict_or_null(data["payload"])'); + expect(output).toContain( + 'var decoded_payload = Payload.from_dict(data["payload"], report_errors)\n\t\t\t\tif decoded_payload == null:\n\t\t\t\t\tif report_errors:\n\t\t\t\t\t\tpush_error("Invalid required Payload value for payload")\n\t\t\t\t\treturn null', ); - expect(output).toContain('\t\t\treturn null'); expect(output).toContain( - 'if data["payload"] is Dictionary:\n\t\t\t\tobj.payload = Payload.from_dict(data["payload"])\n\t\treturn obj', + 'var decoded_required_envelope = RequiredEnvelope.from_dict(data["requiredEnvelope"], report_errors)\n\t\t\t\tif decoded_required_envelope == null:\n\t\t\t\t\tif report_errors:\n\t\t\t\t\t\tpush_error("Invalid required RequiredEnvelope value for requiredEnvelope")\n\t\t\t\t\treturn null', ); + expect(output).toContain('var decoded_payload = Payload.from_dict(item, report_errors)'); + expect(output).toContain('if decoded_payload == null:'); + expect(output).toContain('\t\t\t\t\t\treturn null'); + expect(output).toContain('arr.append(decoded_payload)'); + expect(output).toContain('var decoded_list_only_payload = ListOnlyPayload.from_dict_or_null(item)'); + expect(output).toContain('arr.append(decoded_list_only_payload)'); + expect(output).toContain('var decoded_option = InputPayload.from_dict(data["option"])'); + expect(output).toContain('push_error("Invalid input InputPayload value for option")'); + expect(output).toContain('var formats: Array[Variant] = []'); + expect(output).toContain('if data["formats"] is Array:\n\t\t\t\tvar arr: Array[Variant] = []'); + expect(output).toContain('if item == null:\n\t\t\t\t\t\tarr.append(null)'); + expect(output).toContain('push_error("Invalid StrictFormat list value for formats")'); + const inputPayload = output.slice(output.indexOf('class InputPayload:'), output.indexOf('class RequestInput:')); + expect(inputPayload).not.toContain('from_dict_or_null'); + const operation = output.slice(output.indexOf('class Mutation:')); + expect(operation).toContain('var decoded_input_payload = InputPayload.from_dict(item)'); + expect(operation).not.toContain('InputPayload.from_dict(item, report_errors)'); }); }); diff --git a/packages/gql/src/generated-gdscript.test.ts b/packages/gql/src/generated-gdscript.test.ts index d5c0c88b7..aab868ed3 100644 --- a/packages/gql/src/generated-gdscript.test.ts +++ b/packages/gql/src/generated-gdscript.test.ts @@ -49,7 +49,10 @@ describe('generated GDScript list decoding', () => { const source = classSource('ProductIOS', 'ProductSubscriptionAndroid'); expect(source).toContain('var arr: Array[SubscriptionOffer] = []'); - expect(source).toContain('arr.append(SubscriptionOffer.from_dict(item))'); + expect(source).toContain('var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors)'); + expect(source).toContain('if decoded_subscription_offer == null:'); + expect(source).toContain('return null'); + expect(source).toContain('arr.append(decoded_subscription_offer)'); expect(source).toContain('elif item is SubscriptionOffer:'); expect(source).toContain('var arr: Array[SubscriptionPricingTermsIOS] = []'); }); @@ -110,6 +113,19 @@ describe('generated GDScript list decoding', () => { }, isOverride: false, }, + { + name: 'nullableStrictStatuses', + type: { + kind: 'list', + nullable: false, + elementType: { + kind: 'enum', + name: 'StrictStatus', + nullable: true, + }, + }, + isOverride: false, + }, ], interfaces: [], unions: [], @@ -125,8 +141,15 @@ describe('generated GDScript list decoding', () => { expect(source).toContain('## Status values from the schema. Preserves every documentation line. @see https://openiap.dev/docs/types'); expect(source).toContain('if data["statuses"] is Array:'); expect(source).toContain('arr.append(TEST_STATUS_FROM_STRING.get(item, TestStatus.UNKNOWN))'); + expect(source).toContain('elif item is int and TEST_STATUS_VALUES.has(item):'); + expect(source).toContain('arr.append(TestStatus.UNKNOWN)'); expect(source).toContain('if item is String and STRICT_STATUS_FROM_STRING.has(item):'); expect(source).toContain('arr.append(STRICT_STATUS_FROM_STRING[item])'); - expect(source).toContain('elif item is int:'); + expect(source).toContain('elif item is int and STRICT_STATUS_VALUES.has(item):'); + expect(source).toContain('push_error("Invalid StrictStatus list value for strictStatuses")'); + expect(source).toContain('return null'); + expect(source).toContain('var nullable_strict_statuses: Array[Variant] = []'); + expect(source).toContain('if data["nullableStrictStatuses"] is Array:\n\t\t\t\tvar arr: Array[Variant] = []'); + expect(source).toContain('\t\t\t\t\tarr.append(null)'); }); }); diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index 36c18b74d..4fd848499 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -3984,29 +3984,6 @@ public sealed record DeveloperBillingOptionParamsAndroid public string? ExternalTransactionToken { get; init; } } -public sealed class DeveloperBillingOptionParamsAndroidNullableJsonConverter : JsonConverter -{ - public override DeveloperBillingOptionParamsAndroid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var document = JsonDocument.ParseValue(ref reader); - try - { - return document.RootElement.Deserialize(options); - } - catch (JsonException) - { - return null; - } - catch (InvalidOperationException) - { - return null; - } - } - - public override void Write(Utf8JsonWriter writer, DeveloperBillingOptionParamsAndroid? value, JsonSerializerOptions options) => - JsonSerializer.Serialize(writer, value, options); -} - public sealed record DiscountOfferInputIOS { /// Discount identifier @@ -4203,7 +4180,6 @@ public sealed record RequestPurchaseAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] - [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -4330,7 +4306,6 @@ public sealed record RequestSubscriptionAndroidProps /// (requires Play Billing 9.1.0+). /// [JsonPropertyName("developerBillingOption")] - [JsonConverter(typeof(DeveloperBillingOptionParamsAndroidNullableJsonConverter))] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index f1f7e3463..7a79564c1 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -1666,7 +1666,7 @@ public data class BillingProgramAvailabilityResultAndroid( companion object { fun fromJson(json: Map): BillingProgramAvailabilityResultAndroid { return BillingProgramAvailabilityResultAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified, + billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid"), choiceScreenType = (json["choiceScreenType"] as? String)?.let { BillingChoiceScreenTypeAndroid.fromJson(it) }, isAvailable = json["isAvailable"] as? Boolean ?: false, isExternalLinkAvailable = json["isExternalLinkAvailable"] as? Boolean, @@ -1704,7 +1704,7 @@ public data class BillingProgramReportingDetailsAndroid( companion object { fun fromJson(json: Map): BillingProgramReportingDetailsAndroid { return BillingProgramReportingDetailsAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified, + billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid"), externalTransactionToken = json["externalTransactionToken"] as? String ?: "", ) } @@ -1871,7 +1871,7 @@ public data class DeveloperProvidedBillingProductAndroid( return DeveloperProvidedBillingProductAndroid( id = json["id"] as? String ?: "", offerToken = json["offerToken"] as? String, - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductType"), ) } } @@ -2056,7 +2056,7 @@ public data class DiscountOffer( price = (json["price"] as? Number)?.toDouble() ?: 0.0, purchaseOptionIdAndroid = json["purchaseOptionIdAndroid"] as? String, rentalDetailsAndroid = (json["rentalDetailsAndroid"] as? Map)?.let { RentalDetailsAndroid.fromJson(it) }, - type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory, + type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for DiscountOfferType"), validTimeWindowAndroid = (json["validTimeWindowAndroid"] as? Map)?.let { ValidTimeWindowAndroid.fromJson(it) }, ) } @@ -2224,7 +2224,7 @@ public data class ExternalPurchaseNoticeResultIOS( return ExternalPurchaseNoticeResultIOS( error = json["error"] as? String, externalPurchaseToken = json["externalPurchaseToken"] as? String, - result = (json["result"] as? String)?.let { ExternalPurchaseNoticeAction.fromJson(it) } ?: ExternalPurchaseNoticeAction.Continue, + result = (json["result"] as? String)?.let { ExternalPurchaseNoticeAction.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalPurchaseNoticeAction"), ) } } @@ -2296,7 +2296,7 @@ public data class InAppMessageResultAndroid( fun fromJson(json: Map): InAppMessageResultAndroid { return InAppMessageResultAndroid( purchaseToken = json["purchaseToken"] as? String, - responseCode = (json["responseCode"] as? String)?.let { InAppMessageResponseCodeAndroid.fromJson(it) } ?: InAppMessageResponseCodeAndroid.NoActionNeeded, + responseCode = (json["responseCode"] as? String)?.let { InAppMessageResponseCodeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for InAppMessageResponseCodeAndroid"), ) } } @@ -2541,7 +2541,7 @@ public data class ProductAndroid( displayPrice = json["displayPrice"] as? String ?: "", id = json["id"] as? String ?: "", nameAndroid = json["nameAndroid"] as? String ?: "", - platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, + platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Android, price = (json["price"] as? Number)?.toDouble(), productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, @@ -2617,7 +2617,7 @@ public data class ProductIOS( subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") }, title = json["title"] as? String ?: "", type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, - typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: ProductTypeIOS.Consumable, + typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductTypeIOS"), ) } } @@ -2681,12 +2681,12 @@ public data class ProductSubscriptionAndroid( displayPrice = json["displayPrice"] as? String ?: "", id = json["id"] as? String ?: "", nameAndroid = json["nameAndroid"] as? String ?: "", - platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios, + platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Android, price = (json["price"] as? Number)?.toDouble(), productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) }, subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(), title = json["title"] as? String ?: "", - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, ) } } @@ -2779,8 +2779,8 @@ public data class ProductSubscriptionIOS( subscriptionPeriodNumberIOS = json["subscriptionPeriodNumberIOS"] as? String, subscriptionPeriodUnitIOS = (json["subscriptionPeriodUnitIOS"] as? String)?.let { SubscriptionPeriodIOS.fromJson(it) }, title = json["title"] as? String ?: "", - type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.InApp, - typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: ProductTypeIOS.Consumable, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, + typeIOS = (json["typeIOS"] as? String)?.let { ProductTypeIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ProductTypeIOS"), ) } } @@ -3657,7 +3657,7 @@ public data class SubscriptionOffer( pricingPhasesAndroid = (json["pricingPhasesAndroid"] as? Map)?.let { PricingPhasesAndroid.fromJson(it) }, signatureIOS = json["signatureIOS"] as? String, timestampIOS = (json["timestampIOS"] as? Number)?.toDouble(), - type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory, + type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for DiscountOfferType"), ) } } @@ -4107,7 +4107,7 @@ public data class VerifyPurchaseWithProviderResult( return VerifyPurchaseWithProviderResult( errors = (json["errors"] as? List<*>)?.mapNotNull { (it as? Map)?.let { VerifyPurchaseWithProviderError.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for VerifyPurchaseWithProviderError") }, iapkit = (json["iapkit"] as? Map)?.let { RequestVerifyPurchaseWithIapkitResult.fromJson(it) }, - provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: PurchaseVerificationProvider.Iapkit, + provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider"), ) } } @@ -4441,10 +4441,10 @@ public data class LaunchExternalLinkParamsAndroid( ) { companion object { fun fromJson(json: Map): LaunchExternalLinkParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String - val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: ExternalLinkLaunchModeAndroid.Unspecified - val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: ExternalLinkTypeAndroid.Unspecified + val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkLaunchModeAndroid") + val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkTypeAndroid") val linkUri = json["linkUri"] as? String if (linkUri == null) return null return LaunchExternalLinkParamsAndroid( @@ -4614,7 +4614,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4679,7 +4679,7 @@ public data class RequestPurchaseIosProps( val appAccountToken = json["appAccountToken"] as? String val quantity = (json["quantity"] as? Number)?.toInt() val sku = json["sku"] as? String - val withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } + val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null return RequestPurchaseIosProps( advancedCommerceData = advancedCommerceData, @@ -4785,8 +4785,8 @@ public data class RequestPurchasePropsByPlatforms( companion object { fun fromJson(json: Map): RequestPurchasePropsByPlatforms { return RequestPurchasePropsByPlatforms( - apple = (json["apple"] as? Map)?.let { RequestPurchaseIosProps.fromJson(it) }, - google = (json["google"] as? Map)?.let { RequestPurchaseAndroidProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestPurchaseIosProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestPurchaseIosProps") }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestPurchaseAndroidProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestPurchaseAndroidProps") }, ) } } @@ -4846,7 +4846,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { runCatching { DeveloperBillingOptionParamsAndroid.fromJson(it) }.getOrNull() } + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountId = json["obfuscatedAccountId"] as? String val obfuscatedProfileId = json["obfuscatedProfileId"] as? String @@ -4854,7 +4854,7 @@ public data class RequestSubscriptionAndroidProps( val purchaseToken = json["purchaseToken"] as? String val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } val subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } } - val subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } + val subscriptionProductReplacementParams = json["subscriptionProductReplacementParams"]?.let { value -> (value as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for SubscriptionProductReplacementParamsAndroid") } if (skus == null) return null return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, @@ -4933,11 +4933,11 @@ public data class RequestSubscriptionIosProps( val appAccountToken = json["appAccountToken"] as? String val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } val compactJWS = json["compactJWS"] as? String - val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } + val promotionalOfferJWS = json["promotionalOfferJWS"]?.let { value -> (value as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for PromotionalOfferJWSInputIOS") } val quantity = (json["quantity"] as? Number)?.toInt() val sku = json["sku"] as? String - val winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } - val withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } + val winBackOffer = json["winBackOffer"]?.let { value -> (value as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for WinBackOfferInputIOS") } + val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null return RequestSubscriptionIosProps( advancedCommerceData = advancedCommerceData, @@ -4990,8 +4990,8 @@ public data class RequestSubscriptionPropsByPlatforms( companion object { fun fromJson(json: Map): RequestSubscriptionPropsByPlatforms { return RequestSubscriptionPropsByPlatforms( - apple = (json["apple"] as? Map)?.let { RequestSubscriptionIosProps.fromJson(it) }, - google = (json["google"] as? Map)?.let { RequestSubscriptionAndroidProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestSubscriptionIosProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestSubscriptionIosProps") }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestSubscriptionAndroidProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestSubscriptionAndroidProps") }, ) } } @@ -5165,11 +5165,11 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( - amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, + amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, apiKey = json["apiKey"] as? String, - apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAppleProps") }, baseUrl = json["baseUrl"] as? String, - google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, + google = json["google"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitGoogleProps") }, includeClientPayload = json["includeClientPayload"] as? Boolean, ) } @@ -5367,9 +5367,9 @@ public data class VerifyPurchaseProps( companion object { fun fromJson(json: Map): VerifyPurchaseProps { return VerifyPurchaseProps( - apple = (json["apple"] as? Map)?.let { VerifyPurchaseAppleOptions.fromJson(it) }, - google = (json["google"] as? Map)?.let { VerifyPurchaseGoogleOptions.fromJson(it) }, - horizon = (json["horizon"] as? Map)?.let { VerifyPurchaseHorizonOptions.fromJson(it) }, + apple = json["apple"]?.let { value -> (value as? Map)?.let { VerifyPurchaseAppleOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseAppleOptions") }, + google = json["google"]?.let { value -> (value as? Map)?.let { VerifyPurchaseGoogleOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseGoogleOptions") }, + horizon = json["horizon"]?.let { value -> (value as? Map)?.let { VerifyPurchaseHorizonOptions.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for VerifyPurchaseHorizonOptions") }, ) } } @@ -5387,8 +5387,8 @@ public data class VerifyPurchaseWithProviderProps( ) { companion object { fun fromJson(json: Map): VerifyPurchaseWithProviderProps? { - val iapkit = (json["iapkit"] as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } - val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: PurchaseVerificationProvider.Iapkit + val iapkit = json["iapkit"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitProps") } + val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider") return VerifyPurchaseWithProviderProps( iapkit = iapkit, provider = provider, diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 3cd4c0c61..a50a2c5f8 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -4146,16 +4146,6 @@ class DeveloperBillingOptionParamsAndroid { ); } - static DeveloperBillingOptionParamsAndroid? _tryFromJson(Map json) { - try { - return DeveloperBillingOptionParamsAndroid.fromJson(json); - } on ArgumentError { - return null; - } on TypeError { - return null; - } - } - Map toJson() { return { 'billingProgram': billingProgram.toJson(), @@ -4496,7 +4486,7 @@ class RequestPurchaseAndroidProps { factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, @@ -4699,7 +4689,7 @@ class RequestSubscriptionAndroidProps { factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOption: json['developerBillingOption'] is Map ? DeveloperBillingOptionParamsAndroid._tryFromJson(json['developerBillingOption'] as Map) : null, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountId: json['obfuscatedAccountId'] as String?, obfuscatedProfileId: json['obfuscatedProfileId'] as String?, diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 2e5b03b11..5515aaff2 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -466,10 +466,13 @@ class AdvancedCommerceInfoIOS: arr.append(AdvancedCommerceItemIOS.from_dict(item)) elif item is AdvancedCommerceItemIOS: arr.append(item) + else: + push_error("Invalid AdvancedCommerceItemIOS list value for items") + return null obj.items = arr if data.has("period") and data["period"] != null: if data["period"] is Dictionary: - obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) + obj.period = SubscriptionPeriodValueIOS.from_dict_or_null(data["period"]) if data.has("requestReferenceId") and data["requestReferenceId"] != null: obj.request_reference_id = data["requestReferenceId"] if data.has("taxCode") and data["taxCode"] != null: @@ -559,6 +562,9 @@ class AdvancedCommerceItemIOS: arr.append(AdvancedCommerceRefundIOS.from_dict(item)) elif item is AdvancedCommerceRefundIOS: arr.append(item) + else: + push_error("Invalid AdvancedCommerceRefundIOS list value for refunds") + return null obj.refunds = arr if data.has("revocationDate") and data["revocationDate"] != null: obj.revocation_date = data["revocationDate"] @@ -712,7 +718,11 @@ class BillingProgramAvailabilityResultAndroid: ## Whether external-link payment is available for Billing Choice. Populated only for available BILLING_CHOICE results. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. var is_external_link_available: Variant = null - static func from_dict(data: Dictionary) -> BillingProgramAvailabilityResultAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> BillingProgramAvailabilityResultAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid BillingProgramAvailabilityResultAndroid.billingProgram enum value") + return null var obj = BillingProgramAvailabilityResultAndroid.new() if data.has("isAvailable") and data["isAvailable"] != null: obj.is_available = data["isAvailable"] @@ -755,7 +765,11 @@ class BillingProgramReportingDetailsAndroid: ## External transaction token used to report transactions made outside of Google Play Billing. Do not cache it for a later redirect session. For External Offer, the same token may report multiple purchases made during the session that generated it. var external_transaction_token: String = "" - static func from_dict(data: Dictionary) -> BillingProgramReportingDetailsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> BillingProgramReportingDetailsAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid BillingProgramReportingDetailsAndroid.billingProgram enum value") + return null var obj = BillingProgramReportingDetailsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -869,7 +883,7 @@ class DeveloperProvidedBillingDetailsAndroid: ## Products selected for the developer billing flow. var products: Array[DeveloperProvidedBillingProductAndroid] = [] - static func from_dict(data: Dictionary) -> DeveloperProvidedBillingDetailsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> DeveloperProvidedBillingDetailsAndroid: var obj = DeveloperProvidedBillingDetailsAndroid.new() if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: obj.external_transaction_token = data["externalTransactionToken"] @@ -882,9 +896,16 @@ class DeveloperProvidedBillingDetailsAndroid: var arr: Array[DeveloperProvidedBillingProductAndroid] = [] for item in data["products"]: if item is Dictionary: - arr.append(DeveloperProvidedBillingProductAndroid.from_dict(item)) + var decoded_developer_provided_billing_product_android = DeveloperProvidedBillingProductAndroid.from_dict(item, report_errors) + if decoded_developer_provided_billing_product_android == null: + return null + arr.append(decoded_developer_provided_billing_product_android) elif item is DeveloperProvidedBillingProductAndroid: arr.append(item) + else: + if report_errors: + push_error("Invalid DeveloperProvidedBillingProductAndroid list value for products") + return null obj.products = arr return obj @@ -917,7 +938,11 @@ class DeveloperProvidedBillingProductAndroid: ## Subscription offer token, when applicable. var offer_token: Variant = null - static func from_dict(data: Dictionary) -> DeveloperProvidedBillingProductAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> DeveloperProvidedBillingProductAndroid: + if not data.has("type") or not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid DeveloperProvidedBillingProductAndroid.type enum value") + return null var obj = DeveloperProvidedBillingProductAndroid.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1026,7 +1051,11 @@ class DiscountOffer: ## [Android] Purchase option ID for this offer. Used to identify which purchase option the user selected. Available in Google Play Billing Library 8.0+ var purchase_option_id_android: Variant = null - static func from_dict(data: Dictionary) -> DiscountOffer: + static func from_dict(data: Dictionary, report_errors: bool = true) -> DiscountOffer: + if not data.has("type") or not ((data["type"] is String and DISCOUNT_OFFER_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and DISCOUNT_OFFER_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid DiscountOffer.type enum value") + return null var obj = DiscountOffer.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1223,7 +1252,11 @@ class ExternalPurchaseNoticeResultIOS: ## 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. var external_purchase_token: Variant = null - static func from_dict(data: Dictionary) -> ExternalPurchaseNoticeResultIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ExternalPurchaseNoticeResultIOS: + if not data.has("result") or not ((data["result"] is String and EXTERNAL_PURCHASE_NOTICE_ACTION_FROM_STRING.has(data["result"])) or (data["result"] is int and EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES.has(data["result"]))): + if report_errors: + push_error("Invalid ExternalPurchaseNoticeResultIOS.result enum value") + return null var obj = ExternalPurchaseNoticeResultIOS.new() if data.has("result") and data["result"] != null: var enum_str = data["result"] @@ -1256,8 +1289,10 @@ class IapkitProductClientPayload: var version: float = 0.0 var updated_at: float = 0.0 - static func from_dict(data: Dictionary) -> IapkitProductClientPayload: - if not data.has("format") or not data["format"] is String or not IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING.has(data["format"]): + static func from_dict(data: Dictionary, report_errors: bool = true) -> IapkitProductClientPayload: + if not data.has("format") or not ((data["format"] is String and IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING.has(data["format"])) or (data["format"] is int and IAPKIT_CLIENT_PAYLOAD_FORMAT_VALUES.has(data["format"]))): + if report_errors: + push_error("Invalid IapkitProductClientPayload.format enum value") return null var obj = IapkitProductClientPayload.new() if data.has("format") and data["format"] != null: @@ -1274,6 +1309,9 @@ class IapkitProductClientPayload: obj.updated_at = data["updatedAt"] return obj + static func from_dict_or_null(data: Dictionary) -> Variant: + return from_dict(data, false) + func to_dict() -> Dictionary: var dict = {} if IAPKIT_CLIENT_PAYLOAD_FORMAT_VALUES.has(format): @@ -1292,7 +1330,11 @@ class InAppMessageResultAndroid: ## Purchase token returned when a subscription status changed. var purchase_token: Variant = null - static func from_dict(data: Dictionary) -> InAppMessageResultAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> InAppMessageResultAndroid: + if not data.has("responseCode") or not ((data["responseCode"] is String and IN_APP_MESSAGE_RESPONSE_CODE_ANDROID_FROM_STRING.has(data["responseCode"])) or (data["responseCode"] is int and IN_APP_MESSAGE_RESPONSE_CODE_ANDROID_VALUES.has(data["responseCode"]))): + if report_errors: + push_error("Invalid InAppMessageResultAndroid.responseCode enum value") + return null var obj = InAppMessageResultAndroid.new() if data.has("responseCode") and data["responseCode"] != null: var enum_str = data["responseCode"] @@ -1450,6 +1492,9 @@ class PricingPhasesAndroid: arr.append(PricingPhaseAndroid.from_dict(item)) elif item is PricingPhaseAndroid: arr.append(item) + else: + push_error("Invalid PricingPhaseAndroid list value for pricingPhaseList") + return null obj.pricing_phase_list = arr return obj @@ -1486,7 +1531,15 @@ class ProductAndroid: ## Standardized subscription offers. Cross-platform type with Android-specific fields using suffix. @see https://openiap.dev/docs/types/subscription-offer var subscription_offers: Array[SubscriptionOffer] = [] - static func from_dict(data: Dictionary) -> ProductAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductAndroid: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductAndroid.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductAndroid.platform enum value") + return null var obj = ProductAndroid.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1531,18 +1584,32 @@ class ProductAndroid: var arr: Array[DiscountOffer] = [] for item in data["discountOffers"]: if item is Dictionary: - arr.append(DiscountOffer.from_dict(item)) + var decoded_discount_offer = DiscountOffer.from_dict(item, report_errors) + if decoded_discount_offer == null: + return null + arr.append(decoded_discount_offer) elif item is DiscountOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid DiscountOffer list value for discountOffers") + return null obj.discount_offers = arr if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: if data["subscriptionOffers"] is Array: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr return obj @@ -1615,7 +1682,19 @@ class ProductIOS: ## iOS 26.4+ subscription pricing terms, including billing plan metadata for monthly subscriptions with a 12-month commitment. var pricing_terms_ios: Array[SubscriptionPricingTermsIOS] = [] - static func from_dict(data: Dictionary) -> ProductIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductIOS: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductIOS.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductIOS.platform enum value") + return null + if not data.has("typeIOS") or not ((data["typeIOS"] is String and PRODUCT_TYPE_IOS_FROM_STRING.has(data["typeIOS"])) or (data["typeIOS"] is int and PRODUCT_TYPE_IOS_VALUES.has(data["typeIOS"]))): + if report_errors: + push_error("Invalid ProductIOS.typeIOS enum value") + return null var obj = ProductIOS.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1662,18 +1741,32 @@ class ProductIOS: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null: if data["pricingTermsIOS"] is Array: var arr: Array[SubscriptionPricingTermsIOS] = [] for item in data["pricingTermsIOS"]: if item is Dictionary: - arr.append(SubscriptionPricingTermsIOS.from_dict(item)) + var decoded_subscription_pricing_terms_ios = SubscriptionPricingTermsIOS.from_dict(item, report_errors) + if decoded_subscription_pricing_terms_ios == null: + return null + arr.append(decoded_subscription_pricing_terms_ios) elif item is SubscriptionPricingTermsIOS: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionPricingTermsIOS list value for pricingTermsIOS") + return null obj.pricing_terms_ios = arr return obj @@ -1744,7 +1837,15 @@ class ProductSubscriptionAndroid: ## Standardized subscription offers. Cross-platform type with Android-specific fields using suffix. @see https://openiap.dev/docs/types/subscription-offer var subscription_offers: Array[SubscriptionOffer] = [] - static func from_dict(data: Dictionary) -> ProductSubscriptionAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductSubscriptionAndroid: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductSubscriptionAndroid.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductSubscriptionAndroid.platform enum value") + return null var obj = ProductSubscriptionAndroid.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1789,9 +1890,16 @@ class ProductSubscriptionAndroid: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr return obj @@ -1859,13 +1967,29 @@ class ProductSubscriptionIOS: var subscription_group_id_ios: Variant = null var introductory_price_ios: Variant = null var introductory_price_as_amount_ios: Variant = null - var introductory_price_payment_mode_ios: PaymentModeIOS + var introductory_price_payment_mode_ios: PaymentModeIOS = PaymentModeIOS.EMPTY var introductory_price_number_of_periods_ios: Variant = null var introductory_price_subscription_period_ios: Variant = null var subscription_period_number_ios: Variant = null var subscription_period_unit_ios: Variant = null - static func from_dict(data: Dictionary) -> ProductSubscriptionIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> ProductSubscriptionIOS: + if data.has("type") and data["type"] != null and not ((data["type"] is String and PRODUCT_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and PRODUCT_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.type enum value") + return null + if data.has("platform") and data["platform"] != null and not ((data["platform"] is String and IAP_PLATFORM_FROM_STRING.has(data["platform"])) or (data["platform"] is int and IAP_PLATFORM_VALUES.has(data["platform"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.platform enum value") + return null + if not data.has("typeIOS") or not ((data["typeIOS"] is String and PRODUCT_TYPE_IOS_FROM_STRING.has(data["typeIOS"])) or (data["typeIOS"] is int and PRODUCT_TYPE_IOS_VALUES.has(data["typeIOS"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.typeIOS enum value") + return null + if data.has("introductoryPricePaymentModeIOS") and data["introductoryPricePaymentModeIOS"] != null and not ((data["introductoryPricePaymentModeIOS"] is String and PAYMENT_MODE_IOS_FROM_STRING.has(data["introductoryPricePaymentModeIOS"])) or (data["introductoryPricePaymentModeIOS"] is int and PAYMENT_MODE_IOS_VALUES.has(data["introductoryPricePaymentModeIOS"]))): + if report_errors: + push_error("Invalid ProductSubscriptionIOS.introductoryPricePaymentModeIOS enum value") + return null var obj = ProductSubscriptionIOS.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -1912,18 +2036,32 @@ class ProductSubscriptionIOS: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null: if data["pricingTermsIOS"] is Array: var arr: Array[SubscriptionPricingTermsIOS] = [] for item in data["pricingTermsIOS"]: if item is Dictionary: - arr.append(SubscriptionPricingTermsIOS.from_dict(item)) + var decoded_subscription_pricing_terms_ios = SubscriptionPricingTermsIOS.from_dict(item, report_errors) + if decoded_subscription_pricing_terms_ios == null: + return null + arr.append(decoded_subscription_pricing_terms_ios) elif item is SubscriptionPricingTermsIOS: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionPricingTermsIOS list value for pricingTermsIOS") + return null obj.pricing_terms_ios = arr if data.has("bundledSubscriptionsIOS") and data["bundledSubscriptionsIOS"] != null: if data["bundledSubscriptionsIOS"] is Array: @@ -1933,6 +2071,9 @@ class ProductSubscriptionIOS: arr.append(BundledSubscriptionIOS.from_dict(item)) elif item is BundledSubscriptionIOS: arr.append(item) + else: + push_error("Invalid BundledSubscriptionIOS list value for bundledSubscriptionsIOS") + return null obj.bundled_subscriptions_ios = arr if data.has("subscriptionGroupIdIOS") and data["subscriptionGroupIdIOS"] != null: obj.subscription_group_id_ios = data["subscriptionGroupIdIOS"] @@ -1944,24 +2085,36 @@ class ProductSubscriptionIOS: var enum_str = data["introductoryPricePaymentModeIOS"] if enum_str is String and PAYMENT_MODE_IOS_FROM_STRING.has(enum_str): obj.introductory_price_payment_mode_ios = PAYMENT_MODE_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and PAYMENT_MODE_IOS_VALUES.has(enum_str): obj.introductory_price_payment_mode_ios = enum_str + elif not enum_str is String: + obj.introductory_price_payment_mode_ios = PaymentModeIOS.EMPTY + else: + push_error("Unknown PaymentModeIOS value: %s" % enum_str) if data.has("introductoryPriceNumberOfPeriodsIOS") and data["introductoryPriceNumberOfPeriodsIOS"] != null: obj.introductory_price_number_of_periods_ios = data["introductoryPriceNumberOfPeriodsIOS"] if data.has("introductoryPriceSubscriptionPeriodIOS") and data["introductoryPriceSubscriptionPeriodIOS"] != null: var enum_str = data["introductoryPriceSubscriptionPeriodIOS"] if enum_str is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(enum_str): obj.introductory_price_subscription_period_ios = SUBSCRIPTION_PERIOD_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(enum_str): obj.introductory_price_subscription_period_ios = enum_str + elif not enum_str is String: + obj.introductory_price_subscription_period_ios = SubscriptionPeriodIOS.EMPTY + else: + push_error("Unknown SubscriptionPeriodIOS value: %s" % enum_str) if data.has("subscriptionPeriodNumberIOS") and data["subscriptionPeriodNumberIOS"] != null: obj.subscription_period_number_ios = data["subscriptionPeriodNumberIOS"] if data.has("subscriptionPeriodUnitIOS") and data["subscriptionPeriodUnitIOS"] != null: var enum_str = data["subscriptionPeriodUnitIOS"] if enum_str is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(enum_str): obj.subscription_period_unit_ios = SUBSCRIPTION_PERIOD_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(enum_str): obj.subscription_period_unit_ios = enum_str + elif not enum_str is String: + obj.subscription_period_unit_ios = SubscriptionPeriodIOS.EMPTY + else: + push_error("Unknown SubscriptionPeriodIOS value: %s" % enum_str) return obj func to_dict() -> Dictionary: @@ -2057,9 +2210,9 @@ class PurchaseAndroid: var transaction_date: float = 0.0 var purchase_token: Variant = null ## Store where purchase was made - var store: IapStore + var store: IapStore = IapStore.UNKNOWN var quantity: int = 0 - var purchase_state: PurchaseState + var purchase_state: PurchaseState = PurchaseState.UNKNOWN var is_auto_renewing: bool = false var current_plan_id: Variant = null var data_android: Variant = null @@ -2200,7 +2353,7 @@ class PurchaseAndroid: return dict class PurchaseError: - var code: ErrorCode + var code: ErrorCode = ErrorCode.UNKNOWN var message: String = "" var product_id: Variant = null var debug_message: Variant = null @@ -2280,9 +2433,9 @@ class PurchaseIOS: var transaction_date: float = 0.0 var purchase_token: Variant = null ## Store where purchase was made - var store: IapStore + var store: IapStore = IapStore.UNKNOWN var quantity: int = 0 - var purchase_state: PurchaseState + var purchase_state: PurchaseState = PurchaseState.UNKNOWN var is_auto_renewing: bool = false var current_plan_id: Variant = null var transaction_id: String = "" @@ -2592,7 +2745,7 @@ class RefundResultIOS: class RenewalCommitmentInfoIOS: var commitment_auto_renew_product_id: String = "" var commitment_auto_renew_status: bool = false - var commitment_renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS + var commitment_renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS = SubscriptionBillingPlanTypeIOS.UNKNOWN var commitment_renewal_date: float = 0.0 var commitment_renewal_price: float = 0.0 @@ -2774,13 +2927,13 @@ class RentalDetailsAndroid: return dict class RequestVerifyPurchaseWithIapkitResult: - var store: IapStore + var store: IapStore = IapStore.UNKNOWN ## Available in OpenIAP Spec 3.2.0 / openiap-apple 3.2.0 / openiap-google 3.3.0. Amazon RVS environment selected by IAPKit. Present as `Sandbox` or `Production` on handled Amazon verification results. Deliberately String, not an enum: the value space belongs to IAPKit and the stores behind it, and Apple's App Store Server alone also names `Xcode` and `LocalTesting`. SDKs must forward this value opaquely. Never reject a verification because the environment is unrecognised — that fails a purchase the store already confirmed. var environment: Variant = null ## True when the purchase is valid and actionable. Only entitled, pending-acknowledgment, or ready-to-consume return true. Callers must still match productId and use the platform plus app-owned product type to choose the fulfillment path. var is_valid: bool = false ## The current state of the purchase. - var state: IapkitPurchaseState + var state: IapkitPurchaseState = IapkitPurchaseState.UNKNOWN ## Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. Store-verified product identifier when the provider returns one. var product_id: Variant = null ## Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. Public product payload when includeClientPayload was requested, the Apple or Google receipt is valid, and a payload exists for that product. @@ -2812,7 +2965,7 @@ class RequestVerifyPurchaseWithIapkitResult: obj.product_id = data["productId"] if data.has("clientPayload") and data["clientPayload"] != null: if data["clientPayload"] is Dictionary: - obj.client_payload = IapkitProductClientPayload.from_dict(data["clientPayload"]) + obj.client_payload = IapkitProductClientPayload.from_dict_or_null(data["clientPayload"]) return obj func to_dict() -> Dictionary: @@ -2841,15 +2994,26 @@ class SubscriptionCommitmentInfoIOS: var period: SubscriptionPeriodValueIOS var price: float = 0.0 - static func from_dict(data: Dictionary) -> SubscriptionCommitmentInfoIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionCommitmentInfoIOS: var obj = SubscriptionCommitmentInfoIOS.new() if data.has("displayPrice") and data["displayPrice"] != null: obj.display_price = data["displayPrice"] if data.has("period") and data["period"] != null: if data["period"] is Dictionary: - obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) - else: - obj.period = data["period"] + var decoded_period = SubscriptionPeriodValueIOS.from_dict(data["period"], report_errors) + if decoded_period == null: + if report_errors: + push_error("Invalid required SubscriptionPeriodValueIOS value for period") + return null + obj.period = decoded_period + else: + if report_errors: + push_error("Expected period to be a SubscriptionPeriodValueIOS dictionary") + return null + else: + if report_errors: + push_error("Missing required period value") + return null if data.has("price") and data["price"] != null: obj.price = data["price"] return obj @@ -2905,7 +3069,11 @@ class SubscriptionOffer: ## [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+ var installment_plan_details_android: InstallmentPlanDetailsAndroid - static func from_dict(data: Dictionary) -> SubscriptionOffer: + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionOffer: + if not data.has("type") or not ((data["type"] is String and DISCOUNT_OFFER_TYPE_FROM_STRING.has(data["type"])) or (data["type"] is int and DISCOUNT_OFFER_TYPE_VALUES.has(data["type"]))): + if report_errors: + push_error("Invalid SubscriptionOffer.type enum value") + return null var obj = SubscriptionOffer.new() if data.has("id") and data["id"] != null: obj.id = data["id"] @@ -3023,7 +3191,7 @@ class SubscriptionOffer: ## Subscription period value combining unit and count. class SubscriptionPeriod: ## The period unit (day, week, month, year) - var unit: SubscriptionPeriodUnit + var unit: SubscriptionPeriodUnit = SubscriptionPeriodUnit.UNKNOWN ## The number of units (e.g., 1 for monthly, 3 for quarterly) var value: int = 0 @@ -3051,23 +3219,32 @@ class SubscriptionPeriod: return dict class SubscriptionPeriodValueIOS: - var unit: SubscriptionPeriodIOS + var unit: SubscriptionPeriodIOS = SubscriptionPeriodIOS.EMPTY var value: int = 0 - static func from_dict(data: Dictionary) -> SubscriptionPeriodValueIOS: - if not data.has("unit") or not data["unit"] is String or not SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(data["unit"]): + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionPeriodValueIOS: + if data.has("unit") and data["unit"] != null and not ((data["unit"] is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(data["unit"])) or (data["unit"] is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(data["unit"]))): + if report_errors: + push_error("Invalid SubscriptionPeriodValueIOS.unit enum value") return null var obj = SubscriptionPeriodValueIOS.new() if data.has("unit") and data["unit"] != null: var enum_str = data["unit"] if enum_str is String and SUBSCRIPTION_PERIOD_IOS_FROM_STRING.has(enum_str): obj.unit = SUBSCRIPTION_PERIOD_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and SUBSCRIPTION_PERIOD_IOS_VALUES.has(enum_str): obj.unit = enum_str + elif not enum_str is String: + obj.unit = SubscriptionPeriodIOS.EMPTY + else: + push_error("Unknown SubscriptionPeriodIOS value: %s" % enum_str) if data.has("value") and data["value"] != null: obj.value = data["value"] return obj + static func from_dict_or_null(data: Dictionary) -> Variant: + return from_dict(data, false) + func to_dict() -> Dictionary: var dict = {} if SUBSCRIPTION_PERIOD_IOS_VALUES.has(unit): @@ -3080,20 +3257,31 @@ class SubscriptionPeriodValueIOS: class SubscriptionPricingTermsIOS: var billing_display_price: String = "" var billing_period: SubscriptionPeriodValueIOS - var billing_plan_type: SubscriptionBillingPlanTypeIOS + var billing_plan_type: SubscriptionBillingPlanTypeIOS = SubscriptionBillingPlanTypeIOS.UNKNOWN var billing_price: float = 0.0 var commitment_info: SubscriptionCommitmentInfoIOS var subscription_offers: Array[SubscriptionOffer] = [] - static func from_dict(data: Dictionary) -> SubscriptionPricingTermsIOS: + static func from_dict(data: Dictionary, report_errors: bool = true) -> SubscriptionPricingTermsIOS: var obj = SubscriptionPricingTermsIOS.new() if data.has("billingDisplayPrice") and data["billingDisplayPrice"] != null: obj.billing_display_price = data["billingDisplayPrice"] if data.has("billingPeriod") and data["billingPeriod"] != null: if data["billingPeriod"] is Dictionary: - obj.billing_period = SubscriptionPeriodValueIOS.from_dict(data["billingPeriod"]) - else: - obj.billing_period = data["billingPeriod"] + var decoded_billing_period = SubscriptionPeriodValueIOS.from_dict(data["billingPeriod"], report_errors) + if decoded_billing_period == null: + if report_errors: + push_error("Invalid required SubscriptionPeriodValueIOS value for billingPeriod") + return null + obj.billing_period = decoded_billing_period + else: + if report_errors: + push_error("Expected billingPeriod to be a SubscriptionPeriodValueIOS dictionary") + return null + else: + if report_errors: + push_error("Missing required billingPeriod value") + return null if data.has("billingPlanType") and data["billingPlanType"] != null: var enum_str = data["billingPlanType"] if enum_str is String: @@ -3106,17 +3294,35 @@ class SubscriptionPricingTermsIOS: obj.billing_price = data["billingPrice"] if data.has("commitmentInfo") and data["commitmentInfo"] != null: if data["commitmentInfo"] is Dictionary: - obj.commitment_info = SubscriptionCommitmentInfoIOS.from_dict(data["commitmentInfo"]) - else: - obj.commitment_info = data["commitmentInfo"] + var decoded_commitment_info = SubscriptionCommitmentInfoIOS.from_dict(data["commitmentInfo"], report_errors) + if decoded_commitment_info == null: + if report_errors: + push_error("Invalid required SubscriptionCommitmentInfoIOS value for commitmentInfo") + return null + obj.commitment_info = decoded_commitment_info + else: + if report_errors: + push_error("Expected commitmentInfo to be a SubscriptionCommitmentInfoIOS dictionary") + return null + else: + if report_errors: + push_error("Missing required commitmentInfo value") + return null if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: if data["subscriptionOffers"] is Array: var arr: Array[SubscriptionOffer] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(SubscriptionOffer.from_dict(item)) + var decoded_subscription_offer = SubscriptionOffer.from_dict(item, report_errors) + if decoded_subscription_offer == null: + return null + arr.append(decoded_subscription_offer) elif item is SubscriptionOffer: arr.append(item) + else: + if report_errors: + push_error("Invalid SubscriptionOffer list value for subscriptionOffers") + return null obj.subscription_offers = arr return obj @@ -3209,7 +3415,7 @@ class UserChoiceBillingDetails: ## Structured product details selected in the user-choice flow, including the product type and offer token. Legacy payloads may omit this field; use products as the product-ID fallback. Available in OpenIAP Spec 2.3.0 / openiap-google 2.3.1 (requires Play Billing 9.1+). var product_details_android: Array[DeveloperProvidedBillingProductAndroid] = [] - static func from_dict(data: Dictionary) -> UserChoiceBillingDetails: + static func from_dict(data: Dictionary, report_errors: bool = true) -> UserChoiceBillingDetails: var obj = UserChoiceBillingDetails.new() if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: obj.external_transaction_token = data["externalTransactionToken"] @@ -3227,9 +3433,16 @@ class UserChoiceBillingDetails: var arr: Array[DeveloperProvidedBillingProductAndroid] = [] for item in data["productDetailsAndroid"]: if item is Dictionary: - arr.append(DeveloperProvidedBillingProductAndroid.from_dict(item)) + var decoded_developer_provided_billing_product_android = DeveloperProvidedBillingProductAndroid.from_dict(item, report_errors) + if decoded_developer_provided_billing_product_android == null: + return null + arr.append(decoded_developer_provided_billing_product_android) elif item is DeveloperProvidedBillingProductAndroid: arr.append(item) + else: + if report_errors: + push_error("Invalid DeveloperProvidedBillingProductAndroid list value for productDetailsAndroid") + return null obj.product_details_android = arr return obj @@ -3446,7 +3659,11 @@ class VerifyPurchaseWithProviderResult: ## Error details if verification failed var errors: Array[VerifyPurchaseWithProviderError] = [] - static func from_dict(data: Dictionary) -> VerifyPurchaseWithProviderResult: + static func from_dict(data: Dictionary, report_errors: bool = true) -> VerifyPurchaseWithProviderResult: + if not data.has("provider") or not ((data["provider"] is String and PURCHASE_VERIFICATION_PROVIDER_FROM_STRING.has(data["provider"])) or (data["provider"] is int and PURCHASE_VERIFICATION_PROVIDER_VALUES.has(data["provider"]))): + if report_errors: + push_error("Invalid VerifyPurchaseWithProviderResult.provider enum value") + return null var obj = VerifyPurchaseWithProviderResult.new() if data.has("provider") and data["provider"] != null: var enum_str = data["provider"] @@ -3467,6 +3684,9 @@ class VerifyPurchaseWithProviderResult: arr.append(VerifyPurchaseWithProviderError.from_dict(item)) elif item is VerifyPurchaseWithProviderError: arr.append(item) + else: + push_error("Invalid VerifyPurchaseWithProviderError list value for errors") + return null obj.errors = arr return obj @@ -3539,7 +3759,11 @@ class BillingProgramInformationDialogParamsAndroid: ## External transaction token returned by the Billing Choice reporting-details flow. var external_transaction_token: String = "" - static func from_dict(data: Dictionary) -> BillingProgramInformationDialogParamsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> BillingProgramInformationDialogParamsAndroid: + if data.has("billingProgram") and data["billingProgram"] != null and not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid BillingProgramInformationDialogParamsAndroid.billingProgram enum value") + return null var obj = BillingProgramInformationDialogParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3595,8 +3819,10 @@ class DeveloperBillingOptionParamsAndroid: ## A pre-generated external transaction token for a Billing Choice external-link flow. Omit it when Google Play should provide the token in the callback. var external_transaction_token: Variant = null - static func from_dict(data: Dictionary) -> DeveloperBillingOptionParamsAndroid: - if not data.has("billingProgram") or not data["billingProgram"] is String or not BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"]): + static func from_dict(data: Dictionary, report_errors: bool = true) -> DeveloperBillingOptionParamsAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid DeveloperBillingOptionParamsAndroid.billingProgram enum value") return null var obj = DeveloperBillingOptionParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: @@ -3684,7 +3910,15 @@ class GetBillingChoiceInfoParamsAndroid: ## BCP 47 locale tag. If omitted, Play Billing uses the user's default locale. var user_locale: Variant = null - static func from_dict(data: Dictionary) -> GetBillingChoiceInfoParamsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> GetBillingChoiceInfoParamsAndroid: + if data.has("billingProgram") and data["billingProgram"] != null and not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid GetBillingChoiceInfoParamsAndroid.billingProgram enum value") + return null + if data.has("playBillingChoiceImageLayout") and data["playBillingChoiceImageLayout"] != null and not ((data["playBillingChoiceImageLayout"] is String and BILLING_CHOICE_IMAGE_LAYOUT_ANDROID_FROM_STRING.has(data["playBillingChoiceImageLayout"])) or (data["playBillingChoiceImageLayout"] is int and BILLING_CHOICE_IMAGE_LAYOUT_ANDROID_VALUES.has(data["playBillingChoiceImageLayout"]))): + if report_errors: + push_error("Invalid GetBillingChoiceInfoParamsAndroid.playBillingChoiceImageLayout enum value") + return null var obj = GetBillingChoiceInfoParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3731,8 +3965,10 @@ class InAppMessageParamsAndroid: for item in data["categories"]: if item is String: arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.get(item, InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID)) - elif item is int: + elif item is int and IN_APP_MESSAGE_CATEGORY_ANDROID_VALUES.has(item): arr.append(item) + elif item is int: + arr.append(InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID) obj.categories = arr return obj @@ -3798,7 +4034,19 @@ class LaunchExternalLinkParamsAndroid: ## External transaction token for a developer-rendered Billing Choice external-link flow. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). Generate it with createBillingProgramReportingDetailsAndroid. var external_transaction_token: Variant = null - static func from_dict(data: Dictionary) -> LaunchExternalLinkParamsAndroid: + static func from_dict(data: Dictionary, report_errors: bool = true) -> LaunchExternalLinkParamsAndroid: + if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): + if report_errors: + push_error("Invalid LaunchExternalLinkParamsAndroid.billingProgram enum value") + return null + if not data.has("launchMode") or not ((data["launchMode"] is String and EXTERNAL_LINK_LAUNCH_MODE_ANDROID_FROM_STRING.has(data["launchMode"])) or (data["launchMode"] is int and EXTERNAL_LINK_LAUNCH_MODE_ANDROID_VALUES.has(data["launchMode"]))): + if report_errors: + push_error("Invalid LaunchExternalLinkParamsAndroid.launchMode enum value") + return null + if not data.has("linkType") or not ((data["linkType"] is String and EXTERNAL_LINK_TYPE_ANDROID_FROM_STRING.has(data["linkType"])) or (data["linkType"] is int and EXTERNAL_LINK_TYPE_ANDROID_VALUES.has(data["linkType"]))): + if report_errors: + push_error("Invalid LaunchExternalLinkParamsAndroid.linkType enum value") + return null var obj = LaunchExternalLinkParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: var enum_str = data["billingProgram"] @@ -3912,7 +4160,7 @@ class PurchaseInput: ## Store where purchase was made var store: Variant = null var quantity: int = 0 - var purchase_state: PurchaseState + var purchase_state: PurchaseState = PurchaseState.UNKNOWN var is_auto_renewing: bool = false static func from_dict(data: Dictionary) -> PurchaseInput: @@ -4059,7 +4307,14 @@ class RequestPurchaseAndroidProps: obj.offer_token = data["offerToken"] if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: - obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + var decoded_developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + if decoded_developer_billing_option == null: + push_error("Invalid input DeveloperBillingOptionParamsAndroid value for developerBillingOption") + return null + obj.developer_billing_option = decoded_developer_billing_option + else: + push_error("Expected developerBillingOption to be a DeveloperBillingOptionParamsAndroid dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4107,9 +4362,14 @@ class RequestPurchaseIosProps: obj.quantity = data["quantity"] if data.has("withOffer") and data["withOffer"] != null: if data["withOffer"] is Dictionary: - obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) - else: - obj.with_offer = data["withOffer"] + var decoded_with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) + if decoded_with_offer == null: + push_error("Invalid input DiscountOfferInputIOS value for withOffer") + return null + obj.with_offer = decoded_with_offer + else: + push_error("Expected withOffer to be a DiscountOfferInputIOS dictionary") + return null if data.has("advancedCommerceData") and data["advancedCommerceData"] != null: obj.advanced_commerce_data = data["advancedCommerceData"] return obj @@ -4205,14 +4465,24 @@ class RequestPurchasePropsByPlatforms: var obj = RequestPurchasePropsByPlatforms.new() if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = RequestPurchaseIosProps.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = RequestPurchaseIosProps.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input RequestPurchaseIosProps value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a RequestPurchaseIosProps dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = RequestPurchaseAndroidProps.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = RequestPurchaseAndroidProps.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input RequestPurchaseAndroidProps value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a RequestPurchaseAndroidProps dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4276,15 +4546,30 @@ class RequestSubscriptionAndroidProps: arr.append(AndroidSubscriptionOfferInput.from_dict(item)) elif item is AndroidSubscriptionOfferInput: arr.append(item) + else: + push_error("Invalid AndroidSubscriptionOfferInput list value for subscriptionOffers") + return null obj.subscription_offers = arr if data.has("subscriptionProductReplacementParams") and data["subscriptionProductReplacementParams"] != null: if data["subscriptionProductReplacementParams"] is Dictionary: - obj.subscription_product_replacement_params = SubscriptionProductReplacementParamsAndroid.from_dict(data["subscriptionProductReplacementParams"]) - else: - obj.subscription_product_replacement_params = data["subscriptionProductReplacementParams"] + var decoded_subscription_product_replacement_params = SubscriptionProductReplacementParamsAndroid.from_dict(data["subscriptionProductReplacementParams"]) + if decoded_subscription_product_replacement_params == null: + push_error("Invalid input SubscriptionProductReplacementParamsAndroid value for subscriptionProductReplacementParams") + return null + obj.subscription_product_replacement_params = decoded_subscription_product_replacement_params + else: + push_error("Expected subscriptionProductReplacementParams to be a SubscriptionProductReplacementParamsAndroid dictionary") + return null if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: - obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + var decoded_developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + if decoded_developer_billing_option == null: + push_error("Invalid input DeveloperBillingOptionParamsAndroid value for developerBillingOption") + return null + obj.developer_billing_option = decoded_developer_billing_option + else: + push_error("Expected developerBillingOption to be a DeveloperBillingOptionParamsAndroid dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4351,19 +4636,34 @@ class RequestSubscriptionIosProps: obj.quantity = data["quantity"] if data.has("withOffer") and data["withOffer"] != null: if data["withOffer"] is Dictionary: - obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) - else: - obj.with_offer = data["withOffer"] + var decoded_with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) + if decoded_with_offer == null: + push_error("Invalid input DiscountOfferInputIOS value for withOffer") + return null + obj.with_offer = decoded_with_offer + else: + push_error("Expected withOffer to be a DiscountOfferInputIOS dictionary") + return null if data.has("winBackOffer") and data["winBackOffer"] != null: if data["winBackOffer"] is Dictionary: - obj.win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"]) - else: - obj.win_back_offer = data["winBackOffer"] + var decoded_win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"]) + if decoded_win_back_offer == null: + push_error("Invalid input WinBackOfferInputIOS value for winBackOffer") + return null + obj.win_back_offer = decoded_win_back_offer + else: + push_error("Expected winBackOffer to be a WinBackOfferInputIOS dictionary") + return null if data.has("promotionalOfferJWS") and data["promotionalOfferJWS"] != null: if data["promotionalOfferJWS"] is Dictionary: - obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"]) - else: - obj.promotional_offer_jws = data["promotionalOfferJWS"] + var decoded_promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"]) + if decoded_promotional_offer_jws == null: + push_error("Invalid input PromotionalOfferJWSInputIOS value for promotionalOfferJWS") + return null + obj.promotional_offer_jws = decoded_promotional_offer_jws + else: + push_error("Expected promotionalOfferJWS to be a PromotionalOfferJWSInputIOS dictionary") + return null if data.has("billingPlanType") and data["billingPlanType"] != null: var enum_str = data["billingPlanType"] if enum_str is String: @@ -4425,14 +4725,24 @@ class RequestSubscriptionPropsByPlatforms: var obj = RequestSubscriptionPropsByPlatforms.new() if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = RequestSubscriptionIosProps.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = RequestSubscriptionIosProps.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input RequestSubscriptionIosProps value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a RequestSubscriptionIosProps dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = RequestSubscriptionAndroidProps.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = RequestSubscriptionAndroidProps.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input RequestSubscriptionAndroidProps value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a RequestSubscriptionAndroidProps dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4540,19 +4850,34 @@ class RequestVerifyPurchaseWithIapkitProps: obj.include_client_payload = data["includeClientPayload"] if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = RequestVerifyPurchaseWithIapkitAppleProps.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = RequestVerifyPurchaseWithIapkitAppleProps.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitAppleProps value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a RequestVerifyPurchaseWithIapkitAppleProps dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = RequestVerifyPurchaseWithIapkitGoogleProps.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = RequestVerifyPurchaseWithIapkitGoogleProps.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitGoogleProps value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a RequestVerifyPurchaseWithIapkitGoogleProps dictionary") + return null if data.has("amazon") and data["amazon"] != null: if data["amazon"] is Dictionary: - obj.amazon = RequestVerifyPurchaseWithIapkitAmazonProps.from_dict(data["amazon"]) - else: - obj.amazon = data["amazon"] + var decoded_amazon = RequestVerifyPurchaseWithIapkitAmazonProps.from_dict(data["amazon"]) + if decoded_amazon == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitAmazonProps value for amazon") + return null + obj.amazon = decoded_amazon + else: + push_error("Expected amazon to be a RequestVerifyPurchaseWithIapkitAmazonProps dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4585,7 +4910,7 @@ class SubscriptionProductReplacementParamsAndroid: ## The old product ID that needs to be replaced var old_product_id: String = "" ## The replacement mode for this product change - var replacement_mode: SubscriptionReplacementModeAndroid + var replacement_mode: SubscriptionReplacementModeAndroid = SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE static func from_dict(data: Dictionary) -> SubscriptionProductReplacementParamsAndroid: var obj = SubscriptionProductReplacementParamsAndroid.new() @@ -4712,19 +5037,34 @@ class VerifyPurchaseProps: var obj = VerifyPurchaseProps.new() if data.has("apple") and data["apple"] != null: if data["apple"] is Dictionary: - obj.apple = VerifyPurchaseAppleOptions.from_dict(data["apple"]) - else: - obj.apple = data["apple"] + var decoded_apple = VerifyPurchaseAppleOptions.from_dict(data["apple"]) + if decoded_apple == null: + push_error("Invalid input VerifyPurchaseAppleOptions value for apple") + return null + obj.apple = decoded_apple + else: + push_error("Expected apple to be a VerifyPurchaseAppleOptions dictionary") + return null if data.has("google") and data["google"] != null: if data["google"] is Dictionary: - obj.google = VerifyPurchaseGoogleOptions.from_dict(data["google"]) - else: - obj.google = data["google"] + var decoded_google = VerifyPurchaseGoogleOptions.from_dict(data["google"]) + if decoded_google == null: + push_error("Invalid input VerifyPurchaseGoogleOptions value for google") + return null + obj.google = decoded_google + else: + push_error("Expected google to be a VerifyPurchaseGoogleOptions dictionary") + return null if data.has("horizon") and data["horizon"] != null: if data["horizon"] is Dictionary: - obj.horizon = VerifyPurchaseHorizonOptions.from_dict(data["horizon"]) - else: - obj.horizon = data["horizon"] + var decoded_horizon = VerifyPurchaseHorizonOptions.from_dict(data["horizon"]) + if decoded_horizon == null: + push_error("Invalid input VerifyPurchaseHorizonOptions value for horizon") + return null + obj.horizon = decoded_horizon + else: + push_error("Expected horizon to be a VerifyPurchaseHorizonOptions dictionary") + return null return obj func to_dict() -> Dictionary: @@ -4750,7 +5090,11 @@ class VerifyPurchaseWithProviderProps: var provider: PurchaseVerificationProvider var iapkit: RequestVerifyPurchaseWithIapkitProps - static func from_dict(data: Dictionary) -> VerifyPurchaseWithProviderProps: + static func from_dict(data: Dictionary, report_errors: bool = true) -> VerifyPurchaseWithProviderProps: + if not data.has("provider") or not ((data["provider"] is String and PURCHASE_VERIFICATION_PROVIDER_FROM_STRING.has(data["provider"])) or (data["provider"] is int and PURCHASE_VERIFICATION_PROVIDER_VALUES.has(data["provider"]))): + if report_errors: + push_error("Invalid VerifyPurchaseWithProviderProps.provider enum value") + return null var obj = VerifyPurchaseWithProviderProps.new() if data.has("provider") and data["provider"] != null: var enum_str = data["provider"] @@ -4760,9 +5104,14 @@ class VerifyPurchaseWithProviderProps: obj.provider = enum_str if data.has("iapkit") and data["iapkit"] != null: if data["iapkit"] is Dictionary: - obj.iapkit = RequestVerifyPurchaseWithIapkitProps.from_dict(data["iapkit"]) - else: - obj.iapkit = data["iapkit"] + var decoded_iapkit = RequestVerifyPurchaseWithIapkitProps.from_dict(data["iapkit"]) + if decoded_iapkit == null: + push_error("Invalid input RequestVerifyPurchaseWithIapkitProps value for iapkit") + return null + obj.iapkit = decoded_iapkit + else: + push_error("Expected iapkit to be a RequestVerifyPurchaseWithIapkitProps dictionary") + return null return obj func to_dict() -> Dictionary: From d9c981043eb6f5434fa8f428f373721f47beaae8 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 01:14:27 +0900 Subject: [PATCH 03/12] fix(kit): contain settings card actions --- packages/kit/CONVENTION.md | 25 +++++++-- packages/kit/src/index.css | 27 ++++++++++ .../organization/project/settings.test.tsx | 52 +++++++++++++++++++ .../auth/organization/project/settings.tsx | 36 ++++++------- 4 files changed, 118 insertions(+), 22 deletions(-) diff --git a/packages/kit/CONVENTION.md b/packages/kit/CONVENTION.md index 80814867d..9dbb60f27 100644 --- a/packages/kit/CONVENTION.md +++ b/packages/kit/CONVENTION.md @@ -60,6 +60,23 @@ src/ - Page folders with sub-components use `index.tsx` for the route entry point. +## Responsive card containment + +Cards with dynamic text and trailing controls must keep every child inside the +card at every supported width: + +- Let horizontal card rows wrap instead of assuming the label and actions fit + on one line. +- Put `min-width: 0` on flexible flex/grid children; otherwise filenames and + other intrinsic-width content can push controls outside the parent. +- Allow untrusted or user-provided strings to wrap anywhere, and keep icons and + action buttons non-shrinking. +- Let the action group wrap onto its own row when necessary. Do not use clipping + as the primary fix because it can hide reachable controls or focus rings. + +Use the shared `contained-action-card` classes from `src/index.css` for this +layout instead of recreating the flex constraints per card. + ## Convex Backend (CQRS) Every domain folder under `convex/` follows the same split: @@ -231,7 +248,7 @@ Pattern (mirrors `convex/products/jobs.ts` + `runProductSyncIOS` / 1. **Schema**: a `*Jobs` table with `status` (`queued | running | succeeded | failed`), `progress` (`{phase, - current?, total?, failuresCount?}`), `result?`, `error?`, +current?, total?, failuresCount?}`), `result?`, `error?`, `cancelRequested?`, `expectedDeadline?`, `createdBy?`, `startedAt?`, `completedAt?`, `createdAt`. Indexes: `(projectId, platform, status)` for active-job lookup, @@ -239,7 +256,7 @@ Pattern (mirrors `convex/products/jobs.ts` + `runProductSyncIOS` / `(status, completedAt)` for the pruner. 2. **Enqueue mutation**: validates membership, dedups against an existing `queued`/`running` row for the same `(projectId, - platform)`, inserts the row, schedules the worker via +platform)`, inserts the row, schedules the worker via `ctx.scheduler.runAfter(0, internal..runX, { jobId })`. Returns `{ jobId, deduped }`. 3. **Worker internalAction**: `args: { jobId }`. Reads job → @@ -259,8 +276,8 @@ Pattern (mirrors `convex/products/jobs.ts` + `runProductSyncIOS` / fires once via a `useRef`-gated `useEffect` so reactive updates don't re-toast. 6. **HTTP**: `POST .../sync/...` returns 202 with `{ jobId, - deduped }`; `GET .../sync/jobs/{jobId}` polls; `POST - .../sync/jobs/{jobId}/cancel` cancels. Clients backoff at ~3 s +deduped }`; `GET .../sync/jobs/{jobId}` polls; `POST +.../sync/jobs/{jobId}/cancel` cancels. Clients backoff at ~3 s intervals. Failures arrays should pass through `truncateFailures` (cap 200, diff --git a/packages/kit/src/index.css b/packages/kit/src/index.css index e506b5acf..b7928b9e4 100644 --- a/packages/kit/src/index.css +++ b/packages/kit/src/index.css @@ -585,6 +585,33 @@ border: 1px solid var(--border); } + /* Keep dynamic text and trailing actions inside horizontal cards. */ + .contained-action-card { + @apply flex w-full min-w-0 flex-wrap items-center gap-2; + } + + .contained-action-card > * { + max-width: 100%; + } + + .contained-action-card__content { + @apply flex min-w-0 flex-1 basis-64 items-center gap-2; + } + + .contained-action-card__content > svg { + @apply shrink-0; + } + + .contained-action-card__text { + @apply min-w-0 flex-1; + overflow-wrap: anywhere; + word-break: break-word; + } + + .contained-action-card__actions { + @apply ml-auto flex max-w-full shrink-0 flex-wrap items-center justify-end gap-1; + } + /* Modal overlay */ .modal-overlay { @apply fixed inset-0 z-50 bg-black/50 backdrop-blur-sm; diff --git a/packages/kit/src/pages/auth/organization/project/settings.test.tsx b/packages/kit/src/pages/auth/organization/project/settings.test.tsx index 758549283..8e741b1a1 100644 --- a/packages/kit/src/pages/auth/organization/project/settings.test.tsx +++ b/packages/kit/src/pages/auth/organization/project/settings.test.tsx @@ -223,6 +223,58 @@ describe("ProjectSettings", () => { ); }); + it("keeps uploaded-file actions in wrapping cards", () => { + mocks.files = [ + { + _id: "files_apple_server", + projectId: "projects_test", + purpose: "apple_p8_key", + fileName: "SubscriptionKey_WithAnIntentionallyLongName.p8", + fileSize: 256, + }, + { + _id: "files_apple_connect", + projectId: "projects_test", + purpose: "apple_p8_asc_api_key", + fileName: "ConnectKey_WithAnIntentionallyLongName.p8", + fileSize: 256, + }, + { + _id: "files_apple_review", + projectId: "projects_test", + purpose: "apple_iap_review_screenshot", + fileName: "ReviewScreenshot_WithAnIntentionallyLongName.png", + fileSize: 1024, + }, + { + _id: "files_android", + projectId: "projects_test", + purpose: "android_service_account", + fileName: "ServiceAccount_WithAnIntentionallyLongName.json", + fileSize: 256, + }, + ]; + + render(); + + for (const status of [ + "Authentication file uploaded successfully", + "Connect API key uploaded successfully", + "App Review screenshot configured", + "Service account file uploaded successfully", + ]) { + const card = screen.getByText(status).closest(".contained-action-card"); + expect(card).not.toBeNull(); + expect( + card?.querySelector(".contained-action-card__content"), + ).toBeTruthy(); + expect(card?.querySelector(".contained-action-card__text")).toBeTruthy(); + expect( + card?.querySelector(".contained-action-card__actions"), + ).toBeTruthy(); + } + }); + it.each([ { inputId: "ios-file-upload", diff --git a/packages/kit/src/pages/auth/organization/project/settings.tsx b/packages/kit/src/pages/auth/organization/project/settings.tsx index 9d23d2cc3..913be3b11 100644 --- a/packages/kit/src/pages/auth/organization/project/settings.tsx +++ b/packages/kit/src/pages/auth/organization/project/settings.tsx @@ -1363,10 +1363,10 @@ export default function ProjectSettings() { {iosFileUploaded || hasIosFile ? (
    -
    -
    +
    +
    -
    +
    {"Authentication file uploaded successfully"} @@ -1378,7 +1378,7 @@ export default function ProjectSettings() { )}
    -
    +
    {iosFile && (
  • - Never fail a verification because a value is unrecognised. Log it - and continue. + Never fail a verification because optional metadata is unrecognised. + Required verdict and security-boundary values still fail closed.
  • Keep the SDK reasonably current so new fields become available, but 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 f59999575..5910d107c 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 @@ -28,7 +28,7 @@ public enum class BillingChoiceImageLayoutAndroid(val rawValue: String) { RectangularTwoByTwo("rectangular-two-by-two"); companion object { - fun fromJson(value: String): BillingChoiceImageLayoutAndroid = when (value) { + private fun fromKnownJson(value: String): BillingChoiceImageLayoutAndroid? = when (value) { "rectangular-four-by-one" -> BillingChoiceImageLayoutAndroid.RectangularFourByOne "RECTANGULAR_FOUR_BY_ONE" -> BillingChoiceImageLayoutAndroid.RectangularFourByOne "RectangularFourByOne" -> BillingChoiceImageLayoutAndroid.RectangularFourByOne @@ -38,8 +38,13 @@ public enum class BillingChoiceImageLayoutAndroid(val rawValue: String) { "rectangular-two-by-two" -> BillingChoiceImageLayoutAndroid.RectangularTwoByTwo "RECTANGULAR_TWO_BY_TWO" -> BillingChoiceImageLayoutAndroid.RectangularTwoByTwo "RectangularTwoByTwo" -> BillingChoiceImageLayoutAndroid.RectangularTwoByTwo - else -> throw IllegalArgumentException("Unknown BillingChoiceImageLayoutAndroid value: $value") + else -> null } + + fun fromJson(value: String): BillingChoiceImageLayoutAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceImageLayoutAndroid value: $value") + + fun fromJsonStrict(value: String): BillingChoiceImageLayoutAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceImageLayoutAndroid input value: $value") } fun toJson(): String = rawValue @@ -64,7 +69,7 @@ public enum class BillingChoiceScreenTypeAndroid(val rawValue: String) { GoogleRendered("google-rendered"); companion object { - fun fromJson(value: String): BillingChoiceScreenTypeAndroid = when (value) { + private fun fromKnownJson(value: String): BillingChoiceScreenTypeAndroid? = when (value) { "unspecified" -> BillingChoiceScreenTypeAndroid.Unspecified "UNSPECIFIED" -> BillingChoiceScreenTypeAndroid.Unspecified "Unspecified" -> BillingChoiceScreenTypeAndroid.Unspecified @@ -74,8 +79,13 @@ public enum class BillingChoiceScreenTypeAndroid(val rawValue: String) { "google-rendered" -> BillingChoiceScreenTypeAndroid.GoogleRendered "GOOGLE_RENDERED" -> BillingChoiceScreenTypeAndroid.GoogleRendered "GoogleRendered" -> BillingChoiceScreenTypeAndroid.GoogleRendered - else -> throw IllegalArgumentException("Unknown BillingChoiceScreenTypeAndroid value: $value") + else -> null } + + fun fromJson(value: String): BillingChoiceScreenTypeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceScreenTypeAndroid value: $value") + + fun fromJsonStrict(value: String): BillingChoiceScreenTypeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceScreenTypeAndroid input value: $value") } fun toJson(): String = rawValue @@ -126,7 +136,7 @@ public enum class BillingProgramAndroid(val rawValue: String) { BillingChoice("billing-choice"); companion object { - fun fromJson(value: String): BillingProgramAndroid = when (value) { + private fun fromKnownJson(value: String): BillingProgramAndroid? = when (value) { "unspecified" -> BillingProgramAndroid.Unspecified "UNSPECIFIED" -> BillingProgramAndroid.Unspecified "Unspecified" -> BillingProgramAndroid.Unspecified @@ -145,8 +155,13 @@ public enum class BillingProgramAndroid(val rawValue: String) { "billing-choice" -> BillingProgramAndroid.BillingChoice "BILLING_CHOICE" -> BillingProgramAndroid.BillingChoice "BillingChoice" -> BillingProgramAndroid.BillingChoice - else -> throw IllegalArgumentException("Unknown BillingProgramAndroid value: $value") + else -> null } + + fun fromJson(value: String): BillingProgramAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingProgramAndroid value: $value") + + fun fromJsonStrict(value: String): BillingProgramAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingProgramAndroid input value: $value") } fun toJson(): String = rawValue @@ -174,7 +189,7 @@ public enum class DeveloperBillingLaunchModeAndroid(val rawValue: String) { CallerWillLaunchLink("caller-will-launch-link"); companion object { - fun fromJson(value: String): DeveloperBillingLaunchModeAndroid = when (value) { + private fun fromKnownJson(value: String): DeveloperBillingLaunchModeAndroid? = when (value) { "unspecified" -> DeveloperBillingLaunchModeAndroid.Unspecified "UNSPECIFIED" -> DeveloperBillingLaunchModeAndroid.Unspecified "Unspecified" -> DeveloperBillingLaunchModeAndroid.Unspecified @@ -184,8 +199,13 @@ public enum class DeveloperBillingLaunchModeAndroid(val rawValue: String) { "caller-will-launch-link" -> DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink "CALLER_WILL_LAUNCH_LINK" -> DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink "CallerWillLaunchLink" -> DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink - else -> throw IllegalArgumentException("Unknown DeveloperBillingLaunchModeAndroid value: $value") + else -> null } + + fun fromJson(value: String): DeveloperBillingLaunchModeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingLaunchModeAndroid value: $value") + + fun fromJsonStrict(value: String): DeveloperBillingLaunchModeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingLaunchModeAndroid input value: $value") } fun toJson(): String = rawValue @@ -210,7 +230,7 @@ public enum class DeveloperBillingTypeAndroid(val rawValue: String) { ExternalLink("external-link"); companion object { - fun fromJson(value: String): DeveloperBillingTypeAndroid = when (value) { + private fun fromKnownJson(value: String): DeveloperBillingTypeAndroid? = when (value) { "developer-billing-type-unspecified" -> DeveloperBillingTypeAndroid.DeveloperBillingTypeUnspecified "DEVELOPER_BILLING_TYPE_UNSPECIFIED" -> DeveloperBillingTypeAndroid.DeveloperBillingTypeUnspecified "DeveloperBillingTypeUnspecified" -> DeveloperBillingTypeAndroid.DeveloperBillingTypeUnspecified @@ -220,8 +240,13 @@ public enum class DeveloperBillingTypeAndroid(val rawValue: String) { "external-link" -> DeveloperBillingTypeAndroid.ExternalLink "EXTERNAL_LINK" -> DeveloperBillingTypeAndroid.ExternalLink "ExternalLink" -> DeveloperBillingTypeAndroid.ExternalLink - else -> throw IllegalArgumentException("Unknown DeveloperBillingTypeAndroid value: $value") + else -> null } + + fun fromJson(value: String): DeveloperBillingTypeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingTypeAndroid value: $value") + + fun fromJsonStrict(value: String): DeveloperBillingTypeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingTypeAndroid input value: $value") } fun toJson(): String = rawValue @@ -246,7 +271,7 @@ public enum class DiscountOfferType(val rawValue: String) { OneTime("one-time"); companion object { - fun fromJson(value: String): DiscountOfferType = when (value) { + private fun fromKnownJson(value: String): DiscountOfferType? = when (value) { "introductory" -> DiscountOfferType.Introductory "INTRODUCTORY" -> DiscountOfferType.Introductory "Introductory" -> DiscountOfferType.Introductory @@ -256,8 +281,13 @@ public enum class DiscountOfferType(val rawValue: String) { "one-time" -> DiscountOfferType.OneTime "ONE_TIME" -> DiscountOfferType.OneTime "OneTime" -> DiscountOfferType.OneTime - else -> throw IllegalArgumentException("Unknown DiscountOfferType value: $value") + else -> null } + + fun fromJson(value: String): DiscountOfferType = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DiscountOfferType value: $value") + + fun fromJsonStrict(value: String): DiscountOfferType = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DiscountOfferType input value: $value") } fun toJson(): String = rawValue @@ -302,7 +332,7 @@ public enum class ErrorCode(val rawValue: String) { DuplicatePurchase("duplicate-purchase"); companion object { - fun fromJson(value: String): ErrorCode = when (value) { + private fun fromKnownJson(value: String): ErrorCode? = when (value) { "unknown" -> ErrorCode.Unknown "UNKNOWN" -> ErrorCode.Unknown "Unknown" -> ErrorCode.Unknown @@ -411,8 +441,13 @@ public enum class ErrorCode(val rawValue: String) { "duplicate-purchase" -> ErrorCode.DuplicatePurchase "DUPLICATE_PURCHASE" -> ErrorCode.DuplicatePurchase "DuplicatePurchase" -> ErrorCode.DuplicatePurchase - else -> ErrorCode.Unknown + else -> null } + + fun fromJson(value: String): ErrorCode = fromKnownJson(value) ?: ErrorCode.Unknown + + fun fromJsonStrict(value: String): ErrorCode = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ErrorCode input value: $value") } fun toJson(): String = rawValue @@ -439,7 +474,7 @@ public enum class ExternalLinkLaunchModeAndroid(val rawValue: String) { CallerWillLaunchLink("caller-will-launch-link"); companion object { - fun fromJson(value: String): ExternalLinkLaunchModeAndroid = when (value) { + private fun fromKnownJson(value: String): ExternalLinkLaunchModeAndroid? = when (value) { "unspecified" -> ExternalLinkLaunchModeAndroid.Unspecified "UNSPECIFIED" -> ExternalLinkLaunchModeAndroid.Unspecified "Unspecified" -> ExternalLinkLaunchModeAndroid.Unspecified @@ -449,8 +484,13 @@ public enum class ExternalLinkLaunchModeAndroid(val rawValue: String) { "caller-will-launch-link" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink "CALLER_WILL_LAUNCH_LINK" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink "CallerWillLaunchLink" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink - else -> throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid value: $value") + else -> null } + + fun fromJson(value: String): ExternalLinkLaunchModeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid value: $value") + + fun fromJsonStrict(value: String): ExternalLinkLaunchModeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid input value: $value") } fun toJson(): String = rawValue @@ -476,7 +516,7 @@ public enum class ExternalLinkTypeAndroid(val rawValue: String) { LinkToAppDownload("link-to-app-download"); companion object { - fun fromJson(value: String): ExternalLinkTypeAndroid = when (value) { + private fun fromKnownJson(value: String): ExternalLinkTypeAndroid? = when (value) { "unspecified" -> ExternalLinkTypeAndroid.Unspecified "UNSPECIFIED" -> ExternalLinkTypeAndroid.Unspecified "Unspecified" -> ExternalLinkTypeAndroid.Unspecified @@ -486,8 +526,13 @@ public enum class ExternalLinkTypeAndroid(val rawValue: String) { "link-to-app-download" -> ExternalLinkTypeAndroid.LinkToAppDownload "LINK_TO_APP_DOWNLOAD" -> ExternalLinkTypeAndroid.LinkToAppDownload "LinkToAppDownload" -> ExternalLinkTypeAndroid.LinkToAppDownload - else -> throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid value: $value") + else -> null } + + fun fromJson(value: String): ExternalLinkTypeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid value: $value") + + fun fromJsonStrict(value: String): ExternalLinkTypeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid input value: $value") } fun toJson(): String = rawValue @@ -506,12 +551,17 @@ public enum class ExternalPurchaseCustomLinkNoticeTypeIOS(val rawValue: String) Browser("browser"); companion object { - fun fromJson(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS = when (value) { + private fun fromKnownJson(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS? = when (value) { "browser" -> ExternalPurchaseCustomLinkNoticeTypeIOS.Browser "BROWSER" -> ExternalPurchaseCustomLinkNoticeTypeIOS.Browser "Browser" -> ExternalPurchaseCustomLinkNoticeTypeIOS.Browser - else -> throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value") + + fun fromJsonStrict(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkNoticeTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -535,15 +585,20 @@ public enum class ExternalPurchaseCustomLinkTokenTypeIOS(val rawValue: String) { Services("services"); companion object { - fun fromJson(value: String): ExternalPurchaseCustomLinkTokenTypeIOS = when (value) { + private fun fromKnownJson(value: String): ExternalPurchaseCustomLinkTokenTypeIOS? = when (value) { "acquisition" -> ExternalPurchaseCustomLinkTokenTypeIOS.Acquisition "ACQUISITION" -> ExternalPurchaseCustomLinkTokenTypeIOS.Acquisition "Acquisition" -> ExternalPurchaseCustomLinkTokenTypeIOS.Acquisition "services" -> ExternalPurchaseCustomLinkTokenTypeIOS.Services "SERVICES" -> ExternalPurchaseCustomLinkTokenTypeIOS.Services "Services" -> ExternalPurchaseCustomLinkTokenTypeIOS.Services - else -> throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): ExternalPurchaseCustomLinkTokenTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value") + + fun fromJsonStrict(value: String): ExternalPurchaseCustomLinkTokenTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkTokenTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -563,15 +618,20 @@ public enum class ExternalPurchaseNoticeAction(val rawValue: String) { Dismissed("dismissed"); companion object { - fun fromJson(value: String): ExternalPurchaseNoticeAction = when (value) { + private fun fromKnownJson(value: String): ExternalPurchaseNoticeAction? = when (value) { "continue" -> ExternalPurchaseNoticeAction.Continue "CONTINUE" -> ExternalPurchaseNoticeAction.Continue "Continue" -> ExternalPurchaseNoticeAction.Continue "dismissed" -> ExternalPurchaseNoticeAction.Dismissed "DISMISSED" -> ExternalPurchaseNoticeAction.Dismissed "Dismissed" -> ExternalPurchaseNoticeAction.Dismissed - else -> throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction value: $value") + else -> null } + + fun fromJson(value: String): ExternalPurchaseNoticeAction = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction value: $value") + + fun fromJsonStrict(value: String): ExternalPurchaseNoticeAction = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction input value: $value") } fun toJson(): String = rawValue @@ -599,7 +659,7 @@ public enum class IapEvent(val rawValue: String) { SubscriptionBillingIssue("subscription-billing-issue"); companion object { - fun fromJson(value: String): IapEvent = when (value) { + private fun fromKnownJson(value: String): IapEvent? = when (value) { "purchase-updated" -> IapEvent.PurchaseUpdated "PURCHASE_UPDATED" -> IapEvent.PurchaseUpdated "PurchaseUpdated" -> IapEvent.PurchaseUpdated @@ -619,8 +679,13 @@ public enum class IapEvent(val rawValue: String) { "subscription-billing-issue" -> IapEvent.SubscriptionBillingIssue "SUBSCRIPTION_BILLING_ISSUE" -> IapEvent.SubscriptionBillingIssue "SubscriptionBillingIssue" -> IapEvent.SubscriptionBillingIssue - else -> throw IllegalArgumentException("Unknown IapEvent value: $value") + else -> null } + + fun fromJson(value: String): IapEvent = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapEvent value: $value") + + fun fromJsonStrict(value: String): IapEvent = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapEvent input value: $value") } fun toJson(): String = rawValue @@ -635,7 +700,7 @@ public enum class IapkitClientPayloadFormat(val rawValue: String) { Text("text"); companion object { - fun fromJson(value: String): IapkitClientPayloadFormat = when (value) { + private fun fromKnownJson(value: String): IapkitClientPayloadFormat? = when (value) { "toml" -> IapkitClientPayloadFormat.Toml "TOML" -> IapkitClientPayloadFormat.Toml "Toml" -> IapkitClientPayloadFormat.Toml @@ -645,8 +710,13 @@ public enum class IapkitClientPayloadFormat(val rawValue: String) { "text" -> IapkitClientPayloadFormat.Text "TEXT" -> IapkitClientPayloadFormat.Text "Text" -> IapkitClientPayloadFormat.Text - else -> throw IllegalArgumentException("Unknown IapkitClientPayloadFormat value: $value") + else -> null } + + fun fromJson(value: String): IapkitClientPayloadFormat = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapkitClientPayloadFormat value: $value") + + fun fromJsonStrict(value: String): IapkitClientPayloadFormat = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapkitClientPayloadFormat input value: $value") } fun toJson(): String = rawValue @@ -694,7 +764,7 @@ public enum class IapkitPurchaseState(val rawValue: String) { Inauthentic("inauthentic"); companion object { - fun fromJson(value: String): IapkitPurchaseState = when (value) { + private fun fromKnownJson(value: String): IapkitPurchaseState? = when (value) { "entitled" -> IapkitPurchaseState.Entitled "ENTITLED" -> IapkitPurchaseState.Entitled "Entitled" -> IapkitPurchaseState.Entitled @@ -722,8 +792,13 @@ public enum class IapkitPurchaseState(val rawValue: String) { "inauthentic" -> IapkitPurchaseState.Inauthentic "INAUTHENTIC" -> IapkitPurchaseState.Inauthentic "Inauthentic" -> IapkitPurchaseState.Inauthentic - else -> IapkitPurchaseState.Unknown + else -> null } + + fun fromJson(value: String): IapkitPurchaseState = fromKnownJson(value) ?: IapkitPurchaseState.Unknown + + fun fromJsonStrict(value: String): IapkitPurchaseState = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapkitPurchaseState input value: $value") } fun toJson(): String = rawValue @@ -734,15 +809,20 @@ public enum class IapPlatform(val rawValue: String) { Android("android"); companion object { - fun fromJson(value: String): IapPlatform = when (value) { + private fun fromKnownJson(value: String): IapPlatform? = when (value) { "ios" -> IapPlatform.Ios "IOS" -> IapPlatform.Ios "Ios" -> IapPlatform.Ios "android" -> IapPlatform.Android "ANDROID" -> IapPlatform.Android "Android" -> IapPlatform.Android - else -> throw IllegalArgumentException("Unknown IapPlatform value: $value") + else -> null } + + fun fromJson(value: String): IapPlatform = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapPlatform value: $value") + + fun fromJsonStrict(value: String): IapPlatform = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapPlatform input value: $value") } fun toJson(): String = rawValue @@ -756,7 +836,7 @@ public enum class IapStore(val rawValue: String) { Amazon("amazon"); companion object { - fun fromJson(value: String): IapStore = when (value) { + private fun fromKnownJson(value: String): IapStore? = when (value) { "unknown" -> IapStore.Unknown "UNKNOWN" -> IapStore.Unknown "Unknown" -> IapStore.Unknown @@ -772,8 +852,13 @@ public enum class IapStore(val rawValue: String) { "amazon" -> IapStore.Amazon "AMAZON" -> IapStore.Amazon "Amazon" -> IapStore.Amazon - else -> IapStore.Unknown + else -> null } + + fun fromJson(value: String): IapStore = fromKnownJson(value) ?: IapStore.Unknown + + fun fromJsonStrict(value: String): IapStore = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapStore input value: $value") } fun toJson(): String = rawValue @@ -795,15 +880,20 @@ public enum class InAppMessageCategoryAndroid(val rawValue: String) { Transactional("transactional"); companion object { - fun fromJson(value: String): InAppMessageCategoryAndroid = when (value) { + private fun fromKnownJson(value: String): InAppMessageCategoryAndroid? = when (value) { "unknown-in-app-message-category-id" -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId "UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID" -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId "UnknownInAppMessageCategoryId" -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId "transactional" -> InAppMessageCategoryAndroid.Transactional "TRANSACTIONAL" -> InAppMessageCategoryAndroid.Transactional "Transactional" -> InAppMessageCategoryAndroid.Transactional - else -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId + else -> null } + + fun fromJson(value: String): InAppMessageCategoryAndroid = fromKnownJson(value) ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId + + fun fromJsonStrict(value: String): InAppMessageCategoryAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid input value: $value") } fun toJson(): String = rawValue @@ -825,15 +915,20 @@ public enum class InAppMessageResponseCodeAndroid(val rawValue: String) { SubscriptionStatusUpdated("subscription-status-updated"); companion object { - fun fromJson(value: String): InAppMessageResponseCodeAndroid = when (value) { + private fun fromKnownJson(value: String): InAppMessageResponseCodeAndroid? = when (value) { "no-action-needed" -> InAppMessageResponseCodeAndroid.NoActionNeeded "NO_ACTION_NEEDED" -> InAppMessageResponseCodeAndroid.NoActionNeeded "NoActionNeeded" -> InAppMessageResponseCodeAndroid.NoActionNeeded "subscription-status-updated" -> InAppMessageResponseCodeAndroid.SubscriptionStatusUpdated "SUBSCRIPTION_STATUS_UPDATED" -> InAppMessageResponseCodeAndroid.SubscriptionStatusUpdated "SubscriptionStatusUpdated" -> InAppMessageResponseCodeAndroid.SubscriptionStatusUpdated - else -> throw IllegalArgumentException("Unknown InAppMessageResponseCodeAndroid value: $value") + else -> null } + + fun fromJson(value: String): InAppMessageResponseCodeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown InAppMessageResponseCodeAndroid value: $value") + + fun fromJsonStrict(value: String): InAppMessageResponseCodeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown InAppMessageResponseCodeAndroid input value: $value") } fun toJson(): String = rawValue @@ -862,7 +957,7 @@ public enum class PaymentMode(val rawValue: String) { Unknown("unknown"); companion object { - fun fromJson(value: String): PaymentMode = when (value) { + private fun fromKnownJson(value: String): PaymentMode? = when (value) { "free-trial" -> PaymentMode.FreeTrial "FREE_TRIAL" -> PaymentMode.FreeTrial "FreeTrial" -> PaymentMode.FreeTrial @@ -875,8 +970,13 @@ public enum class PaymentMode(val rawValue: String) { "unknown" -> PaymentMode.Unknown "UNKNOWN" -> PaymentMode.Unknown "Unknown" -> PaymentMode.Unknown - else -> PaymentMode.Unknown + else -> null } + + fun fromJson(value: String): PaymentMode = fromKnownJson(value) ?: PaymentMode.Unknown + + fun fromJsonStrict(value: String): PaymentMode = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PaymentMode input value: $value") } fun toJson(): String = rawValue @@ -889,7 +989,7 @@ public enum class PaymentModeIOS(val rawValue: String) { PayUpFront("pay-up-front"); companion object { - fun fromJson(value: String): PaymentModeIOS = when (value) { + private fun fromKnownJson(value: String): PaymentModeIOS? = when (value) { "empty" -> PaymentModeIOS.Empty "EMPTY" -> PaymentModeIOS.Empty "Empty" -> PaymentModeIOS.Empty @@ -902,8 +1002,13 @@ public enum class PaymentModeIOS(val rawValue: String) { "pay-up-front" -> PaymentModeIOS.PayUpFront "PAY_UP_FRONT" -> PaymentModeIOS.PayUpFront "PayUpFront" -> PaymentModeIOS.PayUpFront - else -> throw IllegalArgumentException("Unknown PaymentModeIOS value: $value") + else -> null } + + fun fromJson(value: String): PaymentModeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PaymentModeIOS value: $value") + + fun fromJsonStrict(value: String): PaymentModeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PaymentModeIOS input value: $value") } fun toJson(): String = rawValue @@ -915,7 +1020,7 @@ public enum class ProductQueryType(val rawValue: String) { All("all"); companion object { - fun fromJson(value: String): ProductQueryType = when (value) { + private fun fromKnownJson(value: String): ProductQueryType? = when (value) { "in-app" -> ProductQueryType.InApp "IN_APP" -> ProductQueryType.InApp "InApp" -> ProductQueryType.InApp @@ -925,8 +1030,13 @@ public enum class ProductQueryType(val rawValue: String) { "all" -> ProductQueryType.All "ALL" -> ProductQueryType.All "All" -> ProductQueryType.All - else -> throw IllegalArgumentException("Unknown ProductQueryType value: $value") + else -> null } + + fun fromJson(value: String): ProductQueryType = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductQueryType value: $value") + + fun fromJsonStrict(value: String): ProductQueryType = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductQueryType input value: $value") } fun toJson(): String = rawValue @@ -957,7 +1067,7 @@ public enum class ProductStatusAndroid(val rawValue: String) { Unknown("unknown"); companion object { - fun fromJson(value: String): ProductStatusAndroid = when (value) { + private fun fromKnownJson(value: String): ProductStatusAndroid? = when (value) { "ok" -> ProductStatusAndroid.Ok "OK" -> ProductStatusAndroid.Ok "Ok" -> ProductStatusAndroid.Ok @@ -970,8 +1080,13 @@ public enum class ProductStatusAndroid(val rawValue: String) { "unknown" -> ProductStatusAndroid.Unknown "UNKNOWN" -> ProductStatusAndroid.Unknown "Unknown" -> ProductStatusAndroid.Unknown - else -> ProductStatusAndroid.Unknown + else -> null } + + fun fromJson(value: String): ProductStatusAndroid = fromKnownJson(value) ?: ProductStatusAndroid.Unknown + + fun fromJsonStrict(value: String): ProductStatusAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductStatusAndroid input value: $value") } fun toJson(): String = rawValue @@ -982,15 +1097,20 @@ public enum class ProductType(val rawValue: String) { Subs("subs"); companion object { - fun fromJson(value: String): ProductType = when (value) { + private fun fromKnownJson(value: String): ProductType? = when (value) { "in-app" -> ProductType.InApp "IN_APP" -> ProductType.InApp "InApp" -> ProductType.InApp "subs" -> ProductType.Subs "SUBS" -> ProductType.Subs "Subs" -> ProductType.Subs - else -> throw IllegalArgumentException("Unknown ProductType value: $value") + else -> null } + + fun fromJson(value: String): ProductType = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductType value: $value") + + fun fromJsonStrict(value: String): ProductType = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductType input value: $value") } fun toJson(): String = rawValue @@ -1011,7 +1131,7 @@ public enum class ProductTypeIOS(val rawValue: String) { SubscriptionSuite("subscription-suite"); companion object { - fun fromJson(value: String): ProductTypeIOS = when (value) { + private fun fromKnownJson(value: String): ProductTypeIOS? = when (value) { "consumable" -> ProductTypeIOS.Consumable "CONSUMABLE" -> ProductTypeIOS.Consumable "Consumable" -> ProductTypeIOS.Consumable @@ -1030,8 +1150,13 @@ public enum class ProductTypeIOS(val rawValue: String) { "subscription-suite" -> ProductTypeIOS.SubscriptionSuite "SUBSCRIPTION_SUITE" -> ProductTypeIOS.SubscriptionSuite "SubscriptionSuite" -> ProductTypeIOS.SubscriptionSuite - else -> throw IllegalArgumentException("Unknown ProductTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): ProductTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductTypeIOS value: $value") + + fun fromJsonStrict(value: String): ProductTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -1043,7 +1168,7 @@ public enum class PurchaseState(val rawValue: String) { Unknown("unknown"); companion object { - fun fromJson(value: String): PurchaseState = when (value) { + private fun fromKnownJson(value: String): PurchaseState? = when (value) { "pending" -> PurchaseState.Pending "PENDING" -> PurchaseState.Pending "Pending" -> PurchaseState.Pending @@ -1053,8 +1178,13 @@ public enum class PurchaseState(val rawValue: String) { "unknown" -> PurchaseState.Unknown "UNKNOWN" -> PurchaseState.Unknown "Unknown" -> PurchaseState.Unknown - else -> PurchaseState.Unknown + else -> null } + + fun fromJson(value: String): PurchaseState = fromKnownJson(value) ?: PurchaseState.Unknown + + fun fromJsonStrict(value: String): PurchaseState = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PurchaseState input value: $value") } fun toJson(): String = rawValue @@ -1064,12 +1194,17 @@ public enum class PurchaseVerificationProvider(val rawValue: String) { Iapkit("iapkit"); companion object { - fun fromJson(value: String): PurchaseVerificationProvider = when (value) { + private fun fromKnownJson(value: String): PurchaseVerificationProvider? = when (value) { "iapkit" -> PurchaseVerificationProvider.Iapkit "IAPKIT" -> PurchaseVerificationProvider.Iapkit "Iapkit" -> PurchaseVerificationProvider.Iapkit - else -> throw IllegalArgumentException("Unknown PurchaseVerificationProvider value: $value") + else -> null } + + fun fromJson(value: String): PurchaseVerificationProvider = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PurchaseVerificationProvider value: $value") + + fun fromJsonStrict(value: String): PurchaseVerificationProvider = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PurchaseVerificationProvider input value: $value") } fun toJson(): String = rawValue @@ -1094,7 +1229,7 @@ public enum class SubResponseCodeAndroid(val rawValue: String) { UserIneligible("user-ineligible"); companion object { - fun fromJson(value: String): SubResponseCodeAndroid = when (value) { + private fun fromKnownJson(value: String): SubResponseCodeAndroid? = when (value) { "no-applicable-sub-response-code" -> SubResponseCodeAndroid.NoApplicableSubResponseCode "NO_APPLICABLE_SUB_RESPONSE_CODE" -> SubResponseCodeAndroid.NoApplicableSubResponseCode "NoApplicableSubResponseCode" -> SubResponseCodeAndroid.NoApplicableSubResponseCode @@ -1104,8 +1239,13 @@ public enum class SubResponseCodeAndroid(val rawValue: String) { "user-ineligible" -> SubResponseCodeAndroid.UserIneligible "USER_INELIGIBLE" -> SubResponseCodeAndroid.UserIneligible "UserIneligible" -> SubResponseCodeAndroid.UserIneligible - else -> throw IllegalArgumentException("Unknown SubResponseCodeAndroid value: $value") + else -> null } + + fun fromJson(value: String): SubResponseCodeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubResponseCodeAndroid value: $value") + + fun fromJsonStrict(value: String): SubResponseCodeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubResponseCodeAndroid input value: $value") } fun toJson(): String = rawValue @@ -1126,7 +1266,7 @@ public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) { UpFront("up-front"); companion object { - fun fromJson(value: String): SubscriptionBillingPlanTypeIOS = when (value) { + private fun fromKnownJson(value: String): SubscriptionBillingPlanTypeIOS? = when (value) { "unknown" -> SubscriptionBillingPlanTypeIOS.Unknown "UNKNOWN" -> SubscriptionBillingPlanTypeIOS.Unknown "Unknown" -> SubscriptionBillingPlanTypeIOS.Unknown @@ -1136,8 +1276,13 @@ public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) { "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront "UP_FRONT" -> SubscriptionBillingPlanTypeIOS.UpFront "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront - else -> SubscriptionBillingPlanTypeIOS.Unknown + else -> null } + + fun fromJson(value: String): SubscriptionBillingPlanTypeIOS = fromKnownJson(value) ?: SubscriptionBillingPlanTypeIOS.Unknown + + fun fromJsonStrict(value: String): SubscriptionBillingPlanTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -1153,7 +1298,7 @@ public enum class SubscriptionOfferTypeIOS(val rawValue: String) { WinBack("win-back"); companion object { - fun fromJson(value: String): SubscriptionOfferTypeIOS = when (value) { + private fun fromKnownJson(value: String): SubscriptionOfferTypeIOS? = when (value) { "introductory" -> SubscriptionOfferTypeIOS.Introductory "INTRODUCTORY" -> SubscriptionOfferTypeIOS.Introductory "Introductory" -> SubscriptionOfferTypeIOS.Introductory @@ -1163,8 +1308,13 @@ public enum class SubscriptionOfferTypeIOS(val rawValue: String) { "win-back" -> SubscriptionOfferTypeIOS.WinBack "WIN_BACK" -> SubscriptionOfferTypeIOS.WinBack "WinBack" -> SubscriptionOfferTypeIOS.WinBack - else -> throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): SubscriptionOfferTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS value: $value") + + fun fromJsonStrict(value: String): SubscriptionOfferTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -1178,7 +1328,7 @@ public enum class SubscriptionPeriodIOS(val rawValue: String) { Empty("empty"); companion object { - fun fromJson(value: String): SubscriptionPeriodIOS = when (value) { + private fun fromKnownJson(value: String): SubscriptionPeriodIOS? = when (value) { "day" -> SubscriptionPeriodIOS.Day "DAY" -> SubscriptionPeriodIOS.Day "Day" -> SubscriptionPeriodIOS.Day @@ -1194,8 +1344,13 @@ public enum class SubscriptionPeriodIOS(val rawValue: String) { "empty" -> SubscriptionPeriodIOS.Empty "EMPTY" -> SubscriptionPeriodIOS.Empty "Empty" -> SubscriptionPeriodIOS.Empty - else -> throw IllegalArgumentException("Unknown SubscriptionPeriodIOS value: $value") + else -> null } + + fun fromJson(value: String): SubscriptionPeriodIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionPeriodIOS value: $value") + + fun fromJsonStrict(value: String): SubscriptionPeriodIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionPeriodIOS input value: $value") } fun toJson(): String = rawValue @@ -1212,7 +1367,7 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { Unknown("unknown"); companion object { - fun fromJson(value: String): SubscriptionPeriodUnit = when (value) { + private fun fromKnownJson(value: String): SubscriptionPeriodUnit? = when (value) { "day" -> SubscriptionPeriodUnit.Day "DAY" -> SubscriptionPeriodUnit.Day "Day" -> SubscriptionPeriodUnit.Day @@ -1228,8 +1383,13 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { "unknown" -> SubscriptionPeriodUnit.Unknown "UNKNOWN" -> SubscriptionPeriodUnit.Unknown "Unknown" -> SubscriptionPeriodUnit.Unknown - else -> SubscriptionPeriodUnit.Unknown + else -> null } + + fun fromJson(value: String): SubscriptionPeriodUnit = fromKnownJson(value) ?: SubscriptionPeriodUnit.Unknown + + fun fromJsonStrict(value: String): SubscriptionPeriodUnit = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionPeriodUnit input value: $value") } fun toJson(): String = rawValue @@ -1271,7 +1431,7 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { KeepExisting("keep-existing"); companion object { - fun fromJson(value: String): SubscriptionReplacementModeAndroid = when (value) { + private fun fromKnownJson(value: String): SubscriptionReplacementModeAndroid? = when (value) { "unknown-replacement-mode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode "UNKNOWN_REPLACEMENT_MODE" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode "UnknownReplacementMode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode @@ -1293,8 +1453,13 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { "keep-existing" -> SubscriptionReplacementModeAndroid.KeepExisting "KEEP_EXISTING" -> SubscriptionReplacementModeAndroid.KeepExisting "KeepExisting" -> SubscriptionReplacementModeAndroid.KeepExisting - else -> SubscriptionReplacementModeAndroid.UnknownReplacementMode + else -> null } + + fun fromJson(value: String): SubscriptionReplacementModeAndroid = fromKnownJson(value) ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode + + fun fromJsonStrict(value: String): SubscriptionReplacementModeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid input value: $value") } fun toJson(): String = rawValue @@ -4222,7 +4387,7 @@ public data class BillingProgramInformationDialogParamsAndroid( ) { companion object { fun fromJson(json: Map): BillingProgramInformationDialogParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.BillingChoice + val billingProgram = if (json["billingProgram"] == null) BillingProgramAndroid.BillingChoice else (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String if (externalTransactionToken == null) return null return BillingProgramInformationDialogParamsAndroid( @@ -4251,8 +4416,8 @@ public data class DeepLinkOptions( companion object { fun fromJson(json: Map): DeepLinkOptions { return DeepLinkOptions( - packageNameAndroid = json["packageNameAndroid"] as? String, - skuAndroid = json["skuAndroid"] as? String, + packageNameAndroid = json["packageNameAndroid"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, + skuAndroid = json["skuAndroid"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, ) } } @@ -4293,10 +4458,10 @@ public data class DeveloperBillingOptionParamsAndroid( ) { companion object { fun fromJson(json: Map): DeveloperBillingOptionParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") - val externalTransactionToken = json["externalTransactionToken"] as? String - val launchMode = (json["launchMode"] as? String)?.let { DeveloperBillingLaunchModeAndroid.fromJson(it) } - val linkUri = json["linkUri"] as? String + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") + val externalTransactionToken = json["externalTransactionToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val launchMode = json["launchMode"]?.let { raw -> (raw as? String)?.let { DeveloperBillingLaunchModeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for DeveloperBillingLaunchModeAndroid") } + val linkUri = json["linkUri"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } return DeveloperBillingOptionParamsAndroid( billingProgram = billingProgram, externalTransactionToken = externalTransactionToken, @@ -4384,9 +4549,9 @@ public data class GetBillingChoiceInfoParamsAndroid( companion object { fun fromJson(json: Map): GetBillingChoiceInfoParamsAndroid { return GetBillingChoiceInfoParamsAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.BillingChoice, - playBillingChoiceImageLayout = (json["playBillingChoiceImageLayout"] as? String)?.let { BillingChoiceImageLayoutAndroid.fromJson(it) } ?: BillingChoiceImageLayoutAndroid.RectangularFourByOne, - userLocale = json["userLocale"] as? String, + billingProgram = if (json["billingProgram"] == null) BillingProgramAndroid.BillingChoice else (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid"), + playBillingChoiceImageLayout = if (json["playBillingChoiceImageLayout"] == null) BillingChoiceImageLayoutAndroid.RectangularFourByOne else (json["playBillingChoiceImageLayout"] as? String)?.let { BillingChoiceImageLayoutAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingChoiceImageLayoutAndroid"), + userLocale = json["userLocale"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, ) } } @@ -4412,7 +4577,7 @@ public data class InAppMessageParamsAndroid( companion object { fun fromJson(json: Map): InAppMessageParamsAndroid { return InAppMessageParamsAndroid( - categories = (json["categories"] as? List<*>)?.mapNotNull { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), + categories = if (json["categories"] == null) listOf(InAppMessageCategoryAndroid.Transactional) else (json["categories"] as? List<*>)?.map { (it as? String)?.let { InAppMessageCategoryAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for InAppMessageCategoryAndroid") } ?: throw IllegalArgumentException("Invalid input list"), ) } } @@ -4451,8 +4616,8 @@ public data class InitConnectionConfig( companion object { fun fromJson(json: Map): InitConnectionConfig { return InitConnectionConfig( - billingChoiceScreenTypeAndroid = (json["billingChoiceScreenTypeAndroid"] as? String)?.let { BillingChoiceScreenTypeAndroid.fromJson(it) } ?: BillingChoiceScreenTypeAndroid.GoogleRendered, - enableBillingProgramAndroid = (json["enableBillingProgramAndroid"] as? String)?.let { BillingProgramAndroid.fromJson(it) }, + billingChoiceScreenTypeAndroid = if (json["billingChoiceScreenTypeAndroid"] == null) BillingChoiceScreenTypeAndroid.GoogleRendered else (json["billingChoiceScreenTypeAndroid"] as? String)?.let { BillingChoiceScreenTypeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingChoiceScreenTypeAndroid"), + enableBillingProgramAndroid = json["enableBillingProgramAndroid"]?.let { raw -> (raw as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") }, ) } } @@ -4495,10 +4660,10 @@ public data class LaunchExternalLinkParamsAndroid( ) { companion object { fun fromJson(json: Map): LaunchExternalLinkParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") - val externalTransactionToken = json["externalTransactionToken"] as? String - val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkLaunchModeAndroid") - val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkTypeAndroid") + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") + val externalTransactionToken = json["externalTransactionToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for ExternalLinkLaunchModeAndroid") + val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for ExternalLinkTypeAndroid") val linkUri = json["linkUri"] as? String if (linkUri == null) return null return LaunchExternalLinkParamsAndroid( @@ -4526,9 +4691,8 @@ public data class ProductRequest( ) { companion object { fun fromJson(json: Map): ProductRequest? { - val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } - val type = (json["type"] as? String)?.let { ProductQueryType.fromJson(it) } ?: ProductQueryType.InApp - if (skus == null) return null + val skus = (json["skus"] as? List<*>)?.map { it as? String ?: throw IllegalArgumentException("Invalid String input list element") } ?: throw IllegalArgumentException("Invalid input list") + val type = if (json["type"] == null) ProductQueryType.InApp else (json["type"] as? String)?.let { ProductQueryType.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for ProductQueryType") return ProductRequest( skus = skus, type = type, @@ -4600,9 +4764,9 @@ public data class PurchaseOptions( companion object { fun fromJson(json: Map): PurchaseOptions { return PurchaseOptions( - alsoPublishToEventListenerIOS = json["alsoPublishToEventListenerIOS"] as? Boolean, - includeSuspendedAndroid = json["includeSuspendedAndroid"] as? Boolean, - onlyIncludeActiveItemsIOS = json["onlyIncludeActiveItemsIOS"] as? Boolean, + alsoPublishToEventListenerIOS = json["alsoPublishToEventListenerIOS"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, + includeSuspendedAndroid = json["includeSuspendedAndroid"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, + onlyIncludeActiveItemsIOS = json["onlyIncludeActiveItemsIOS"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, ) } } @@ -4625,7 +4789,7 @@ public data class PurchaseUpdatedListenerOptions( companion object { fun fromJson(json: Map): PurchaseUpdatedListenerOptions { return PurchaseUpdatedListenerOptions( - dedupeTransactionIOS = json["dedupeTransactionIOS"] as? Boolean, + dedupeTransactionIOS = json["dedupeTransactionIOS"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, ) } } @@ -4669,12 +4833,11 @@ public data class RequestPurchaseAndroidProps( companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } - val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean - val obfuscatedAccountId = json["obfuscatedAccountId"] as? String - val obfuscatedProfileId = json["obfuscatedProfileId"] as? String - val offerToken = json["offerToken"] as? String - val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } - if (skus == null) return null + val isOfferPersonalized = json["isOfferPersonalized"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val obfuscatedAccountId = json["obfuscatedAccountId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val obfuscatedProfileId = json["obfuscatedProfileId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val offerToken = json["offerToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val skus = (json["skus"] as? List<*>)?.map { it as? String ?: throw IllegalArgumentException("Invalid String input list element") } ?: throw IllegalArgumentException("Invalid input list") return RequestPurchaseAndroidProps( developerBillingOption = developerBillingOption, isOfferPersonalized = isOfferPersonalized, @@ -4728,10 +4891,10 @@ public data class RequestPurchaseIosProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseIosProps? { - val advancedCommerceData = json["advancedCommerceData"] as? String - val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean - val appAccountToken = json["appAccountToken"] as? String - val quantity = (json["quantity"] as? Number)?.toInt() + val advancedCommerceData = json["advancedCommerceData"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val appAccountToken = json["appAccountToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val quantity = json["quantity"]?.let { raw -> ((raw as? Number)?.let { number -> number.toLong().takeIf { value -> value >= Int.MIN_VALUE.toLong() && value <= Int.MAX_VALUE.toLong() && value.toDouble() == number.toDouble() }?.toInt() }) ?: throw IllegalArgumentException("Invalid Int input value") } val sku = json["sku"] as? String val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null @@ -4901,15 +5064,14 @@ public data class RequestSubscriptionAndroidProps( companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } - val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean - val obfuscatedAccountId = json["obfuscatedAccountId"] as? String - val obfuscatedProfileId = json["obfuscatedProfileId"] as? String - val originalExternalTransactionId = json["originalExternalTransactionId"] as? String - val purchaseToken = json["purchaseToken"] as? String - val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } - val subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } } + val isOfferPersonalized = json["isOfferPersonalized"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val obfuscatedAccountId = json["obfuscatedAccountId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val obfuscatedProfileId = json["obfuscatedProfileId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val originalExternalTransactionId = json["originalExternalTransactionId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val purchaseToken = json["purchaseToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val skus = (json["skus"] as? List<*>)?.map { it as? String ?: throw IllegalArgumentException("Invalid String input list element") } ?: throw IllegalArgumentException("Invalid input list") + val subscriptionOffers = json["subscriptionOffers"]?.let { raw -> (raw as? List<*>)?.map { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for AndroidSubscriptionOfferInput") } ?: throw IllegalArgumentException("Invalid input list") } val subscriptionProductReplacementParams = json["subscriptionProductReplacementParams"]?.let { value -> (value as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for SubscriptionProductReplacementParamsAndroid") } - if (skus == null) return null return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, isOfferPersonalized = isOfferPersonalized, @@ -4982,13 +5144,13 @@ public data class RequestSubscriptionIosProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionIosProps? { - val advancedCommerceData = json["advancedCommerceData"] as? String - val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean - val appAccountToken = json["appAccountToken"] as? String - val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } - val compactJWS = json["compactJWS"] as? String + val advancedCommerceData = json["advancedCommerceData"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val appAccountToken = json["appAccountToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val billingPlanType = json["billingPlanType"]?.let { raw -> (raw as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for SubscriptionBillingPlanTypeIOS") } + val compactJWS = json["compactJWS"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } val promotionalOfferJWS = json["promotionalOfferJWS"]?.let { value -> (value as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for PromotionalOfferJWSInputIOS") } - val quantity = (json["quantity"] as? Number)?.toInt() + val quantity = json["quantity"]?.let { raw -> ((raw as? Number)?.let { number -> number.toLong().takeIf { value -> value >= Int.MIN_VALUE.toLong() && value <= Int.MAX_VALUE.toLong() && value.toDouble() == number.toDouble() }?.toInt() }) ?: throw IllegalArgumentException("Invalid Int input value") } val sku = json["sku"] as? String val winBackOffer = json["winBackOffer"]?.let { value -> (value as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for WinBackOfferInputIOS") } val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } @@ -5094,9 +5256,9 @@ public data class RequestVerifyPurchaseWithIapkitAmazonProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitAmazonProps? { val receiptId = json["receiptId"] as? String - val sandbox = json["sandbox"] as? Boolean - val userId = json["userId"] as? String - val expectedProductId = json["expectedProductId"] as? String + val sandbox = json["sandbox"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val userId = json["userId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val expectedProductId = json["expectedProductId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } if (receiptId == null) return null return RequestVerifyPurchaseWithIapkitAmazonProps( receiptId = receiptId, @@ -5220,11 +5382,11 @@ public data class RequestVerifyPurchaseWithIapkitProps( fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, - apiKey = json["apiKey"] as? String, + apiKey = json["apiKey"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAppleProps") }, - baseUrl = json["baseUrl"] as? String, + baseUrl = json["baseUrl"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, google = json["google"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitGoogleProps") }, - includeClientPayload = json["includeClientPayload"] as? Boolean, + includeClientPayload = json["includeClientPayload"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, ) } } @@ -5257,7 +5419,7 @@ public data class SubscriptionProductReplacementParamsAndroid( companion object { fun fromJson(json: Map): SubscriptionProductReplacementParamsAndroid? { val oldProductId = json["oldProductId"] as? String - val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode + val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for SubscriptionReplacementModeAndroid") if (oldProductId == null) return null return SubscriptionProductReplacementParamsAndroid( oldProductId = oldProductId, @@ -5330,7 +5492,7 @@ public data class VerifyPurchaseGoogleOptions( companion object { fun fromJson(json: Map): VerifyPurchaseGoogleOptions? { val accessToken = json["accessToken"] as? String - val isSub = json["isSub"] as? Boolean + val isSub = json["isSub"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } val packageName = json["packageName"] as? String val purchaseToken = json["purchaseToken"] as? String val sku = json["sku"] as? String @@ -5442,7 +5604,7 @@ public data class VerifyPurchaseWithProviderProps( companion object { fun fromJson(json: Map): VerifyPurchaseWithProviderProps? { val iapkit = json["iapkit"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitProps") } - val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider") + val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for PurchaseVerificationProvider") return VerifyPurchaseWithProviderProps( iapkit = iapkit, provider = provider, diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt index 76cb1812a..ddb3e165e 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 @@ -123,6 +123,27 @@ internal fun recordRecoverableProductQueryFailure( return firstError ?: error } +internal fun hasInvalidPlaySubscriptionOffers( + requestedSkus: Collection, + offers: Collection, +): Boolean { + if (offers.isEmpty()) return false + val requested = requestedSkus.toSet() + val offered = offers.mapTo(mutableSetOf()) { it.sku } + return offers.any { it.offerToken.isBlank() || it.sku !in requested } || + !offered.containsAll(requested) +} + +internal fun isValidPlaySubscriptionOfferToken( + token: String?, + availableTokens: Collection, +): Boolean = !token.isNullOrEmpty() && token in availableTokens + +internal fun areValidPlaySubscriptionOfferTokens( + tokens: Collection, + availableTokens: Collection, +): Boolean = tokens.isNotEmpty() && tokens.all { it in availableTokens } + internal data class AllProductQueryResults( val inApp: T?, val subscriptions: T?, @@ -1595,6 +1616,18 @@ class OpenIapModule( return@withContext emptyList() } + if ( + androidArgs.type == ProductQueryType.Subs && + hasInvalidPlaySubscriptionOffers( + androidArgs.skus, + androidArgs.subscriptionOffers.orEmpty(), + ) + ) { + val err = OpenIapError.SkuOfferMismatchFailure() + errorEventGate.publishOnce(err) + return@withContext emptyList() + } + if (!isSubscriptionReplacementTargetCountValid( targetSkuCount = androidArgs.skus.size, hasProductLevelReplacementParams = @@ -1711,28 +1744,37 @@ class OpenIapModule( OpenIapLog.debug("Available offer base plans for ${productDetails.productId}: $availableOffers", TAG) val availableTokens = productDetails.subscriptionOfferDetails?.map { it.offerToken } ?: emptyList() - val fromQueue = requestedOffersBySku[productDetails.productId]?.let { queue -> - if (queue.isNotEmpty()) queue.removeAt(0) else null + val requestedTokens = requestedOffersBySku[productDetails.productId].orEmpty() + if (androidArgs.subscriptionOffers?.isNotEmpty() == true && + !areValidPlaySubscriptionOfferTokens(requestedTokens, availableTokens)) { + OpenIapLog.warn("Invalid offer token for ${productDetails.productId}", TAG) + val err = OpenIapError.SkuOfferMismatchFailure() + finishWithError(err) + return + } + val resolved = if (androidArgs.subscriptionOffers.isNullOrEmpty()) { + productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken + } else { + requestedTokens.firstOrNull() } - val fromIndex = androidArgs.subscriptionOffers?.getOrNull(index)?.takeIf { it.sku == productDetails.productId }?.offerToken - val resolved = fromQueue ?: fromIndex ?: productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken OpenIapLog.debug( "Resolved offer token for ${productDetails.productId}: present=${!resolved.isNullOrEmpty()}", TAG, ) - if (resolved.isNullOrEmpty() || (availableTokens.isNotEmpty() && !availableTokens.contains(resolved))) { + if (!isValidPlaySubscriptionOfferToken(resolved, availableTokens)) { OpenIapLog.warn("Invalid offer token for ${productDetails.productId}", TAG) val err = OpenIapError.SkuOfferMismatchFailure() finishWithError(err) return } + val selectedOfferToken = checkNotNull(resolved) - builder.setOfferToken(resolved) + builder.setOfferToken(selectedOfferToken) selectedBasePlanIdsBySku[productDetails.productId] = productDetails.subscriptionOfferDetails - ?.firstOrNull { it.offerToken == resolved } + ?.firstOrNull { it.offerToken == selectedOfferToken } ?.basePlanId // Apply per-product subscription replacement params (8.1.0+) diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingChoiceAndroidTypesTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingChoiceAndroidTypesTest.kt index 0dc5e9c4f..beb120ac8 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingChoiceAndroidTypesTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingChoiceAndroidTypesTest.kt @@ -6,6 +6,7 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Assert.assertThrows import org.junit.Test class BillingChoiceAndroidTypesTest { @@ -152,6 +153,24 @@ class BillingChoiceAndroidTypesTest { ) } + @Test + fun `in-app message input rejects unknown and malformed categories`() { + val known = InAppMessageParamsAndroid.fromJson( + mapOf("categories" to listOf("unknown-in-app-message-category-id")) + ) + assertEquals(InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId, known.categories?.single()) + + listOf( + mapOf("categories" to listOf("future-category")), + mapOf("categories" to listOf(999)), + mapOf("categories" to "transactional"), + ).forEach { input -> + assertThrows(IllegalArgumentException::class.java) { + InAppMessageParamsAndroid.fromJson(input) + } + } + } + @Test fun `developer billing option supports in-app and external-link Billing Choice`() { val inApp = DeveloperBillingOptionParamsAndroid.fromJson( @@ -177,6 +196,29 @@ class BillingChoiceAndroidTypesTest { ) } + @Test + fun `subscription purchase input rejects malformed explicit list entries`() { + val invalidInputs = listOf( + mapOf("skus" to listOf("monthly", 7)), + mapOf("skus" to listOf("monthly"), "obfuscatedAccountId" to 7), + mapOf( + "skus" to listOf("monthly"), + "subscriptionOffers" to listOf(mapOf("offerToken" to "token")), + ), + mapOf( + "skus" to listOf("monthly"), + "subscriptionOffers" to listOf(mapOf("sku" to "monthly")), + ), + mapOf("skus" to listOf("monthly"), "subscriptionOffers" to listOf(7)), + ) + + invalidInputs.forEach { input -> + assertThrows(IllegalArgumentException::class.java) { + RequestSubscriptionAndroidProps.fromJson(input) + } + } + } + @Test fun `developer provided billing details preserve nullable fields and products`() { val details = DeveloperProvidedBillingDetailsAndroid.fromJson( diff --git a/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt b/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt index 170d55a58..779d17a9c 100644 --- a/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt +++ b/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionReplacementModeTest.kt @@ -120,14 +120,34 @@ class SubscriptionReplacementModeTest { ) for (input in inputs) { - val params = requireNotNull(SubscriptionProductReplacementParamsAndroid.fromJson(input)) - assertEquals(SubscriptionReplacementModeAndroid.UnknownReplacementMode, params.replacementMode) assertThrows(IllegalArgumentException::class.java) { - params.toBillingSubscriptionProductReplacementParams() + SubscriptionProductReplacementParamsAndroid.fromJson(input) } } } + @Test + fun `explicit subscription offers cover only requested SKUs`() { + val monthly = AndroidSubscriptionOfferInput("monthly-token", "monthly") + val yearly = AndroidSubscriptionOfferInput("yearly-token", "yearly") + + assertEquals(false, hasInvalidPlaySubscriptionOffers(listOf("monthly"), emptyList())) + assertEquals(false, hasInvalidPlaySubscriptionOffers(listOf("monthly", "yearly"), listOf(monthly, yearly))) + assertEquals(true, hasInvalidPlaySubscriptionOffers(listOf("monthly"), listOf(yearly))) + assertEquals(false, hasInvalidPlaySubscriptionOffers(listOf("monthly"), listOf(monthly, monthly))) + assertEquals(true, hasInvalidPlaySubscriptionOffers(listOf("monthly", "yearly"), listOf(monthly))) + } + + @Test + fun `explicit subscription offer tokens must exist in store metadata`() { + assertEquals(false, isValidPlaySubscriptionOfferToken("token", emptyList())) + assertEquals(false, isValidPlaySubscriptionOfferToken("future", listOf("known"))) + assertEquals(true, isValidPlaySubscriptionOfferToken("known", listOf("known"))) + assertEquals(false, areValidPlaySubscriptionOfferTokens(listOf("known", "future"), listOf("known"))) + assertEquals(false, areValidPlaySubscriptionOfferTokens(listOf("future", "known"), listOf("known"))) + assertEquals(true, areValidPlaySubscriptionOfferTokens(listOf("known", "known"), listOf("known"))) + } + @Test fun `SubscriptionReplacementModeAndroid fromJson parses correctly`() { assertEquals(SubscriptionReplacementModeAndroid.UnknownReplacementMode, SubscriptionReplacementModeAndroid.fromJson("unknown-replacement-mode")) diff --git a/packages/gql/codegen/fixtures/nullable-enum-list.ts b/packages/gql/codegen/fixtures/nullable-enum-list.ts new file mode 100644 index 000000000..2b02bf857 --- /dev/null +++ b/packages/gql/codegen/fixtures/nullable-enum-list.ts @@ -0,0 +1,89 @@ +import type { IRSchema } from "../core/types.js"; + +export function nullableEnumListSchema(): IRSchema { + return { + enums: [ + { + name: "TestStatus", + values: [ + { name: "Unknown", rawValue: "unknown", legacyAliases: [] }, + { name: "Active", rawValue: "active", legacyAliases: [] }, + ], + isErrorCode: false, + }, + { + name: "StrictStatus", + values: [{ name: "Active", rawValue: "active", legacyAliases: [] }], + isErrorCode: false, + }, + ], + interfaces: [], + objects: [ + { + name: "EnumListHolder", + fields: [ + { + name: "statuses", + description: + "Status values from the schema.\n\nPreserves every documentation line.\n@see https://openiap.dev/docs/types", + type: { + kind: "list", + nullable: false, + elementType: { + kind: "enum", + name: "TestStatus", + nullable: false, + }, + }, + isOverride: false, + }, + { + name: "strictStatuses", + type: { + kind: "list", + nullable: false, + elementType: { + kind: "enum", + name: "StrictStatus", + nullable: false, + }, + }, + isOverride: false, + }, + { + name: "nullableStrictStatuses", + type: { + kind: "list", + nullable: false, + elementType: { + kind: "enum", + name: "StrictStatus", + nullable: true, + }, + }, + isOverride: false, + }, + { + name: "nullableLabels", + type: { + kind: "list", + nullable: false, + elementType: { + kind: "scalar", + name: "String", + nullable: true, + }, + }, + isOverride: false, + }, + ], + interfaces: [], + unions: [], + isResultUnion: false, + }, + ], + inputs: [], + unions: [], + operations: [], + }; +} diff --git a/packages/gql/codegen/plugins/csharp.ts b/packages/gql/codegen/plugins/csharp.ts index 3ceb5442f..bef21fef3 100644 --- a/packages/gql/codegen/plugins/csharp.ts +++ b/packages/gql/codegen/plugins/csharp.ts @@ -247,6 +247,116 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(''); this.emit(`namespace ${NAMESPACE};`); this.emit(''); + this.emit('public interface IOpenIapEnumJsonConverter where TEnum : struct, Enum'); + this.emit('{'); + this.emit(' bool TryReadRaw(string value, out TEnum result);'); + this.emit(' string WriteRaw(TEnum value);'); + this.emit('}'); + this.emit(''); + this.emit('public sealed class StrictEnumJsonConverter : JsonConverter'); + this.emit(' where TEnum : struct, Enum'); + this.emit(' where TConverter : IOpenIapEnumJsonConverter, new()'); + this.emit('{'); + this.emit(' private static readonly TConverter Converter = new();'); + this.emit(''); + this.emit(' public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)'); + this.emit(' {'); + this.emit(' var raw = reader.GetString();'); + this.emit(' if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value;'); + this.emit(' throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}");'); + this.emit(' }'); + this.emit(''); + this.emit(' public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) =>'); + this.emit(' writer.WriteStringValue(Converter.WriteRaw(value));'); + this.emit('}'); + this.emit(''); + this.emit('public sealed class StrictNullableEnumJsonConverter : JsonConverter'); + this.emit(' where TEnum : struct, Enum'); + this.emit(' where TConverter : IOpenIapEnumJsonConverter, new()'); + this.emit('{'); + this.emit(' private static readonly TConverter Converter = new();'); + this.emit(''); + this.emit(' public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)'); + this.emit(' {'); + this.emit(' var raw = reader.GetString();'); + this.emit(' if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value;'); + this.emit(' throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}");'); + this.emit(' }'); + this.emit(''); + this.emit(' public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options)'); + this.emit(' {'); + this.emit(' if (value is null) writer.WriteNullValue();'); + this.emit(' else writer.WriteStringValue(Converter.WriteRaw(value.Value));'); + this.emit(' }'); + this.emit('}'); + this.emit(''); + this.emit('public sealed class StrictEnumListJsonConverter : JsonConverter>'); + this.emit(' where TEnum : struct, Enum'); + this.emit(' where TConverter : IOpenIapEnumJsonConverter, new()'); + this.emit('{'); + this.emit(' private static readonly TConverter Converter = new();'); + this.emit(''); + this.emit(' public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)'); + this.emit(' {'); + this.emit(' using var document = JsonDocument.ParseValue(ref reader);'); + this.emit(' if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("Expected an enum input array.");'); + this.emit(' var values = new List();'); + this.emit(' foreach (var element in document.RootElement.EnumerateArray())'); + this.emit(' {'); + this.emit(' var raw = element.ValueKind == JsonValueKind.String ? element.GetString() : null;'); + this.emit(' if (raw is null || !Converter.TryReadRaw(raw, out var value))'); + this.emit(' throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}");'); + this.emit(' values.Add(value);'); + this.emit(' }'); + this.emit(' return values;'); + this.emit(' }'); + this.emit(''); + this.emit(' public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options)'); + this.emit(' {'); + this.emit(' writer.WriteStartArray();'); + this.emit(' foreach (var item in value) writer.WriteStringValue(Converter.WriteRaw(item));'); + this.emit(' writer.WriteEndArray();'); + this.emit(' }'); + this.emit('}'); + this.emit(''); + this.emit('public sealed class StrictNullableEnumListJsonConverter : JsonConverter>'); + this.emit(' where TEnum : struct, Enum'); + this.emit(' where TConverter : IOpenIapEnumJsonConverter, new()'); + this.emit('{'); + this.emit(' private static readonly TConverter Converter = new();'); + this.emit(''); + this.emit(' public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)'); + this.emit(' {'); + this.emit(' using var document = JsonDocument.ParseValue(ref reader);'); + this.emit(' if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("Expected an enum input array.");'); + this.emit(' var values = new List();'); + this.emit(' foreach (var element in document.RootElement.EnumerateArray())'); + this.emit(' {'); + this.emit(' if (element.ValueKind == JsonValueKind.Null)'); + this.emit(' {'); + this.emit(' values.Add(null);'); + this.emit(' continue;'); + this.emit(' }'); + this.emit(' var raw = element.ValueKind == JsonValueKind.String ? element.GetString() : null;'); + this.emit(' if (raw is null || !Converter.TryReadRaw(raw, out var value))'); + this.emit(' throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}");'); + this.emit(' values.Add(value);'); + this.emit(' }'); + this.emit(' return values;'); + this.emit(' }'); + this.emit(''); + this.emit(' public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options)'); + this.emit(' {'); + this.emit(' writer.WriteStartArray();'); + this.emit(' foreach (var item in value)'); + this.emit(' {'); + this.emit(' if (item is null) writer.WriteNullValue();'); + this.emit(' else writer.WriteStringValue(Converter.WriteRaw(item.Value));'); + this.emit(' }'); + this.emit(' writer.WriteEndArray();'); + this.emit(' }'); + this.emit('}'); + this.emit(''); } private addSection(title: string): void { @@ -294,7 +404,7 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(''); // Generate the per-enum JsonConverter that maps enum <-> raw string. - this.emit(`public sealed class ${converterName} : JsonConverter<${irEnum.name}>`); + this.emit(`public sealed class ${converterName} : JsonConverter<${irEnum.name}>, IOpenIapEnumJsonConverter<${irEnum.name}>`); this.emit('{'); this.emit(` private static readonly Dictionary _fromString = new()`); this.emit(' {'); @@ -338,6 +448,11 @@ export class CSharpPlugin extends CodegenPlugin { ? `${irEnum.name}.${this.enumValueCase(unknownValue.name)}` : `throw new ArgumentException($"Unknown ${irEnum.name} value: {value}")`; this.emit(` _fromString.TryGetValue(value, out var v) ? v : ${fromRawFallback};`); + this.emit(''); + this.emit(` public bool TryReadRaw(string value, out ${irEnum.name} result) =>`); + this.emit(' _fromString.TryGetValue(value, out result);'); + this.emit(''); + this.emit(` public string WriteRaw(${irEnum.name} value) => _toString[value];`); this.emit('}'); this.emit(''); @@ -509,7 +624,7 @@ export class CSharpPlugin extends CodegenPlugin { return names; } - private emitProperties(fields: IRField[], inheritedFields = new Set()): void { + private emitProperties(fields: IRField[], inheritedFields = new Set(), isInputContext = false): void { fields.forEach((field) => { this.emitDoc(field.description, ' '); this.emitDeprecation(field.description, ' '); @@ -519,7 +634,18 @@ export class CSharpPlugin extends CodegenPlugin { const jsonName = field.name; const overrideModifier = inheritedFields.has(field.name) ? 'override ' : ''; this.emit(` [JsonPropertyName("${jsonName}")]`); - if ( + if (isInputContext && field.type.kind === 'enum') { + const converter = field.type.nullable ? 'StrictNullableEnumJsonConverter' : 'StrictEnumJsonConverter'; + this.emit( + ` [JsonConverter(typeof(${converter}<${field.type.name}, ${field.type.name}JsonConverter>))]`, + ); + } else if (isInputContext && field.type.kind === 'list' && field.type.elementType?.kind === 'enum') { + const converter = field.type.elementType.nullable + ? 'StrictNullableEnumListJsonConverter' + : 'StrictEnumListJsonConverter'; + const enumName = field.type.elementType.name; + this.emit(` [JsonConverter(typeof(${converter}<${enumName}, ${enumName}JsonConverter>))]`); + } else if ( field.type.nullable && ['object', 'input'].includes(field.type.kind) && this.typeNeedsTolerantNullableDecoder(field.type.name!, this.schema) @@ -636,7 +762,7 @@ export class CSharpPlugin extends CodegenPlugin { this.emitDoc(irInput.description); this.emit(`public sealed record ${irInput.name}`); this.emit('{'); - this.emitProperties(irInput.fields); + this.emitProperties(irInput.fields, new Set(), true); this.emit('}'); this.emit(''); this.emitNullableJsonConverter(irInput.name); @@ -757,6 +883,7 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(''); this.emitDoc(type.description, ' '); this.emit(' [JsonPropertyName("type")]'); + this.emit(' [JsonConverter(typeof(StrictEnumJsonConverter))]'); this.emit(' public required ProductQueryType Type { get; init; }'); this.emit(''); this.emit(' public void Validate()'); diff --git a/packages/gql/codegen/plugins/dart.ts b/packages/gql/codegen/plugins/dart.ts index 5b03564ef..ab0e62493 100644 --- a/packages/gql/codegen/plugins/dart.ts +++ b/packages/gql/codegen/plugins/dart.ts @@ -173,7 +173,7 @@ export class DartPlugin extends CodegenPlugin { this.emit(` const ${irEnum.name}(this.value);`); this.emit(' final String value;'); this.emit(''); - this.emit(` factory ${irEnum.name}.fromJson(String value) {`); + this.emit(` static ${irEnum.name}? _fromKnownJson(String value) {`); this.emit(" final normalized = value.toLowerCase().replaceAll('_', '-');"); this.emit(' switch (normalized) {'); @@ -184,6 +184,12 @@ export class DartPlugin extends CodegenPlugin { } this.emit(' }'); + this.emit(' return null;'); + this.emit(' }'); + this.emit(''); + this.emit(` factory ${irEnum.name}.fromJson(String value) {`); + this.emit(' final known = _fromKnownJson(value);'); + this.emit(' if (known != null) return known;'); const unknownValue = this.enumUnknownValue(irEnum); if (unknownValue) { this.emit(` return ${irEnum.name}.${this.escapeKeyword(this.enumValueCase(unknownValue.name))};`); @@ -192,6 +198,12 @@ export class DartPlugin extends CodegenPlugin { } this.emit(' }'); this.emit(''); + this.emit(` factory ${irEnum.name}.fromJsonStrict(String value) {`); + this.emit(' final known = _fromKnownJson(value);'); + this.emit(' if (known != null) return known;'); + this.emit(` throw ArgumentError('Unknown ${irEnum.name} input value: \$value');`); + this.emit(' }'); + this.emit(''); this.emit(' String toJson() => value;'); this.emit('}'); this.emit(''); @@ -399,7 +411,12 @@ export class DartPlugin extends CodegenPlugin { this.emit(` factory ${irInput.name}.fromJson(Map json) {`); this.emit(` return ${irInput.name}(`); for (const field of sortedFields) { - const jsonExpr = this.buildFromJsonExpression(field.type, `json['${field.name}']`, this.buildDefaultValueExpression(field)); + const jsonExpr = this.buildFromJsonExpression( + field.type, + `json['${field.name}']`, + this.buildDefaultValueExpression(field), + true, + ); this.emit(` ${this.escapeKeyword(field.name)}: ${jsonExpr},`); } this.emit(' );'); @@ -779,10 +796,15 @@ export class DartPlugin extends CodegenPlugin { return this.getPropertyType(field.resolvedReturnType); } - private buildFromJsonExpression(type: IRType, sourceExpr: string, defaultExpression?: string | null): string { + private buildFromJsonExpression( + type: IRType, + sourceExpr: string, + defaultExpression?: string | null, + isInputContext: boolean = false, + ): string { if (type.kind === 'list') { const listCast = `(${sourceExpr} as List${type.nullable ? '?' : ''})`; - const elementExpr = this.buildFromJsonExpression(type.elementType!, 'e'); + const elementExpr = this.buildFromJsonExpression(type.elementType!, 'e', undefined, isInputContext); const mapCall = (target: string) => `${target}.map((e) => ${elementExpr}).toList()`; if (defaultExpression) { return `${listCast} == null ? ${defaultExpression} : ${mapCall(`${listCast}${type.nullable ? '!' : ''}`)}`; @@ -822,12 +844,13 @@ export class DartPlugin extends CodegenPlugin { } if (type.kind === 'enum') { + const enumDecoder = isInputContext ? 'fromJsonStrict' : 'fromJson'; if (defaultExpression) { - return `${sourceExpr} != null ? ${type.name}.fromJson(${sourceExpr} as String) : ${defaultExpression}`; + return `${sourceExpr} != null ? ${type.name}.${enumDecoder}(${sourceExpr} as String) : ${defaultExpression}`; } return type.nullable - ? `${sourceExpr} != null ? ${type.name}.fromJson(${sourceExpr} as String) : null` - : `${type.name}.fromJson(${sourceExpr} as String)`; + ? `${sourceExpr} != null ? ${type.name}.${enumDecoder}(${sourceExpr} as String) : null` + : `${type.name}.${enumDecoder}(${sourceExpr} as String)`; } if (['object', 'input', 'interface', 'union'].includes(type.kind)) { diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts index 73a15333d..7beb20402 100644 --- a/packages/gql/codegen/plugins/gdscript.ts +++ b/packages/gql/codegen/plugins/gdscript.ts @@ -42,8 +42,9 @@ export class GDScriptPlugin extends CodegenPlugin { mapType(type: IRType): string { if (type.kind === 'list') { const element = type.elementType!; - const isNullableEnum = element.nullable && (element.kind === 'enum' || this.enumNames.has(element.name!)); - const elementType = isNullableEnum ? 'Variant' : this.mapType(element); + const needsNullableVariant = + element.nullable && (element.kind === 'scalar' || element.kind === 'enum' || this.enumNames.has(element.name!)); + const elementType = needsNullableVariant ? 'Variant' : this.mapType(element); return `Array[${elementType}]`; } if (type.kind === 'scalar') { @@ -328,24 +329,35 @@ export class GDScriptPlugin extends CodegenPlugin { return empty ? `${typeName}.${this.enumValueCase(empty.name)}` : null; } - private emitEnumFromDictAssignment(indent: string, target: string, typeName: string, sourceExpression: string): void { + private emitEnumFromDictAssignment( + indent: string, + target: string, + typeName: string, + sourceExpression: string, + isInputContext = false, + ): void { const enumReverseLookup = toConstantCase(typeName) + '_FROM_STRING'; + const enumValues = toConstantCase(typeName) + '_VALUES'; const fallback = this.getEnumUnknownFallback(typeName); this.emit(`${indent}var enum_str = ${sourceExpression}`); if (fallback) { - this.emit(`${indent}if enum_str is String:`); - this.emit(`${indent}\t${target} = ${enumReverseLookup}.get(enum_str, ${fallback})`); - this.emit(`${indent}elif enum_str is int:`); + this.emit(`${indent}if enum_str is String and ${enumReverseLookup}.has(enum_str):`); + this.emit(`${indent}\t${target} = ${enumReverseLookup}[enum_str]`); + this.emit(`${indent}elif enum_str is int and ${enumValues}.has(enum_str):`); this.emit(`${indent}\t${target} = enum_str`); this.emit(`${indent}else:`); - this.emit(`${indent}\t${target} = ${fallback}`); + if (isInputContext) { + this.emit(`${indent}\tpush_error("Invalid ${typeName} input value")`); + this.emit(`${indent}\treturn null`); + } else { + this.emit(`${indent}\t${target} = ${fallback}`); + } return; } const emptyFallback = this.getEnumEmptyFallback(typeName); if (emptyFallback) { - const enumValues = toConstantCase(typeName) + '_VALUES'; this.emit(`${indent}if enum_str is String and ${enumReverseLookup}.has(enum_str):`); this.emit(`${indent}\t${target} = ${enumReverseLookup}[enum_str]`); this.emit(`${indent}elif enum_str is int and ${enumValues}.has(enum_str):`); @@ -359,8 +371,15 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`${indent}if enum_str is String and ${enumReverseLookup}.has(enum_str):`); this.emit(`${indent}\t${target} = ${enumReverseLookup}[enum_str]`); - this.emit(`${indent}else:`); + this.emit(`${indent}elif enum_str is int and ${enumValues}.has(enum_str):`); this.emit(`${indent}\t${target} = enum_str`); + this.emit(`${indent}else:`); + if (isInputContext) { + this.emit(`${indent}\tpush_error("Invalid ${typeName} input value")`); + this.emit(`${indent}\treturn null`); + } else { + this.emit(`${indent}\t${target} = enum_str`); + } } private emitEnumListToDictAssignment(indent: string, graphqlName: string, fieldName: string, typeName: string): void { @@ -532,7 +551,7 @@ export class GDScriptPlugin extends CodegenPlugin { const elementTypeName = elementType.name!; const gdElementType = this.mapType(elementType); const isEnumElement = elementType.kind === 'enum' || this.enumNames.has(elementTypeName); - const listElementType = elementType.nullable && isEnumElement ? 'Variant' : gdElementType; + const listElementType = elementType.nullable && (elementType.kind === 'scalar' || isEnumElement) ? 'Variant' : gdElementType; const listIndent = `${indent}\t`; const itemIndent = `${listIndent}\t`; this.emit(`${indent}if data["${graphqlName}"] is Array:`); @@ -541,7 +560,13 @@ export class GDScriptPlugin extends CodegenPlugin { if (this.isObjectOrInput(elementType)) { const needsStrictValidation = this.typeHasRequiredEnumWithoutUnknown(elementTypeName, this.schema); this.emit(`${itemIndent}if item is Dictionary:`); - if (needsStrictValidation) { + if (isRequestContext && this.inputNames.has(elementTypeName)) { + const decodedName = `decoded_${this.getGdscriptFieldName(elementTypeName)}`; + this.emit(`${itemIndent}\tvar ${decodedName} = ${elementTypeName}.from_dict(item)`); + this.emit(`${itemIndent}\tif ${decodedName} == null:`); + this.emit(`${itemIndent}\t\treturn null`); + this.emit(`${itemIndent}\tarr.append(${decodedName})`); + } else if (needsStrictValidation) { const decodedName = `decoded_${this.getGdscriptFieldName(elementTypeName)}`; const isRequestInput = this.inputNames.has(elementTypeName); if (elementType.nullable && !isRequestInput) { @@ -579,12 +604,16 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`${itemIndent}if item == null:`); this.emit(`${itemIndent}\tarr.append(null)`); } - this.emit(`${itemIndent}${elementType.nullable ? 'elif' : 'if'} item is String:`); - this.emit(`${itemIndent}\tarr.append(${enumReverseLookup}.get(item, ${fallback}))`); + this.emit(`${itemIndent}${elementType.nullable ? 'elif' : 'if'} item is String and ${enumReverseLookup}.has(item):`); + this.emit(`${itemIndent}\tarr.append(${enumReverseLookup}[item])`); this.emit(`${itemIndent}elif item is int and ${enumValues}.has(item):`); this.emit(`${itemIndent}\tarr.append(item)`); - this.emit(`${itemIndent}elif item is int:`); - this.emit(`${itemIndent}\tarr.append(${fallback})`); + this.emit(`${itemIndent}else:`); + if (isRequestContext) { + this.emitListDecodeFailure(itemIndent, elementTypeName, graphqlName, reportErrorsExpression); + } else { + this.emit(`${itemIndent}\tarr.append(${fallback})`); + } } else { if (elementType.nullable) { this.emit(`${itemIndent}if item == null:`); @@ -602,34 +631,52 @@ export class GDScriptPlugin extends CodegenPlugin { } } } else { + if (elementType.nullable) { + this.emit(`${itemIndent}if item == null:`); + this.emit(`${itemIndent}\tarr.append(null)`); + } + const conditionPrefix = elementType.nullable ? 'elif' : 'if'; switch (gdElementType) { case 'String': - this.emit(`${itemIndent}if item is String:`); + this.emit(`${itemIndent}${conditionPrefix} item is String:`); this.emit(`${itemIndent}\tarr.append(str(item))`); break; case 'int': - this.emit(`${itemIndent}if item is int:`); + this.emit(`${itemIndent}${conditionPrefix} item is int:`); this.emit(`${itemIndent}\tarr.append(item)`); this.emit(`${itemIndent}elif item is float:`); this.emit(`${itemIndent}\tarr.append(int(item))`); - this.emit(`${itemIndent}elif item is String and item.is_valid_int():`); - this.emit(`${itemIndent}\tarr.append(int(item))`); + if (!isRequestContext) { + this.emit(`${itemIndent}elif item is String and item.is_valid_int():`); + this.emit(`${itemIndent}\tarr.append(int(item))`); + } break; case 'float': - this.emit(`${itemIndent}if item is int or item is float:`); - this.emit(`${itemIndent}\tarr.append(float(item))`); - this.emit(`${itemIndent}elif item is String and item.is_valid_float():`); + this.emit(`${itemIndent}${conditionPrefix} item is int or item is float:`); this.emit(`${itemIndent}\tarr.append(float(item))`); + if (!isRequestContext) { + this.emit(`${itemIndent}elif item is String and item.is_valid_float():`); + this.emit(`${itemIndent}\tarr.append(float(item))`); + } break; case 'bool': - this.emit(`${itemIndent}if item is bool:`); + this.emit(`${itemIndent}${conditionPrefix} item is bool:`); this.emit(`${itemIndent}\tarr.append(bool(item))`); break; default: this.emit(`${itemIndent}arr.append(item)`); } + if (isRequestContext && gdElementType !== 'Variant') { + this.emit(`${itemIndent}else:`); + this.emitListDecodeFailure(itemIndent, gdElementType, graphqlName, reportErrorsExpression); + } } this.emit(`${listIndent}obj.${fieldName} = arr`); + if (isRequestContext) { + this.emit(`${indent}else:`); + this.emit(`${indent}\tpush_error("Invalid input list for ${graphqlName}")`); + this.emit(`${indent}\treturn null`); + } } private emitListDecodeFailure( @@ -742,6 +789,7 @@ export class GDScriptPlugin extends CodegenPlugin { } } + const responseDerivedInput = irInput.customTypeKind === 'PurchaseInput'; this.generateDocComment(irInput.description); this.emit(`class ${irInput.name}:`); @@ -769,16 +817,23 @@ export class GDScriptPlugin extends CodegenPlugin { // from_dict method this.emit(''); - const hasStrictRequiredEnum = this.typeHasRequiredEnumWithoutUnknown(irInput.name, this.schema); + const hasStrictRequiredEnum = !responseDerivedInput && this.typeHasRequiredEnumWithoutUnknown(irInput.name, this.schema); const decoderOptions = hasStrictRequiredEnum ? ', report_errors: bool = true' : ''; this.emit(`\tstatic func from_dict(data: Dictionary${decoderOptions}) -> ${irInput.name}:`); + if (!responseDerivedInput) { + this.emitInputFieldGuards(irInput.name, fields); + } if (hasStrictRequiredEnum) { this.emitRequiredEnumGuards(irInput.name, fields); } this.emit(`\t\tvar obj = ${irInput.name}.new()`); for (const field of fields) { const fieldName = this.getGdscriptFieldName(field.name, irInput.name); - this.generateInputFromDictField(field, fieldName); + if (responseDerivedInput) { + this.generateFromDictField(field, fieldName); + } else { + this.generateInputFromDictField(field, fieldName); + } } this.emit('\t\treturn obj'); @@ -796,6 +851,57 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(''); } + private emitInputFieldGuards(containerName: string, fields: IRField[]): void { + for (const field of fields) { + const source = `data["${field.name}"]`; + if (field.type.kind === 'enum') { + if (field.type.nullable || field.defaultValue !== undefined || !field.type.name) continue; + const irEnum = this.schema.enums.find((candidate) => candidate.name === field.type.name); + if (!irEnum || !this.enumUnknownValue(irEnum)) continue; + const lookup = `${toConstantCase(field.type.name)}_FROM_STRING`; + const values = `${toConstantCase(field.type.name)}_VALUES`; + const validValue = `((${source} is String and ${lookup}.has(${source})) or (${source} is int and ${values}.has(${source})))`; + this.emit(`\t\tif not data.has("${field.name}") or not ${validValue}:`); + this.emit(`\t\t\tpush_error("Invalid required ${containerName}.${field.name} value")`); + this.emit('\t\t\treturn null'); + continue; + } + let validValue: string | null = null; + if (field.type.kind === 'list') { + validValue = `${source} is Array`; + } else if (this.isObjectOrInput(field.type)) { + validValue = `${source} is Dictionary`; + } else if (field.type.kind === 'scalar') { + switch (this.mapScalar(field.type.name!)) { + case 'String': + validValue = `${source} is String`; + break; + case 'int': + validValue = `${source} is int`; + break; + case 'float': + validValue = `(${source} is int or ${source} is float)`; + break; + case 'bool': + validValue = `${source} is bool`; + break; + } + } + if (!validValue) continue; + const isRequired = !field.type.nullable && field.defaultValue === undefined; + if (isRequired) { + this.emit(`\t\tif not data.has("${field.name}") or not ${validValue}:`); + } else if (field.type.kind === 'scalar') { + this.emit(`\t\tif data.has("${field.name}") and data["${field.name}"] != null and not ${validValue}:`); + } else { + continue; + } + const qualifier = isRequired ? 'required ' : ''; + this.emit(`\t\t\tpush_error("Invalid ${qualifier}${containerName}.${field.name} value")`); + this.emit('\t\t\treturn null'); + } + } + private emitRequiredEnumGuards(containerName: string, fields: IRField[]): void { for (const field of fields) { if (field.type.kind !== 'enum' || field.type.nullable || !field.type.name) continue; @@ -876,14 +982,20 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit('\t\tvar obj = RequestPurchaseProps.new()'); this.emit('\t\tif has_purchase:'); this.emit('\t\t\tvar purchase_value = data["requestPurchase"]'); - this.emit( - '\t\t\tobj.request = RequestPurchasePropsByPlatforms.from_dict(purchase_value) if purchase_value is Dictionary else purchase_value', - ); + this.emit('\t\t\tif not purchase_value is Dictionary:'); + this.emit('\t\t\t\tpush_error("requestPurchase must be a dictionary")'); + this.emit('\t\t\t\treturn null'); + this.emit('\t\t\tobj.request = RequestPurchasePropsByPlatforms.from_dict(purchase_value)'); + this.emit('\t\t\tif obj.request == null:'); + this.emit('\t\t\t\treturn null'); this.emit('\t\telse:'); this.emit('\t\t\tvar subscription_value = data["requestSubscription"]'); - this.emit( - '\t\t\tobj.request_subscription = RequestSubscriptionPropsByPlatforms.from_dict(subscription_value) if subscription_value is Dictionary else subscription_value', - ); + this.emit('\t\t\tif not subscription_value is Dictionary:'); + this.emit('\t\t\t\tpush_error("requestSubscription must be a dictionary")'); + this.emit('\t\t\t\treturn null'); + this.emit('\t\t\tobj.request_subscription = RequestSubscriptionPropsByPlatforms.from_dict(subscription_value)'); + this.emit('\t\t\tif obj.request_subscription == null:'); + this.emit('\t\t\t\treturn null'); this.emit('\t\tvar expected_type = ProductQueryType.IN_APP if has_purchase else ProductQueryType.SUBS'); this.emit('\t\tobj.type = expected_type'); this.emit('\t\tif data.has("type") and data["type"] != null:'); @@ -923,11 +1035,11 @@ export class GDScriptPlugin extends CodegenPlugin { this.emit(`\t\tif data.has("${graphqlName}") and data["${graphqlName}"] != null:`); if (type.kind === 'list') { - this.generateListFromDictAssignment(type, graphqlName, fieldName, '\t\t\t', 'report_errors', true); + this.generateListFromDictAssignment(type, graphqlName, fieldName, '\t\t\t', null, true); } else if (this.isObjectOrInput(type)) { this.generateNestedFromDictAssignment(type, graphqlName, fieldName); } else if (type.kind === 'enum') { - this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`); + this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`, true); } else { this.emit(`\t\t\tobj.${fieldName} = data["${graphqlName}"]`); } @@ -1015,7 +1127,7 @@ export class GDScriptPlugin extends CodegenPlugin { if (arg.type.kind === 'list') { this.generateListFromDictAssignment(arg.type, arg.name, argSnakeName, '\t\t\t\t\t', null, true); } else if (arg.type.kind === 'enum') { - this.emitEnumFromDictAssignment('\t\t\t\t\t', `obj.${argSnakeName}`, arg.type.name!, `data["${arg.name}"]`); + this.emitEnumFromDictAssignment('\t\t\t\t\t', `obj.${argSnakeName}`, arg.type.name!, `data["${arg.name}"]`, true); } else { this.emit(`\t\t\t\t\tobj.${argSnakeName} = data["${arg.name}"]`); } diff --git a/packages/gql/codegen/plugins/kotlin.ts b/packages/gql/codegen/plugins/kotlin.ts index ab008e54c..46b9173dc 100644 --- a/packages/gql/codegen/plugins/kotlin.ts +++ b/packages/gql/codegen/plugins/kotlin.ts @@ -210,7 +210,7 @@ export class KotlinPlugin extends CodegenPlugin { this.emit(''); this.emit(' companion object {'); - this.emit(` fun fromJson(value: String): ${irEnum.name} = when (value) {`); + this.emit(` private fun fromKnownJson(value: String): ${irEnum.name}? = when (value) {`); for (const value of irEnum.values) { const caseName = this.escapeKeyword(this.enumValueCase(value.name)); @@ -227,12 +227,17 @@ export class KotlinPlugin extends CodegenPlugin { } } + this.emit(' else -> null'); + this.emit(' }'); + this.emit(''); const unknownValue = this.enumUnknownValue(irEnum); const unknownExpression = unknownValue ? `${irEnum.name}.${this.escapeKeyword(this.enumValueCase(unknownValue.name))}` : `throw IllegalArgumentException("Unknown ${irEnum.name} value: $value")`; - this.emit(` else -> ${unknownExpression}`); - this.emit(' }'); + this.emit(` fun fromJson(value: String): ${irEnum.name} = fromKnownJson(value) ?: ${unknownExpression}`); + this.emit(''); + this.emit(` fun fromJsonStrict(value: String): ${irEnum.name} =`); + this.emit(` fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ${irEnum.name} input value: $value")`); this.emit(' }'); this.emit(''); this.emit(' fun toJson(): String = rawValue'); @@ -541,12 +546,15 @@ export class KotlinPlugin extends CodegenPlugin { true, this.buildDefaultValueExpression(field), rejectMissingStrictEnums, + true, ); this.emit(` val ${propertyName} = ${expression}`); } // Null check for required fields (excluding enums which have fallbacks) - const requiredFields = sortedFields.filter((f) => !f.type.nullable && !this.hasSchemaDefault(f) && f.type.kind !== 'enum'); + const requiredFields = sortedFields.filter( + (f) => !f.type.nullable && !this.hasSchemaDefault(f) && !['enum', 'input', 'list'].includes(f.type.kind), + ); if (requiredFields.length > 0) { const nullChecks = requiredFields.map((f) => `${this.escapeKeyword(this.fieldNameCase(f.name))} == null`).join(' || '); this.emit(` if (${nullChecks}) return null`); @@ -572,6 +580,7 @@ export class KotlinPlugin extends CodegenPlugin { false, this.buildDefaultValueExpression(field), rejectMissingStrictEnums, + true, ); this.emit(` ${propertyName} = ${expression},`); } @@ -671,6 +680,8 @@ export class KotlinPlugin extends CodegenPlugin { false, true, this.buildDefaultValueExpression(value), + false, + true, ); this.emit(` val ${value.name} = ${expression}`); } @@ -689,6 +700,8 @@ export class KotlinPlugin extends CodegenPlugin { false, false, this.buildDefaultValueExpression(value), + false, + true, ); this.emit(` ${value.name} = ${expression},`); } @@ -761,6 +774,7 @@ export class KotlinPlugin extends CodegenPlugin { true, this.buildDefaultValueExpression(field), rejectMissingStrictEnums, + true, ); this.emit(` val ${propertyName} = ${expression}`); } @@ -792,6 +806,7 @@ export class KotlinPlugin extends CodegenPlugin { false, this.buildDefaultValueExpression(field), rejectMissingStrictEnums, + true, ); this.emit(` ${propertyName} = ${expression},`); } @@ -1067,6 +1082,7 @@ export class KotlinPlugin extends CodegenPlugin { forNullableFromJson: boolean = false, defaultExpression?: string | null, rejectMissingStrictEnums: boolean = false, + isInputContext: boolean = false, ): string { if (type.kind === 'list') { const element = this.buildFromJsonExpression( @@ -1076,31 +1092,66 @@ export class KotlinPlugin extends CodegenPlugin { forNullableFromJson, undefined, rejectMissingStrictEnums, + isInputContext, ); - const mapFn = type.elementType!.nullable ? 'map' : 'mapNotNull'; + const mapFn = isInputContext || type.elementType!.nullable ? 'map' : 'mapNotNull'; + const decodedList = `(${sourceExpr} as? List<*>)?.${mapFn} { ${element} }`; + if (isInputContext) { + const invalidInput = 'throw IllegalArgumentException("Invalid input list")'; + if (defaultExpression) { + return `if (${sourceExpr} == null) ${defaultExpression} else ${decodedList} ?: ${invalidInput}`; + } + if (type.nullable) { + return `${sourceExpr}?.let { raw -> (raw as? List<*>)?.${mapFn} { ${element} } ?: ${invalidInput} }`; + } + return `${decodedList} ?: ${invalidInput}`; + } if (defaultExpression) { - return `(${sourceExpr} as? List<*>)?.${mapFn} { ${element} } ?: ${defaultExpression}`; + return `${decodedList} ?: ${defaultExpression}`; } if (type.nullable || forNullableFromJson) { - return `(${sourceExpr} as? List<*>)?.${mapFn} { ${element} }`; + return decodedList; } - return `(${sourceExpr} as? List<*>)?.${mapFn} { ${element} } ?: emptyList()`; + return `${decodedList} ?: emptyList()`; } if (type.kind === 'scalar') { const useNullable = type.nullable || isListElement || forNullableFromJson; + const strictInputScalar = (cast: string): string => { + const location = isListElement ? 'input list element' : 'input value'; + const invalidInput = `throw IllegalArgumentException("Invalid ${type.name} ${location}")`; + if (isListElement && !type.nullable) return `${cast} ?: ${invalidInput}`; + if (defaultExpression) { + return `if (${sourceExpr} == null) ${defaultExpression} else (${cast}) ?: ${invalidInput}`; + } + if (type.nullable) { + return `${sourceExpr}?.let { raw -> (${cast.replace(sourceExpr, 'raw')}) ?: ${invalidInput} }`; + } + return cast; + }; switch (type.name) { case 'Float': + if (isInputContext) { + return strictInputScalar(`(${sourceExpr} as? Number)?.toDouble()`); + } if (defaultExpression) { return `(${sourceExpr} as? Number)?.toDouble() ?: ${defaultExpression}`; } return useNullable ? `(${sourceExpr} as? Number)?.toDouble()` : `(${sourceExpr} as? Number)?.toDouble() ?: 0.0`; case 'Int': + if (isInputContext) { + return strictInputScalar( + `(${sourceExpr} as? Number)?.let { number -> number.toLong().takeIf { value -> value >= Int.MIN_VALUE.toLong() && value <= Int.MAX_VALUE.toLong() && value.toDouble() == number.toDouble() }?.toInt() }`, + ); + } if (defaultExpression) { return `(${sourceExpr} as? Number)?.toInt() ?: ${defaultExpression}`; } return useNullable ? `(${sourceExpr} as? Number)?.toInt()` : `(${sourceExpr} as? Number)?.toInt() ?: 0`; case 'Boolean': + if (isInputContext) { + return strictInputScalar(`${sourceExpr} as? Boolean`); + } if (defaultExpression) { return `${sourceExpr} as? Boolean ?: ${defaultExpression}`; } @@ -1108,6 +1159,9 @@ export class KotlinPlugin extends CodegenPlugin { case 'ID': case 'String': default: + if (isInputContext) { + return strictInputScalar(`${sourceExpr} as? String`); + } if (defaultExpression) { return `${sourceExpr} as? String ?: ${defaultExpression}`; } @@ -1117,7 +1171,18 @@ export class KotlinPlugin extends CodegenPlugin { if (type.kind === 'enum') { const unknownFallback = this.buildUnknownEnumFallbackExpression(type); - const enumRead = `(${sourceExpr} as? String)?.let { ${type.name}.fromJson(it) }`; + const enumDecoder = isInputContext ? 'fromJsonStrict' : 'fromJson'; + const enumRead = `(${sourceExpr} as? String)?.let { ${type.name}.${enumDecoder}(it) }`; + if (isInputContext) { + const invalidInput = `throw IllegalArgumentException("Missing or invalid enum input value for ${type.name}")`; + if (defaultExpression) { + return `if (${sourceExpr} == null) ${defaultExpression} else ${enumRead} ?: ${invalidInput}`; + } + if (type.nullable) { + return `${sourceExpr}?.let { raw -> (raw as? String)?.let { ${type.name}.fromJsonStrict(it) } ?: ${invalidInput} }`; + } + return `${enumRead} ?: ${invalidInput}`; + } if (defaultExpression) { return `${enumRead} ?: ${defaultExpression}`; } @@ -1146,8 +1211,12 @@ export class KotlinPlugin extends CodegenPlugin { if (['object', 'input', 'interface', 'union'].includes(type.kind)) { const callTarget = type.name!; - if (type.kind === 'input' && type.nullable) { - return `${sourceExpr}?.let { value -> (value as? Map)?.let { ${callTarget}.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for ${callTarget}") }`; + if (isInputContext && type.kind === 'input') { + const invalidInput = `throw IllegalArgumentException("Invalid input object for ${callTarget}")`; + if (type.nullable) { + return `${sourceExpr}?.let { value -> (value as? Map)?.let { ${callTarget}.fromJson(it) } ?: ${invalidInput} }`; + } + return `(${sourceExpr} as? Map)?.let { ${callTarget}.fromJson(it) } ?: ${invalidInput}`; } if (type.nullable || forNullableFromJson) { if (this.typeNeedsTolerantNullableDecoder(callTarget, this.schema)) { diff --git a/packages/gql/generated-sync-manifest.mjs b/packages/gql/generated-sync-manifest.mjs index 0f2d378be..5aa4b122e 100644 --- a/packages/gql/generated-sync-manifest.mjs +++ b/packages/gql/generated-sync-manifest.mjs @@ -119,12 +119,17 @@ export const GENERATED_SYNC_EDGES = Object.freeze( ), ); +export const GENERATED_TEST_FIXTURE_PATHS = Object.freeze([ + 'libraries/godot-iap/Example/tests/generated_nullable_enum_list_types.gd', +]); + export const GENERATED_DRIFT_PATHS = Object.freeze([ ...new Set([ ...Object.values(GENERATED_SYNC_MANIFEST) .filter((definition) => definition.generated) .map((definition) => definition.source), ...GENERATED_SYNC_EDGES.map((edge) => edge.path), + ...GENERATED_TEST_FIXTURE_PATHS, ]), ]); diff --git a/packages/gql/package.json b/packages/gql/package.json index 24971dc83..3fbb53905 100644 --- a/packages/gql/package.json +++ b/packages/gql/package.json @@ -13,7 +13,7 @@ "./csharp": "./src/generated/Types.cs" }, "scripts": { - "generate": "bun run generate:ts && bun codegen/index.ts && bun run sync", + "generate": "bun run generate:ts && bun codegen/index.ts && bun scripts/generate-gdscript-runtime-fixture.ts && bun run sync", "generate:ts": "graphql-codegen --config codegen.ts && bun scripts/fix-generated-types.mjs", "generate:swift": "bun codegen/index.ts swift", "generate:kotlin": "bun codegen/index.ts kotlin", diff --git a/packages/gql/scripts/generate-gdscript-runtime-fixture.ts b/packages/gql/scripts/generate-gdscript-runtime-fixture.ts new file mode 100644 index 000000000..f35a52018 --- /dev/null +++ b/packages/gql/scripts/generate-gdscript-runtime-fixture.ts @@ -0,0 +1,17 @@ +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { nullableEnumListSchema } from "../codegen/fixtures/nullable-enum-list.js"; +import { GDScriptPlugin } from "../codegen/plugins/gdscript.js"; + +const packageRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); +const repositoryRoot = resolve(packageRoot, "../.."); +const outputPath = resolve( + repositoryRoot, + "libraries/godot-iap/Example/tests/generated_nullable_enum_list_types.gd", +); +const source = new GDScriptPlugin({ + outputPath: "generated_nullable_enum_list_types.gd", +}).generate(nullableEnumListSchema()); + +writeFileSync(outputPath, source); diff --git a/packages/gql/src/enum-decoder-compatibility.test.ts b/packages/gql/src/enum-decoder-compatibility.test.ts index 78a4e236c..18e6a8063 100644 --- a/packages/gql/src/enum-decoder-compatibility.test.ts +++ b/packages/gql/src/enum-decoder-compatibility.test.ts @@ -191,6 +191,22 @@ function decoderSchema(): IRSchema { { name: 'InputPayload', fields: [ + field('status', { + kind: 'enum', + name: 'TolerantStatus', + nullable: false, + }), + field('labels', { + kind: 'list', + nullable: false, + elementType: { + kind: 'scalar', + name: 'String', + nullable: false, + }, + }), + field('note', { kind: 'scalar', name: 'String', nullable: true }), + field('count', { kind: 'scalar', name: 'Int', nullable: true }), field('format', { kind: 'enum', name: 'StrictFormat', @@ -217,6 +233,15 @@ function decoderSchema(): IRSchema { nullable: true, }, }), + field('statuses', { + kind: 'list', + nullable: true, + elementType: { + kind: 'enum', + name: 'TolerantStatus', + nullable: false, + }, + }), ], hasRequiredFields: false, isCustomType: false, @@ -257,9 +282,11 @@ describe('unknown enum decoder compatibility', () => { it('degrades neutral Kotlin enums and drops an unreadable optional payload', () => { const output = new KotlinPlugin({ outputPath: 'Types.kt' }).generate(decoderSchema()); - expect(output).toContain('else -> TolerantStatus.Unknown'); + expect(output).toContain('fun fromJson(value: String): TolerantStatus = fromKnownJson(value) ?: TolerantStatus.Unknown'); expect(output).not.toContain('Unknown TolerantStatus value'); - expect(output).toContain('else -> throw IllegalArgumentException("Unknown StrictFormat value: $value")'); + expect(output).toContain( + 'fun fromJson(value: String): StrictFormat = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown StrictFormat value: $value")', + ); expect(output).toContain( 'format = (json["format"] as? String)?.let { StrictFormat.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for StrictFormat")', ); @@ -278,6 +305,19 @@ describe('unknown enum decoder compatibility', () => { expect(output).toContain( 'option = json["option"]?.let { value -> (value as? Map)?.let { InputPayload.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for InputPayload") }', ); + expect(output).toContain('(json["status"] as? String)?.let { TolerantStatus.fromJsonStrict(it) }'); + expect(output).toContain( + 'statuses = json["statuses"]?.let { raw -> (raw as? List<*>)?.map { (it as? String)?.let { TolerantStatus.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for TolerantStatus") } ?: throw IllegalArgumentException("Invalid input list") }', + ); + expect(output).toContain( + 'labels = (json["labels"] as? List<*>)?.map { it as? String ?: throw IllegalArgumentException("Invalid String input list element") } ?: throw IllegalArgumentException("Invalid input list")', + ); + expect(output).toContain( + 'note = json["note"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }', + ); + expect(output).toContain( + 'number.toLong().takeIf { value -> value >= Int.MIN_VALUE.toLong() && value <= Int.MAX_VALUE.toLong() && value.toDouble() == number.toDouble() }?.toInt()', + ); expect(output).toContain('platform = (json["platform"] as? String)?.let { Platform.fromJson(it) } ?: Platform.Android'); }); @@ -299,6 +339,10 @@ describe('unknown enum decoder compatibility', () => { expect(output).toContain( "payload: json['payload'] is Map ? Payload._tryFromJson(json['payload'] as Map) : null", ); + expect(output).toContain("status: TolerantStatus.fromJsonStrict(json['status'] as String)"); + expect(output).toContain( + "statuses: (json['statuses'] as List?) == null ? null : (json['statuses'] as List?)!.map((e) => TolerantStatus.fromJsonStrict(e as String)).toList()", + ); }); it('degrades neutral C# enums and drops an unreadable optional payload', () => { @@ -315,6 +359,13 @@ describe('unknown enum decoder compatibility', () => { expect(output).toContain('[JsonConverter(typeof(ListOnlyPayloadNullableElementListJsonConverter))]'); expect(output).not.toContain('InputPayloadNullableJsonConverter'); expect(output).toContain('[JsonConverter(typeof(PayloadNullableJsonConverter))]'); + expect(output).toContain('public sealed class StrictEnumJsonConverter'); + expect(output).toContain( + '[JsonConverter(typeof(StrictEnumJsonConverter))]', + ); + expect(output).toContain( + '[JsonConverter(typeof(StrictEnumListJsonConverter))]', + ); expect(output).toContain('return document.RootElement.Deserialize(options);'); expect(output).toContain('catch (JsonException)'); }); @@ -322,7 +373,8 @@ describe('unknown enum decoder compatibility', () => { it('degrades neutral GDScript enums and drops an unreadable optional payload', () => { const output = new GDScriptPlugin({ outputPath: 'types.gd' }).generate(decoderSchema()); - expect(output).toContain('obj.status = TOLERANT_STATUS_FROM_STRING.get(enum_str, TolerantStatus.UNKNOWN)'); + expect(output).toContain('if enum_str is String and TOLERANT_STATUS_FROM_STRING.has(enum_str):'); + expect(output).toContain('elif enum_str is int and TOLERANT_STATUS_VALUES.has(enum_str):'); expect(output).toContain('obj.status = TolerantStatus.UNKNOWN'); expect(output).toContain('static func from_dict(data: Dictionary, report_errors: bool = true) -> Payload:'); expect(output).toContain('push_error("Invalid Payload.format enum value")'); @@ -349,6 +401,12 @@ describe('unknown enum decoder compatibility', () => { expect(output).toContain('if data["formats"] is Array:\n\t\t\t\tvar arr: Array[Variant] = []'); expect(output).toContain('if item == null:\n\t\t\t\t\t\tarr.append(null)'); expect(output).toContain('push_error("Invalid StrictFormat list value for formats")'); + expect(output).toContain('push_error("Invalid TolerantStatus input value")'); + expect(output).toContain('push_error("Invalid required InputPayload.status value")'); + expect(output).toContain('push_error("Invalid TolerantStatus list value for statuses")'); + expect(output).toContain('push_error("Invalid required InputPayload.labels value")'); + expect(output).toContain('push_error("Invalid String list value for labels")'); + expect(output).toContain('push_error("Invalid InputPayload.note value")'); const inputPayload = output.slice(output.indexOf('class InputPayload:'), output.indexOf('class RequestInput:')); expect(inputPayload).not.toContain('from_dict_or_null'); const operation = output.slice(output.indexOf('class Mutation:')); diff --git a/packages/gql/src/generated-gdscript.test.ts b/packages/gql/src/generated-gdscript.test.ts index aab868ed3..5504d6b70 100644 --- a/packages/gql/src/generated-gdscript.test.ts +++ b/packages/gql/src/generated-gdscript.test.ts @@ -1,9 +1,13 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -import type { IRSchema } from '../codegen/core/types.js'; +import { nullableEnumListSchema } from '../codegen/fixtures/nullable-enum-list.js'; import { GDScriptPlugin } from '../codegen/plugins/gdscript.js'; const generated = readFileSync(new URL('./generated/types.gd', import.meta.url), 'utf8'); +const nullableEnumListFixture = readFileSync( + new URL('../../../libraries/godot-iap/Example/tests/generated_nullable_enum_list_types.gd', import.meta.url), + 'utf8', +); function classSource(className: string, nextClassName: string): string { const start = generated.indexOf(`class ${className}:`); @@ -65,82 +69,16 @@ describe('generated GDScript list decoding', () => { }); it('validates enum list values before appending to a typed array', () => { - const schema: IRSchema = { - enums: [ - { - name: 'TestStatus', - values: [ - { name: 'Unknown', rawValue: 'unknown', legacyAliases: [] }, - { name: 'Active', rawValue: 'active', legacyAliases: [] }, - ], - isErrorCode: false, - }, - { - name: 'StrictStatus', - values: [{ name: 'Active', rawValue: 'active', legacyAliases: [] }], - isErrorCode: false, - }, - ], - interfaces: [], - objects: [ - { - name: 'EnumListHolder', - fields: [ - { - name: 'statuses', - description: 'Status values from the schema.\n\nPreserves every documentation line.\n@see https://openiap.dev/docs/types', - type: { - kind: 'list', - nullable: false, - elementType: { - kind: 'enum', - name: 'TestStatus', - nullable: false, - }, - }, - isOverride: false, - }, - { - name: 'strictStatuses', - type: { - kind: 'list', - nullable: false, - elementType: { - kind: 'enum', - name: 'StrictStatus', - nullable: false, - }, - }, - isOverride: false, - }, - { - name: 'nullableStrictStatuses', - type: { - kind: 'list', - nullable: false, - elementType: { - kind: 'enum', - name: 'StrictStatus', - nullable: true, - }, - }, - isOverride: false, - }, - ], - interfaces: [], - unions: [], - isResultUnion: false, - }, - ], - inputs: [], - unions: [], - operations: [], - }; - const source = new GDScriptPlugin({ outputPath: 'types.gd' }).generate(schema); + const source = new GDScriptPlugin({ outputPath: 'generated_nullable_enum_list_types.gd' }).generate( + nullableEnumListSchema(), + ); + + expect(nullableEnumListFixture).toBe(source); expect(source).toContain('## Status values from the schema. Preserves every documentation line. @see https://openiap.dev/docs/types'); expect(source).toContain('if data["statuses"] is Array:'); - expect(source).toContain('arr.append(TEST_STATUS_FROM_STRING.get(item, TestStatus.UNKNOWN))'); + expect(source).toContain('if item is String and TEST_STATUS_FROM_STRING.has(item):'); + expect(source).toContain('arr.append(TEST_STATUS_FROM_STRING[item])'); expect(source).toContain('elif item is int and TEST_STATUS_VALUES.has(item):'); expect(source).toContain('arr.append(TestStatus.UNKNOWN)'); expect(source).toContain('if item is String and STRICT_STATUS_FROM_STRING.has(item):'); @@ -149,7 +87,13 @@ describe('generated GDScript list decoding', () => { expect(source).toContain('push_error("Invalid StrictStatus list value for strictStatuses")'); expect(source).toContain('return null'); expect(source).toContain('var nullable_strict_statuses: Array[Variant] = []'); - expect(source).toContain('if data["nullableStrictStatuses"] is Array:\n\t\t\t\tvar arr: Array[Variant] = []'); - expect(source).toContain('\t\t\t\t\tarr.append(null)'); + const nullableStrictStart = source.indexOf('if data["nullableStrictStatuses"] is Array:'); + const nullableStrictEnd = source.indexOf('obj.nullable_strict_statuses = arr', nullableStrictStart); + const nullableStrictDecoder = source.slice(nullableStrictStart, nullableStrictEnd); + expect(nullableStrictDecoder).toContain('var arr: Array[Variant] = []'); + expect(nullableStrictDecoder).toContain('if item == null:\n\t\t\t\t\t\tarr.append(null)'); + expect(nullableStrictDecoder).toContain('else:\n\t\t\t\t\t\tarr.append(null)'); + expect(source).toContain('var nullable_labels: Array[Variant] = []'); + expect(source).toContain('if data["nullableLabels"] is Array:\n\t\t\t\tvar arr: Array[Variant] = []'); }); }); diff --git a/packages/gql/src/generated-sync-manifest.test.mjs b/packages/gql/src/generated-sync-manifest.test.mjs index 5e719871d..65ebf3979 100644 --- a/packages/gql/src/generated-sync-manifest.test.mjs +++ b/packages/gql/src/generated-sync-manifest.test.mjs @@ -14,6 +14,7 @@ import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { GENERATED_DRIFT_PATHS, + GENERATED_TEST_FIXTURE_PATHS, GENERATED_SYNC_EDGES, GENERATED_SYNC_MANIFEST, GQL_GENERATED_SOURCE_DIRECTORY, @@ -86,6 +87,7 @@ describe("generated sync manifest", () => { .filter((definition) => definition.generated) .map((definition) => definition.source), ...GENERATED_SYNC_EDGES.map((edge) => edge.path), + ...GENERATED_TEST_FIXTURE_PATHS, ]); expect(new Set(GENERATED_DRIFT_PATHS)).toEqual(expected); @@ -345,7 +347,7 @@ printf '%s|%s' "$GIT_INDEX_FILE" "$GIT_DIR" > "$HOOK_AFTER_FILE" "cd packages/gql && bun run generate", ); expect(gqlPackageJson.scripts.generate).toBe( - "bun run generate:ts && bun codegen/index.ts && bun run sync", + "bun run generate:ts && bun codegen/index.ts && bun scripts/generate-gdscript-runtime-fixture.ts && bun run sync", ); for (const path of [ diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index 4fd848499..79f56d862 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -13,6 +13,116 @@ namespace OpenIap; +public interface IOpenIapEnumJsonConverter where TEnum : struct, Enum +{ + bool TryReadRaw(string value, out TEnum result); + string WriteRaw(TEnum value); +} + +public sealed class StrictEnumJsonConverter : JsonConverter + where TEnum : struct, Enum + where TConverter : IOpenIapEnumJsonConverter, new() +{ + private static readonly TConverter Converter = new(); + + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var raw = reader.GetString(); + if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value; + throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}"); + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) => + writer.WriteStringValue(Converter.WriteRaw(value)); +} + +public sealed class StrictNullableEnumJsonConverter : JsonConverter + where TEnum : struct, Enum + where TConverter : IOpenIapEnumJsonConverter, new() +{ + private static readonly TConverter Converter = new(); + + public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var raw = reader.GetString(); + if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value; + throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}"); + } + + public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options) + { + if (value is null) writer.WriteNullValue(); + else writer.WriteStringValue(Converter.WriteRaw(value.Value)); + } +} + +public sealed class StrictEnumListJsonConverter : JsonConverter> + where TEnum : struct, Enum + where TConverter : IOpenIapEnumJsonConverter, new() +{ + private static readonly TConverter Converter = new(); + + public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("Expected an enum input array."); + var values = new List(); + foreach (var element in document.RootElement.EnumerateArray()) + { + var raw = element.ValueKind == JsonValueKind.String ? element.GetString() : null; + if (raw is null || !Converter.TryReadRaw(raw, out var value)) + throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}"); + values.Add(value); + } + return values; + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + foreach (var item in value) writer.WriteStringValue(Converter.WriteRaw(item)); + writer.WriteEndArray(); + } +} + +public sealed class StrictNullableEnumListJsonConverter : JsonConverter> + where TEnum : struct, Enum + where TConverter : IOpenIapEnumJsonConverter, new() +{ + private static readonly TConverter Converter = new(); + + public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + if (document.RootElement.ValueKind != JsonValueKind.Array) throw new JsonException("Expected an enum input array."); + var values = new List(); + foreach (var element in document.RootElement.EnumerateArray()) + { + if (element.ValueKind == JsonValueKind.Null) + { + values.Add(null); + continue; + } + var raw = element.ValueKind == JsonValueKind.String ? element.GetString() : null; + if (raw is null || !Converter.TryReadRaw(raw, out var value)) + throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}"); + values.Add(value); + } + return values; + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + foreach (var item in value) + { + if (item is null) writer.WriteNullValue(); + else writer.WriteStringValue(Converter.WriteRaw(item.Value)); + } + writer.WriteEndArray(); + } +} + // ============================================================================ // Enums // ============================================================================ @@ -32,7 +142,7 @@ public enum BillingChoiceImageLayoutAndroid RectangularTwoByTwo } -public sealed class BillingChoiceImageLayoutAndroidJsonConverter : JsonConverter +public sealed class BillingChoiceImageLayoutAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -66,6 +176,11 @@ public override void Write(Utf8JsonWriter writer, BillingChoiceImageLayoutAndroi internal static string ToRawString(BillingChoiceImageLayoutAndroid value) => _toString[value]; internal static BillingChoiceImageLayoutAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown BillingChoiceImageLayoutAndroid value: {value}"); + + public bool TryReadRaw(string value, out BillingChoiceImageLayoutAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(BillingChoiceImageLayoutAndroid value) => _toString[value]; } public static class BillingChoiceImageLayoutAndroidExtensions @@ -89,7 +204,7 @@ public enum BillingChoiceScreenTypeAndroid GoogleRendered } -public sealed class BillingChoiceScreenTypeAndroidJsonConverter : JsonConverter +public sealed class BillingChoiceScreenTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -123,6 +238,11 @@ public override void Write(Utf8JsonWriter writer, BillingChoiceScreenTypeAndroid internal static string ToRawString(BillingChoiceScreenTypeAndroid value) => _toString[value]; internal static BillingChoiceScreenTypeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown BillingChoiceScreenTypeAndroid value: {value}"); + + public bool TryReadRaw(string value, out BillingChoiceScreenTypeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(BillingChoiceScreenTypeAndroid value) => _toString[value]; } public static class BillingChoiceScreenTypeAndroidExtensions @@ -176,7 +296,7 @@ public enum BillingProgramAndroid BillingChoice } -public sealed class BillingProgramAndroidJsonConverter : JsonConverter +public sealed class BillingProgramAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -219,6 +339,11 @@ public override void Write(Utf8JsonWriter writer, BillingProgramAndroid value, J internal static string ToRawString(BillingProgramAndroid value) => _toString[value]; internal static BillingProgramAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown BillingProgramAndroid value: {value}"); + + public bool TryReadRaw(string value, out BillingProgramAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(BillingProgramAndroid value) => _toString[value]; } public static class BillingProgramAndroidExtensions @@ -249,7 +374,7 @@ public enum DeveloperBillingLaunchModeAndroid CallerWillLaunchLink } -public sealed class DeveloperBillingLaunchModeAndroidJsonConverter : JsonConverter +public sealed class DeveloperBillingLaunchModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -283,6 +408,11 @@ public override void Write(Utf8JsonWriter writer, DeveloperBillingLaunchModeAndr internal static string ToRawString(DeveloperBillingLaunchModeAndroid value) => _toString[value]; internal static DeveloperBillingLaunchModeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown DeveloperBillingLaunchModeAndroid value: {value}"); + + public bool TryReadRaw(string value, out DeveloperBillingLaunchModeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(DeveloperBillingLaunchModeAndroid value) => _toString[value]; } public static class DeveloperBillingLaunchModeAndroidExtensions @@ -306,7 +436,7 @@ public enum DeveloperBillingTypeAndroid ExternalLink } -public sealed class DeveloperBillingTypeAndroidJsonConverter : JsonConverter +public sealed class DeveloperBillingTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -340,6 +470,11 @@ public override void Write(Utf8JsonWriter writer, DeveloperBillingTypeAndroid va internal static string ToRawString(DeveloperBillingTypeAndroid value) => _toString[value]; internal static DeveloperBillingTypeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown DeveloperBillingTypeAndroid value: {value}"); + + public bool TryReadRaw(string value, out DeveloperBillingTypeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(DeveloperBillingTypeAndroid value) => _toString[value]; } public static class DeveloperBillingTypeAndroidExtensions @@ -363,7 +498,7 @@ public enum DiscountOfferType OneTime } -public sealed class DiscountOfferTypeJsonConverter : JsonConverter +public sealed class DiscountOfferTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -400,6 +535,11 @@ public override void Write(Utf8JsonWriter writer, DiscountOfferType value, JsonS internal static string ToRawString(DiscountOfferType value) => _toString[value]; internal static DiscountOfferType FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown DiscountOfferType value: {value}"); + + public bool TryReadRaw(string value, out DiscountOfferType result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(DiscountOfferType value) => _toString[value]; } public static class DiscountOfferTypeExtensions @@ -449,7 +589,7 @@ public enum ErrorCode DuplicatePurchase } -public sealed class ErrorCodeJsonConverter : JsonConverter +public sealed class ErrorCodeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -618,6 +758,11 @@ public override void Write(Utf8JsonWriter writer, ErrorCode value, JsonSerialize internal static string ToRawString(ErrorCode value) => _toString[value]; internal static ErrorCode FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : ErrorCode.Unknown; + + public bool TryReadRaw(string value, out ErrorCode result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ErrorCode value) => _toString[value]; } public static class ErrorCodeExtensions @@ -643,7 +788,7 @@ public enum ExternalLinkLaunchModeAndroid CallerWillLaunchLink } -public sealed class ExternalLinkLaunchModeAndroidJsonConverter : JsonConverter +public sealed class ExternalLinkLaunchModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -677,6 +822,11 @@ public override void Write(Utf8JsonWriter writer, ExternalLinkLaunchModeAndroid internal static string ToRawString(ExternalLinkLaunchModeAndroid value) => _toString[value]; internal static ExternalLinkLaunchModeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ExternalLinkLaunchModeAndroid value: {value}"); + + public bool TryReadRaw(string value, out ExternalLinkLaunchModeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ExternalLinkLaunchModeAndroid value) => _toString[value]; } public static class ExternalLinkLaunchModeAndroidExtensions @@ -701,7 +851,7 @@ public enum ExternalLinkTypeAndroid LinkToAppDownload } -public sealed class ExternalLinkTypeAndroidJsonConverter : JsonConverter +public sealed class ExternalLinkTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -735,6 +885,11 @@ public override void Write(Utf8JsonWriter writer, ExternalLinkTypeAndroid value, internal static string ToRawString(ExternalLinkTypeAndroid value) => _toString[value]; internal static ExternalLinkTypeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ExternalLinkTypeAndroid value: {value}"); + + public bool TryReadRaw(string value, out ExternalLinkTypeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ExternalLinkTypeAndroid value) => _toString[value]; } public static class ExternalLinkTypeAndroidExtensions @@ -758,7 +913,7 @@ public enum ExternalPurchaseCustomLinkNoticeTypeIOS Browser } -public sealed class ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter : JsonConverter +public sealed class ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -787,6 +942,11 @@ public override void Write(Utf8JsonWriter writer, ExternalPurchaseCustomLinkNoti internal static string ToRawString(ExternalPurchaseCustomLinkNoticeTypeIOS value) => _toString[value]; internal static ExternalPurchaseCustomLinkNoticeTypeIOS FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: {value}"); + + public bool TryReadRaw(string value, out ExternalPurchaseCustomLinkNoticeTypeIOS result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ExternalPurchaseCustomLinkNoticeTypeIOS value) => _toString[value]; } public static class ExternalPurchaseCustomLinkNoticeTypeIOSExtensions @@ -815,7 +975,7 @@ public enum ExternalPurchaseCustomLinkTokenTypeIOS Services } -public sealed class ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter : JsonConverter +public sealed class ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -848,6 +1008,11 @@ public override void Write(Utf8JsonWriter writer, ExternalPurchaseCustomLinkToke internal static string ToRawString(ExternalPurchaseCustomLinkTokenTypeIOS value) => _toString[value]; internal static ExternalPurchaseCustomLinkTokenTypeIOS FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: {value}"); + + public bool TryReadRaw(string value, out ExternalPurchaseCustomLinkTokenTypeIOS result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ExternalPurchaseCustomLinkTokenTypeIOS value) => _toString[value]; } public static class ExternalPurchaseCustomLinkTokenTypeIOSExtensions @@ -866,7 +1031,7 @@ public enum ExternalPurchaseNoticeAction Dismissed } -public sealed class ExternalPurchaseNoticeActionJsonConverter : JsonConverter +public sealed class ExternalPurchaseNoticeActionJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -899,6 +1064,11 @@ public override void Write(Utf8JsonWriter writer, ExternalPurchaseNoticeAction v internal static string ToRawString(ExternalPurchaseNoticeAction value) => _toString[value]; internal static ExternalPurchaseNoticeAction FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ExternalPurchaseNoticeAction value: {value}"); + + public bool TryReadRaw(string value, out ExternalPurchaseNoticeAction result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ExternalPurchaseNoticeAction value) => _toString[value]; } public static class ExternalPurchaseNoticeActionExtensions @@ -931,7 +1101,7 @@ public enum IapEvent SubscriptionBillingIssue } -public sealed class IapEventJsonConverter : JsonConverter +public sealed class IapEventJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -980,6 +1150,11 @@ public override void Write(Utf8JsonWriter writer, IapEvent value, JsonSerializer internal static string ToRawString(IapEvent value) => _toString[value]; internal static IapEvent FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown IapEvent value: {value}"); + + public bool TryReadRaw(string value, out IapEvent result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(IapEvent value) => _toString[value]; } public static class IapEventExtensions @@ -997,7 +1172,7 @@ public enum IapkitClientPayloadFormat Text } -public sealed class IapkitClientPayloadFormatJsonConverter : JsonConverter +public sealed class IapkitClientPayloadFormatJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1034,6 +1209,11 @@ public override void Write(Utf8JsonWriter writer, IapkitClientPayloadFormat valu internal static string ToRawString(IapkitClientPayloadFormat value) => _toString[value]; internal static IapkitClientPayloadFormat FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown IapkitClientPayloadFormat value: {value}"); + + public bool TryReadRaw(string value, out IapkitClientPayloadFormat result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(IapkitClientPayloadFormat value) => _toString[value]; } public static class IapkitClientPayloadFormatExtensions @@ -1066,7 +1246,7 @@ public enum IapkitPurchaseState Inauthentic } -public sealed class IapkitPurchaseStateJsonConverter : JsonConverter +public sealed class IapkitPurchaseStateJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1118,6 +1298,11 @@ public override void Write(Utf8JsonWriter writer, IapkitPurchaseState value, Jso internal static string ToRawString(IapkitPurchaseState value) => _toString[value]; internal static IapkitPurchaseState FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : IapkitPurchaseState.Unknown; + + public bool TryReadRaw(string value, out IapkitPurchaseState result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(IapkitPurchaseState value) => _toString[value]; } public static class IapkitPurchaseStateExtensions @@ -1133,7 +1318,7 @@ public enum IapPlatform Android } -public sealed class IapPlatformJsonConverter : JsonConverter +public sealed class IapPlatformJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1165,6 +1350,11 @@ public override void Write(Utf8JsonWriter writer, IapPlatform value, JsonSeriali internal static string ToRawString(IapPlatform value) => _toString[value]; internal static IapPlatform FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown IapPlatform value: {value}"); + + public bool TryReadRaw(string value, out IapPlatform result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(IapPlatform value) => _toString[value]; } public static class IapPlatformExtensions @@ -1183,7 +1373,7 @@ public enum IapStore Amazon } -public sealed class IapStoreJsonConverter : JsonConverter +public sealed class IapStoreJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1228,6 +1418,11 @@ public override void Write(Utf8JsonWriter writer, IapStore value, JsonSerializer internal static string ToRawString(IapStore value) => _toString[value]; internal static IapStore FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : IapStore.Unknown; + + public bool TryReadRaw(string value, out IapStore result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(IapStore value) => _toString[value]; } public static class IapStoreExtensions @@ -1250,7 +1445,7 @@ public enum InAppMessageCategoryAndroid Transactional } -public sealed class InAppMessageCategoryAndroidJsonConverter : JsonConverter +public sealed class InAppMessageCategoryAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1281,6 +1476,11 @@ public override void Write(Utf8JsonWriter writer, InAppMessageCategoryAndroid va internal static string ToRawString(InAppMessageCategoryAndroid value) => _toString[value]; internal static InAppMessageCategoryAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; + + public bool TryReadRaw(string value, out InAppMessageCategoryAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(InAppMessageCategoryAndroid value) => _toString[value]; } public static class InAppMessageCategoryAndroidExtensions @@ -1303,7 +1503,7 @@ public enum InAppMessageResponseCodeAndroid SubscriptionStatusUpdated } -public sealed class InAppMessageResponseCodeAndroidJsonConverter : JsonConverter +public sealed class InAppMessageResponseCodeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1334,6 +1534,11 @@ public override void Write(Utf8JsonWriter writer, InAppMessageResponseCodeAndroi internal static string ToRawString(InAppMessageResponseCodeAndroid value) => _toString[value]; internal static InAppMessageResponseCodeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown InAppMessageResponseCodeAndroid value: {value}"); + + public bool TryReadRaw(string value, out InAppMessageResponseCodeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(InAppMessageResponseCodeAndroid value) => _toString[value]; } public static class InAppMessageResponseCodeAndroidExtensions @@ -1359,7 +1564,7 @@ public enum PaymentMode Unknown } -public sealed class PaymentModeJsonConverter : JsonConverter +public sealed class PaymentModeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1400,6 +1605,11 @@ public override void Write(Utf8JsonWriter writer, PaymentMode value, JsonSeriali internal static string ToRawString(PaymentMode value) => _toString[value]; internal static PaymentMode FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : PaymentMode.Unknown; + + public bool TryReadRaw(string value, out PaymentMode result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(PaymentMode value) => _toString[value]; } public static class PaymentModeExtensions @@ -1417,7 +1627,7 @@ public enum PaymentModeIOS PayUpFront } -public sealed class PaymentModeIOSJsonConverter : JsonConverter +public sealed class PaymentModeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1458,6 +1668,11 @@ public override void Write(Utf8JsonWriter writer, PaymentModeIOS value, JsonSeri internal static string ToRawString(PaymentModeIOS value) => _toString[value]; internal static PaymentModeIOS FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown PaymentModeIOS value: {value}"); + + public bool TryReadRaw(string value, out PaymentModeIOS result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(PaymentModeIOS value) => _toString[value]; } public static class PaymentModeIOSExtensions @@ -1474,7 +1689,7 @@ public enum ProductQueryType All } -public sealed class ProductQueryTypeJsonConverter : JsonConverter +public sealed class ProductQueryTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1511,6 +1726,11 @@ public override void Write(Utf8JsonWriter writer, ProductQueryType value, JsonSe internal static string ToRawString(ProductQueryType value) => _toString[value]; internal static ProductQueryType FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ProductQueryType value: {value}"); + + public bool TryReadRaw(string value, out ProductQueryType result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ProductQueryType value) => _toString[value]; } public static class ProductQueryTypeExtensions @@ -1538,7 +1758,7 @@ public enum ProductStatusAndroid Unknown } -public sealed class ProductStatusAndroidJsonConverter : JsonConverter +public sealed class ProductStatusAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1575,6 +1795,11 @@ public override void Write(Utf8JsonWriter writer, ProductStatusAndroid value, Js internal static string ToRawString(ProductStatusAndroid value) => _toString[value]; internal static ProductStatusAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : ProductStatusAndroid.Unknown; + + public bool TryReadRaw(string value, out ProductStatusAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ProductStatusAndroid value) => _toString[value]; } public static class ProductStatusAndroidExtensions @@ -1590,7 +1815,7 @@ public enum ProductType Subs } -public sealed class ProductTypeJsonConverter : JsonConverter +public sealed class ProductTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1623,6 +1848,11 @@ public override void Write(Utf8JsonWriter writer, ProductType value, JsonSeriali internal static string ToRawString(ProductType value) => _toString[value]; internal static ProductType FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ProductType value: {value}"); + + public bool TryReadRaw(string value, out ProductType result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ProductType value) => _toString[value]; } public static class ProductTypeExtensions @@ -1644,7 +1874,7 @@ public enum ProductTypeIOS SubscriptionSuite } -public sealed class ProductTypeIOSJsonConverter : JsonConverter +public sealed class ProductTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1693,6 +1923,11 @@ public override void Write(Utf8JsonWriter writer, ProductTypeIOS value, JsonSeri internal static string ToRawString(ProductTypeIOS value) => _toString[value]; internal static ProductTypeIOS FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown ProductTypeIOS value: {value}"); + + public bool TryReadRaw(string value, out ProductTypeIOS result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(ProductTypeIOS value) => _toString[value]; } public static class ProductTypeIOSExtensions @@ -1709,7 +1944,7 @@ public enum PurchaseState Unknown } -public sealed class PurchaseStateJsonConverter : JsonConverter +public sealed class PurchaseStateJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1746,6 +1981,11 @@ public override void Write(Utf8JsonWriter writer, PurchaseState value, JsonSeria internal static string ToRawString(PurchaseState value) => _toString[value]; internal static PurchaseState FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : PurchaseState.Unknown; + + public bool TryReadRaw(string value, out PurchaseState result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(PurchaseState value) => _toString[value]; } public static class PurchaseStateExtensions @@ -1760,7 +2000,7 @@ public enum PurchaseVerificationProvider Iapkit } -public sealed class PurchaseVerificationProviderJsonConverter : JsonConverter +public sealed class PurchaseVerificationProviderJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1789,6 +2029,11 @@ public override void Write(Utf8JsonWriter writer, PurchaseVerificationProvider v internal static string ToRawString(PurchaseVerificationProvider value) => _toString[value]; internal static PurchaseVerificationProvider FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown PurchaseVerificationProvider value: {value}"); + + public bool TryReadRaw(string value, out PurchaseVerificationProvider result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(PurchaseVerificationProvider value) => _toString[value]; } public static class PurchaseVerificationProviderExtensions @@ -1812,7 +2057,7 @@ public enum SubResponseCodeAndroid UserIneligible } -public sealed class SubResponseCodeAndroidJsonConverter : JsonConverter +public sealed class SubResponseCodeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1846,6 +2091,11 @@ public override void Write(Utf8JsonWriter writer, SubResponseCodeAndroid value, internal static string ToRawString(SubResponseCodeAndroid value) => _toString[value]; internal static SubResponseCodeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubResponseCodeAndroid value: {value}"); + + public bool TryReadRaw(string value, out SubResponseCodeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubResponseCodeAndroid value) => _toString[value]; } public static class SubResponseCodeAndroidExtensions @@ -1865,7 +2115,7 @@ public enum SubscriptionBillingPlanTypeIOS UpFront } -public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter +public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1902,6 +2152,11 @@ public override void Write(Utf8JsonWriter writer, SubscriptionBillingPlanTypeIOS internal static string ToRawString(SubscriptionBillingPlanTypeIOS value) => _toString[value]; internal static SubscriptionBillingPlanTypeIOS FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : SubscriptionBillingPlanTypeIOS.Unknown; + + public bool TryReadRaw(string value, out SubscriptionBillingPlanTypeIOS result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubscriptionBillingPlanTypeIOS value) => _toString[value]; } public static class SubscriptionBillingPlanTypeIOSExtensions @@ -1922,7 +2177,7 @@ public enum SubscriptionOfferTypeIOS WinBack } -public sealed class SubscriptionOfferTypeIOSJsonConverter : JsonConverter +public sealed class SubscriptionOfferTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1959,6 +2214,11 @@ public override void Write(Utf8JsonWriter writer, SubscriptionOfferTypeIOS value internal static string ToRawString(SubscriptionOfferTypeIOS value) => _toString[value]; internal static SubscriptionOfferTypeIOS FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionOfferTypeIOS value: {value}"); + + public bool TryReadRaw(string value, out SubscriptionOfferTypeIOS result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubscriptionOfferTypeIOS value) => _toString[value]; } public static class SubscriptionOfferTypeIOSExtensions @@ -1977,7 +2237,7 @@ public enum SubscriptionPeriodIOS Empty } -public sealed class SubscriptionPeriodIOSJsonConverter : JsonConverter +public sealed class SubscriptionPeriodIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2022,6 +2282,11 @@ public override void Write(Utf8JsonWriter writer, SubscriptionPeriodIOS value, J internal static string ToRawString(SubscriptionPeriodIOS value) => _toString[value]; internal static SubscriptionPeriodIOS FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionPeriodIOS value: {value}"); + + public bool TryReadRaw(string value, out SubscriptionPeriodIOS result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubscriptionPeriodIOS value) => _toString[value]; } public static class SubscriptionPeriodIOSExtensions @@ -2041,7 +2306,7 @@ public enum SubscriptionPeriodUnit Unknown } -public sealed class SubscriptionPeriodUnitJsonConverter : JsonConverter +public sealed class SubscriptionPeriodUnitJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2086,6 +2351,11 @@ public override void Write(Utf8JsonWriter writer, SubscriptionPeriodUnit value, internal static string ToRawString(SubscriptionPeriodUnit value) => _toString[value]; internal static SubscriptionPeriodUnit FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : SubscriptionPeriodUnit.Unknown; + + public bool TryReadRaw(string value, out SubscriptionPeriodUnit result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubscriptionPeriodUnit value) => _toString[value]; } public static class SubscriptionPeriodUnitExtensions @@ -2118,7 +2388,7 @@ public enum SubscriptionReplacementModeAndroid KeepExisting } -public sealed class SubscriptionReplacementModeAndroidJsonConverter : JsonConverter +public sealed class SubscriptionReplacementModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2164,6 +2434,11 @@ public override void Write(Utf8JsonWriter writer, SubscriptionReplacementModeAnd internal static string ToRawString(SubscriptionReplacementModeAndroid value) => _toString[value]; internal static SubscriptionReplacementModeAndroid FromRawString(string value) => _fromString.TryGetValue(value, out var v) ? v : SubscriptionReplacementModeAndroid.UnknownReplacementMode; + + public bool TryReadRaw(string value, out SubscriptionReplacementModeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubscriptionReplacementModeAndroid value) => _toString[value]; } public static class SubscriptionReplacementModeAndroidExtensions @@ -3936,6 +4211,7 @@ public sealed record BillingProgramInformationDialogParamsAndroid { /// Billing program. Currently only BILLING_CHOICE is supported. [JsonPropertyName("billingProgram")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public BillingProgramAndroid BillingProgram { get; init; } = global::OpenIap.BillingProgramAndroid.BillingChoice; /// External transaction token returned by the Billing Choice reporting-details flow. [JsonPropertyName("externalTransactionToken")] @@ -3963,6 +4239,7 @@ public sealed record DeveloperBillingOptionParamsAndroid { /// The billing program. Use EXTERNAL_PAYMENTS or BILLING_CHOICE. [JsonPropertyName("billingProgram")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public required BillingProgramAndroid BillingProgram { get; init; } /// /// The URI where the external payment will be processed. @@ -3975,6 +4252,7 @@ public sealed record DeveloperBillingOptionParamsAndroid /// Required only when the selected billing program links outside the app. /// [JsonPropertyName("launchMode")] + [JsonConverter(typeof(StrictNullableEnumJsonConverter))] public DeveloperBillingLaunchModeAndroid? LaunchMode { get; init; } /// /// A pre-generated external transaction token for a Billing Choice external-link @@ -4011,9 +4289,11 @@ public sealed record GetBillingChoiceInfoParamsAndroid { /// Billing program. Currently only BILLING_CHOICE is supported. [JsonPropertyName("billingProgram")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public BillingProgramAndroid BillingProgram { get; init; } = global::OpenIap.BillingProgramAndroid.BillingChoice; /// Desired Play Billing choice image layout. [JsonPropertyName("playBillingChoiceImageLayout")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public BillingChoiceImageLayoutAndroid PlayBillingChoiceImageLayout { get; init; } = global::OpenIap.BillingChoiceImageLayoutAndroid.RectangularFourByOne; /// BCP 47 locale tag. If omitted, Play Billing uses the user's default locale. [JsonPropertyName("userLocale")] @@ -4029,6 +4309,7 @@ public sealed record InAppMessageParamsAndroid { /// In-app message categories to show. Defaults to transactional messages. [JsonPropertyName("categories")] + [JsonConverter(typeof(StrictEnumListJsonConverter))] public IReadOnlyList? Categories { get; init; } = new List { global::OpenIap.InAppMessageCategoryAndroid.Transactional }; } @@ -4046,6 +4327,7 @@ public sealed record InitConnectionConfig /// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+) /// [JsonPropertyName("enableBillingProgramAndroid")] + [JsonConverter(typeof(StrictNullableEnumJsonConverter))] public BillingProgramAndroid? EnableBillingProgramAndroid { get; init; } /// /// Billing Choice renderer configured in Play Console. Available in OpenIAP @@ -4057,6 +4339,7 @@ public sealed record InitConnectionConfig /// Defaults to GOOGLE_RENDERED. /// [JsonPropertyName("billingChoiceScreenTypeAndroid")] + [JsonConverter(typeof(StrictNullableEnumJsonConverter))] public BillingChoiceScreenTypeAndroid? BillingChoiceScreenTypeAndroid { get; init; } = global::OpenIap.BillingChoiceScreenTypeAndroid.GoogleRendered; } @@ -4070,12 +4353,15 @@ public sealed record LaunchExternalLinkParamsAndroid { /// The billing program (EXTERNAL_CONTENT_LINK, EXTERNAL_OFFER, or BILLING_CHOICE) [JsonPropertyName("billingProgram")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public required BillingProgramAndroid BillingProgram { get; init; } /// The external link launch mode [JsonPropertyName("launchMode")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public required ExternalLinkLaunchModeAndroid LaunchMode { get; init; } /// The type of the external link [JsonPropertyName("linkType")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public required ExternalLinkTypeAndroid LinkType { get; init; } /// The URI where the content will be accessed from [JsonPropertyName("linkUri")] @@ -4094,6 +4380,7 @@ public sealed record ProductRequest [JsonPropertyName("skus")] public required IReadOnlyList Skus { get; init; } [JsonPropertyName("type")] + [JsonConverter(typeof(StrictNullableEnumJsonConverter))] public ProductQueryType? Type { get; init; } = global::OpenIap.ProductQueryType.InApp; } @@ -4225,6 +4512,7 @@ public sealed record RequestPurchaseProps : IJsonOnDeserialized /// Explicit purchase type hint (defaults to in-app) [JsonPropertyName("type")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public required ProductQueryType Type { get; init; } public void Validate() @@ -4345,6 +4633,7 @@ public sealed record RequestSubscriptionIosProps /// monthly billing with a 12-month commitment (iOS 26.4+). /// [JsonPropertyName("billingPlanType")] + [JsonConverter(typeof(StrictNullableEnumJsonConverter))] public SubscriptionBillingPlanTypeIOS? BillingPlanType { get; init; } /// /// Compact JWS string for overriding introductory offer eligibility @@ -4467,6 +4756,7 @@ public sealed record SubscriptionProductReplacementParamsAndroid public required string OldProductId { get; init; } /// The replacement mode for this product change [JsonPropertyName("replacementMode")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public required SubscriptionReplacementModeAndroid ReplacementMode { get; init; } } @@ -4558,6 +4848,7 @@ public sealed record VerifyPurchaseProps public sealed record VerifyPurchaseWithProviderProps { [JsonPropertyName("provider")] + [JsonConverter(typeof(StrictEnumJsonConverter))] public required PurchaseVerificationProvider Provider { get; init; } [JsonPropertyName("iapkit")] public RequestVerifyPurchaseWithIapkitProps? Iapkit { get; init; } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 7a79564c1..35a105805 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -27,15 +27,20 @@ public enum class BillingChoiceImageLayoutAndroid(val rawValue: String) { RectangularTwoByTwo("rectangular-two-by-two") companion object { - fun fromJson(value: String): BillingChoiceImageLayoutAndroid = when (value) { + private fun fromKnownJson(value: String): BillingChoiceImageLayoutAndroid? = when (value) { "rectangular-four-by-one" -> BillingChoiceImageLayoutAndroid.RectangularFourByOne "RECTANGULAR_FOUR_BY_ONE" -> BillingChoiceImageLayoutAndroid.RectangularFourByOne "rectangular-three-by-one" -> BillingChoiceImageLayoutAndroid.RectangularThreeByOne "RECTANGULAR_THREE_BY_ONE" -> BillingChoiceImageLayoutAndroid.RectangularThreeByOne "rectangular-two-by-two" -> BillingChoiceImageLayoutAndroid.RectangularTwoByTwo "RECTANGULAR_TWO_BY_TWO" -> BillingChoiceImageLayoutAndroid.RectangularTwoByTwo - else -> throw IllegalArgumentException("Unknown BillingChoiceImageLayoutAndroid value: $value") + else -> null } + + fun fromJson(value: String): BillingChoiceImageLayoutAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceImageLayoutAndroid value: $value") + + fun fromJsonStrict(value: String): BillingChoiceImageLayoutAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceImageLayoutAndroid input value: $value") } fun toJson(): String = rawValue @@ -60,15 +65,20 @@ public enum class BillingChoiceScreenTypeAndroid(val rawValue: String) { GoogleRendered("google-rendered") companion object { - fun fromJson(value: String): BillingChoiceScreenTypeAndroid = when (value) { + private fun fromKnownJson(value: String): BillingChoiceScreenTypeAndroid? = when (value) { "unspecified" -> BillingChoiceScreenTypeAndroid.Unspecified "UNSPECIFIED" -> BillingChoiceScreenTypeAndroid.Unspecified "developer-rendered" -> BillingChoiceScreenTypeAndroid.DeveloperRendered "DEVELOPER_RENDERED" -> BillingChoiceScreenTypeAndroid.DeveloperRendered "google-rendered" -> BillingChoiceScreenTypeAndroid.GoogleRendered "GOOGLE_RENDERED" -> BillingChoiceScreenTypeAndroid.GoogleRendered - else -> throw IllegalArgumentException("Unknown BillingChoiceScreenTypeAndroid value: $value") + else -> null } + + fun fromJson(value: String): BillingChoiceScreenTypeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceScreenTypeAndroid value: $value") + + fun fromJsonStrict(value: String): BillingChoiceScreenTypeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingChoiceScreenTypeAndroid input value: $value") } fun toJson(): String = rawValue @@ -119,7 +129,7 @@ public enum class BillingProgramAndroid(val rawValue: String) { BillingChoice("billing-choice") companion object { - fun fromJson(value: String): BillingProgramAndroid = when (value) { + private fun fromKnownJson(value: String): BillingProgramAndroid? = when (value) { "unspecified" -> BillingProgramAndroid.Unspecified "UNSPECIFIED" -> BillingProgramAndroid.Unspecified "user-choice-billing" -> BillingProgramAndroid.UserChoiceBilling @@ -132,8 +142,13 @@ public enum class BillingProgramAndroid(val rawValue: String) { "EXTERNAL_PAYMENTS" -> BillingProgramAndroid.ExternalPayments "billing-choice" -> BillingProgramAndroid.BillingChoice "BILLING_CHOICE" -> BillingProgramAndroid.BillingChoice - else -> throw IllegalArgumentException("Unknown BillingProgramAndroid value: $value") + else -> null } + + fun fromJson(value: String): BillingProgramAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingProgramAndroid value: $value") + + fun fromJsonStrict(value: String): BillingProgramAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown BillingProgramAndroid input value: $value") } fun toJson(): String = rawValue @@ -161,15 +176,20 @@ public enum class DeveloperBillingLaunchModeAndroid(val rawValue: String) { CallerWillLaunchLink("caller-will-launch-link") companion object { - fun fromJson(value: String): DeveloperBillingLaunchModeAndroid = when (value) { + private fun fromKnownJson(value: String): DeveloperBillingLaunchModeAndroid? = when (value) { "unspecified" -> DeveloperBillingLaunchModeAndroid.Unspecified "UNSPECIFIED" -> DeveloperBillingLaunchModeAndroid.Unspecified "launch-in-external-browser-or-app" -> DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp "LAUNCH_IN_EXTERNAL_BROWSER_OR_APP" -> DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp "caller-will-launch-link" -> DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink "CALLER_WILL_LAUNCH_LINK" -> DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink - else -> throw IllegalArgumentException("Unknown DeveloperBillingLaunchModeAndroid value: $value") + else -> null } + + fun fromJson(value: String): DeveloperBillingLaunchModeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingLaunchModeAndroid value: $value") + + fun fromJsonStrict(value: String): DeveloperBillingLaunchModeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingLaunchModeAndroid input value: $value") } fun toJson(): String = rawValue @@ -194,15 +214,20 @@ public enum class DeveloperBillingTypeAndroid(val rawValue: String) { ExternalLink("external-link") companion object { - fun fromJson(value: String): DeveloperBillingTypeAndroid = when (value) { + private fun fromKnownJson(value: String): DeveloperBillingTypeAndroid? = when (value) { "developer-billing-type-unspecified" -> DeveloperBillingTypeAndroid.DeveloperBillingTypeUnspecified "DEVELOPER_BILLING_TYPE_UNSPECIFIED" -> DeveloperBillingTypeAndroid.DeveloperBillingTypeUnspecified "in-app" -> DeveloperBillingTypeAndroid.InApp "IN_APP" -> DeveloperBillingTypeAndroid.InApp "external-link" -> DeveloperBillingTypeAndroid.ExternalLink "EXTERNAL_LINK" -> DeveloperBillingTypeAndroid.ExternalLink - else -> throw IllegalArgumentException("Unknown DeveloperBillingTypeAndroid value: $value") + else -> null } + + fun fromJson(value: String): DeveloperBillingTypeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingTypeAndroid value: $value") + + fun fromJsonStrict(value: String): DeveloperBillingTypeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DeveloperBillingTypeAndroid input value: $value") } fun toJson(): String = rawValue @@ -227,7 +252,7 @@ public enum class DiscountOfferType(val rawValue: String) { OneTime("one-time") companion object { - fun fromJson(value: String): DiscountOfferType = when (value) { + private fun fromKnownJson(value: String): DiscountOfferType? = when (value) { "introductory" -> DiscountOfferType.Introductory "INTRODUCTORY" -> DiscountOfferType.Introductory "Introductory" -> DiscountOfferType.Introductory @@ -237,8 +262,13 @@ public enum class DiscountOfferType(val rawValue: String) { "one-time" -> DiscountOfferType.OneTime "ONE_TIME" -> DiscountOfferType.OneTime "OneTime" -> DiscountOfferType.OneTime - else -> throw IllegalArgumentException("Unknown DiscountOfferType value: $value") + else -> null } + + fun fromJson(value: String): DiscountOfferType = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DiscountOfferType value: $value") + + fun fromJsonStrict(value: String): DiscountOfferType = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown DiscountOfferType input value: $value") } fun toJson(): String = rawValue @@ -283,7 +313,7 @@ public enum class ErrorCode(val rawValue: String) { DuplicatePurchase("duplicate-purchase") companion object { - fun fromJson(value: String): ErrorCode = when (value) { + private fun fromKnownJson(value: String): ErrorCode? = when (value) { "unknown" -> ErrorCode.Unknown "UNKNOWN" -> ErrorCode.Unknown "Unknown" -> ErrorCode.Unknown @@ -392,8 +422,13 @@ public enum class ErrorCode(val rawValue: String) { "duplicate-purchase" -> ErrorCode.DuplicatePurchase "DUPLICATE_PURCHASE" -> ErrorCode.DuplicatePurchase "DuplicatePurchase" -> ErrorCode.DuplicatePurchase - else -> ErrorCode.Unknown + else -> null } + + fun fromJson(value: String): ErrorCode = fromKnownJson(value) ?: ErrorCode.Unknown + + fun fromJsonStrict(value: String): ErrorCode = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ErrorCode input value: $value") } fun toJson(): String = rawValue @@ -420,15 +455,20 @@ public enum class ExternalLinkLaunchModeAndroid(val rawValue: String) { CallerWillLaunchLink("caller-will-launch-link") companion object { - fun fromJson(value: String): ExternalLinkLaunchModeAndroid = when (value) { + private fun fromKnownJson(value: String): ExternalLinkLaunchModeAndroid? = when (value) { "unspecified" -> ExternalLinkLaunchModeAndroid.Unspecified "UNSPECIFIED" -> ExternalLinkLaunchModeAndroid.Unspecified "launch-in-external-browser-or-app" -> ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp "LAUNCH_IN_EXTERNAL_BROWSER_OR_APP" -> ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp "caller-will-launch-link" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink "CALLER_WILL_LAUNCH_LINK" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink - else -> throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid value: $value") + else -> null } + + fun fromJson(value: String): ExternalLinkLaunchModeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid value: $value") + + fun fromJsonStrict(value: String): ExternalLinkLaunchModeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid input value: $value") } fun toJson(): String = rawValue @@ -454,15 +494,20 @@ public enum class ExternalLinkTypeAndroid(val rawValue: String) { LinkToAppDownload("link-to-app-download") companion object { - fun fromJson(value: String): ExternalLinkTypeAndroid = when (value) { + private fun fromKnownJson(value: String): ExternalLinkTypeAndroid? = when (value) { "unspecified" -> ExternalLinkTypeAndroid.Unspecified "UNSPECIFIED" -> ExternalLinkTypeAndroid.Unspecified "link-to-digital-content-offer" -> ExternalLinkTypeAndroid.LinkToDigitalContentOffer "LINK_TO_DIGITAL_CONTENT_OFFER" -> ExternalLinkTypeAndroid.LinkToDigitalContentOffer "link-to-app-download" -> ExternalLinkTypeAndroid.LinkToAppDownload "LINK_TO_APP_DOWNLOAD" -> ExternalLinkTypeAndroid.LinkToAppDownload - else -> throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid value: $value") + else -> null } + + fun fromJson(value: String): ExternalLinkTypeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid value: $value") + + fun fromJsonStrict(value: String): ExternalLinkTypeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid input value: $value") } fun toJson(): String = rawValue @@ -481,12 +526,17 @@ public enum class ExternalPurchaseCustomLinkNoticeTypeIOS(val rawValue: String) Browser("browser") companion object { - fun fromJson(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS = when (value) { + private fun fromKnownJson(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS? = when (value) { "browser" -> ExternalPurchaseCustomLinkNoticeTypeIOS.Browser "BROWSER" -> ExternalPurchaseCustomLinkNoticeTypeIOS.Browser "Browser" -> ExternalPurchaseCustomLinkNoticeTypeIOS.Browser - else -> throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value") + + fun fromJsonStrict(value: String): ExternalPurchaseCustomLinkNoticeTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkNoticeTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -510,15 +560,20 @@ public enum class ExternalPurchaseCustomLinkTokenTypeIOS(val rawValue: String) { Services("services") companion object { - fun fromJson(value: String): ExternalPurchaseCustomLinkTokenTypeIOS = when (value) { + private fun fromKnownJson(value: String): ExternalPurchaseCustomLinkTokenTypeIOS? = when (value) { "acquisition" -> ExternalPurchaseCustomLinkTokenTypeIOS.Acquisition "ACQUISITION" -> ExternalPurchaseCustomLinkTokenTypeIOS.Acquisition "Acquisition" -> ExternalPurchaseCustomLinkTokenTypeIOS.Acquisition "services" -> ExternalPurchaseCustomLinkTokenTypeIOS.Services "SERVICES" -> ExternalPurchaseCustomLinkTokenTypeIOS.Services "Services" -> ExternalPurchaseCustomLinkTokenTypeIOS.Services - else -> throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): ExternalPurchaseCustomLinkTokenTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value") + + fun fromJsonStrict(value: String): ExternalPurchaseCustomLinkTokenTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseCustomLinkTokenTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -538,15 +593,20 @@ public enum class ExternalPurchaseNoticeAction(val rawValue: String) { Dismissed("dismissed") companion object { - fun fromJson(value: String): ExternalPurchaseNoticeAction = when (value) { + private fun fromKnownJson(value: String): ExternalPurchaseNoticeAction? = when (value) { "continue" -> ExternalPurchaseNoticeAction.Continue "CONTINUE" -> ExternalPurchaseNoticeAction.Continue "Continue" -> ExternalPurchaseNoticeAction.Continue "dismissed" -> ExternalPurchaseNoticeAction.Dismissed "DISMISSED" -> ExternalPurchaseNoticeAction.Dismissed "Dismissed" -> ExternalPurchaseNoticeAction.Dismissed - else -> throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction value: $value") + else -> null } + + fun fromJson(value: String): ExternalPurchaseNoticeAction = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction value: $value") + + fun fromJsonStrict(value: String): ExternalPurchaseNoticeAction = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction input value: $value") } fun toJson(): String = rawValue @@ -574,7 +634,7 @@ public enum class IapEvent(val rawValue: String) { SubscriptionBillingIssue("subscription-billing-issue") companion object { - fun fromJson(value: String): IapEvent = when (value) { + private fun fromKnownJson(value: String): IapEvent? = when (value) { "purchase-updated" -> IapEvent.PurchaseUpdated "PURCHASE_UPDATED" -> IapEvent.PurchaseUpdated "PurchaseUpdated" -> IapEvent.PurchaseUpdated @@ -593,8 +653,13 @@ public enum class IapEvent(val rawValue: String) { "subscription-billing-issue" -> IapEvent.SubscriptionBillingIssue "SUBSCRIPTION_BILLING_ISSUE" -> IapEvent.SubscriptionBillingIssue "SubscriptionBillingIssue" -> IapEvent.SubscriptionBillingIssue - else -> throw IllegalArgumentException("Unknown IapEvent value: $value") + else -> null } + + fun fromJson(value: String): IapEvent = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapEvent value: $value") + + fun fromJsonStrict(value: String): IapEvent = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapEvent input value: $value") } fun toJson(): String = rawValue @@ -609,7 +674,7 @@ public enum class IapkitClientPayloadFormat(val rawValue: String) { Text("text") companion object { - fun fromJson(value: String): IapkitClientPayloadFormat = when (value) { + private fun fromKnownJson(value: String): IapkitClientPayloadFormat? = when (value) { "toml" -> IapkitClientPayloadFormat.Toml "TOML" -> IapkitClientPayloadFormat.Toml "Toml" -> IapkitClientPayloadFormat.Toml @@ -619,8 +684,13 @@ public enum class IapkitClientPayloadFormat(val rawValue: String) { "text" -> IapkitClientPayloadFormat.Text "TEXT" -> IapkitClientPayloadFormat.Text "Text" -> IapkitClientPayloadFormat.Text - else -> throw IllegalArgumentException("Unknown IapkitClientPayloadFormat value: $value") + else -> null } + + fun fromJson(value: String): IapkitClientPayloadFormat = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapkitClientPayloadFormat value: $value") + + fun fromJsonStrict(value: String): IapkitClientPayloadFormat = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapkitClientPayloadFormat input value: $value") } fun toJson(): String = rawValue @@ -668,7 +738,7 @@ public enum class IapkitPurchaseState(val rawValue: String) { Inauthentic("inauthentic") companion object { - fun fromJson(value: String): IapkitPurchaseState = when (value) { + private fun fromKnownJson(value: String): IapkitPurchaseState? = when (value) { "entitled" -> IapkitPurchaseState.Entitled "ENTITLED" -> IapkitPurchaseState.Entitled "pending-acknowledgment" -> IapkitPurchaseState.PendingAcknowledgment @@ -687,8 +757,13 @@ public enum class IapkitPurchaseState(val rawValue: String) { "UNKNOWN" -> IapkitPurchaseState.Unknown "inauthentic" -> IapkitPurchaseState.Inauthentic "INAUTHENTIC" -> IapkitPurchaseState.Inauthentic - else -> IapkitPurchaseState.Unknown + else -> null } + + fun fromJson(value: String): IapkitPurchaseState = fromKnownJson(value) ?: IapkitPurchaseState.Unknown + + fun fromJsonStrict(value: String): IapkitPurchaseState = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapkitPurchaseState input value: $value") } fun toJson(): String = rawValue @@ -699,14 +774,19 @@ public enum class IapPlatform(val rawValue: String) { Android("android") companion object { - fun fromJson(value: String): IapPlatform = when (value) { + private fun fromKnownJson(value: String): IapPlatform? = when (value) { "ios" -> IapPlatform.Ios "IOS" -> IapPlatform.Ios "android" -> IapPlatform.Android "ANDROID" -> IapPlatform.Android "Android" -> IapPlatform.Android - else -> throw IllegalArgumentException("Unknown IapPlatform value: $value") + else -> null } + + fun fromJson(value: String): IapPlatform = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapPlatform value: $value") + + fun fromJsonStrict(value: String): IapPlatform = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapPlatform input value: $value") } fun toJson(): String = rawValue @@ -720,7 +800,7 @@ public enum class IapStore(val rawValue: String) { Amazon("amazon") companion object { - fun fromJson(value: String): IapStore = when (value) { + private fun fromKnownJson(value: String): IapStore? = when (value) { "unknown" -> IapStore.Unknown "UNKNOWN" -> IapStore.Unknown "Unknown" -> IapStore.Unknown @@ -736,8 +816,13 @@ public enum class IapStore(val rawValue: String) { "amazon" -> IapStore.Amazon "AMAZON" -> IapStore.Amazon "Amazon" -> IapStore.Amazon - else -> IapStore.Unknown + else -> null } + + fun fromJson(value: String): IapStore = fromKnownJson(value) ?: IapStore.Unknown + + fun fromJsonStrict(value: String): IapStore = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown IapStore input value: $value") } fun toJson(): String = rawValue @@ -759,13 +844,18 @@ public enum class InAppMessageCategoryAndroid(val rawValue: String) { Transactional("transactional") companion object { - fun fromJson(value: String): InAppMessageCategoryAndroid = when (value) { + private fun fromKnownJson(value: String): InAppMessageCategoryAndroid? = when (value) { "unknown-in-app-message-category-id" -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId "UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID" -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId "transactional" -> InAppMessageCategoryAndroid.Transactional "TRANSACTIONAL" -> InAppMessageCategoryAndroid.Transactional - else -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId + else -> null } + + fun fromJson(value: String): InAppMessageCategoryAndroid = fromKnownJson(value) ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId + + fun fromJsonStrict(value: String): InAppMessageCategoryAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid input value: $value") } fun toJson(): String = rawValue @@ -787,13 +877,18 @@ public enum class InAppMessageResponseCodeAndroid(val rawValue: String) { SubscriptionStatusUpdated("subscription-status-updated") companion object { - fun fromJson(value: String): InAppMessageResponseCodeAndroid = when (value) { + private fun fromKnownJson(value: String): InAppMessageResponseCodeAndroid? = when (value) { "no-action-needed" -> InAppMessageResponseCodeAndroid.NoActionNeeded "NO_ACTION_NEEDED" -> InAppMessageResponseCodeAndroid.NoActionNeeded "subscription-status-updated" -> InAppMessageResponseCodeAndroid.SubscriptionStatusUpdated "SUBSCRIPTION_STATUS_UPDATED" -> InAppMessageResponseCodeAndroid.SubscriptionStatusUpdated - else -> throw IllegalArgumentException("Unknown InAppMessageResponseCodeAndroid value: $value") + else -> null } + + fun fromJson(value: String): InAppMessageResponseCodeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown InAppMessageResponseCodeAndroid value: $value") + + fun fromJsonStrict(value: String): InAppMessageResponseCodeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown InAppMessageResponseCodeAndroid input value: $value") } fun toJson(): String = rawValue @@ -822,7 +917,7 @@ public enum class PaymentMode(val rawValue: String) { Unknown("unknown") companion object { - fun fromJson(value: String): PaymentMode = when (value) { + private fun fromKnownJson(value: String): PaymentMode? = when (value) { "free-trial" -> PaymentMode.FreeTrial "FREE_TRIAL" -> PaymentMode.FreeTrial "FreeTrial" -> PaymentMode.FreeTrial @@ -835,8 +930,13 @@ public enum class PaymentMode(val rawValue: String) { "unknown" -> PaymentMode.Unknown "UNKNOWN" -> PaymentMode.Unknown "Unknown" -> PaymentMode.Unknown - else -> PaymentMode.Unknown + else -> null } + + fun fromJson(value: String): PaymentMode = fromKnownJson(value) ?: PaymentMode.Unknown + + fun fromJsonStrict(value: String): PaymentMode = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PaymentMode input value: $value") } fun toJson(): String = rawValue @@ -849,7 +949,7 @@ public enum class PaymentModeIOS(val rawValue: String) { PayUpFront("pay-up-front") companion object { - fun fromJson(value: String): PaymentModeIOS = when (value) { + private fun fromKnownJson(value: String): PaymentModeIOS? = when (value) { "empty" -> PaymentModeIOS.Empty "EMPTY" -> PaymentModeIOS.Empty "Empty" -> PaymentModeIOS.Empty @@ -862,8 +962,13 @@ public enum class PaymentModeIOS(val rawValue: String) { "pay-up-front" -> PaymentModeIOS.PayUpFront "PAY_UP_FRONT" -> PaymentModeIOS.PayUpFront "PayUpFront" -> PaymentModeIOS.PayUpFront - else -> throw IllegalArgumentException("Unknown PaymentModeIOS value: $value") + else -> null } + + fun fromJson(value: String): PaymentModeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PaymentModeIOS value: $value") + + fun fromJsonStrict(value: String): PaymentModeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PaymentModeIOS input value: $value") } fun toJson(): String = rawValue @@ -875,7 +980,7 @@ public enum class ProductQueryType(val rawValue: String) { All("all") companion object { - fun fromJson(value: String): ProductQueryType = when (value) { + private fun fromKnownJson(value: String): ProductQueryType? = when (value) { "in-app" -> ProductQueryType.InApp "IN_APP" -> ProductQueryType.InApp "InApp" -> ProductQueryType.InApp @@ -885,8 +990,13 @@ public enum class ProductQueryType(val rawValue: String) { "all" -> ProductQueryType.All "ALL" -> ProductQueryType.All "All" -> ProductQueryType.All - else -> throw IllegalArgumentException("Unknown ProductQueryType value: $value") + else -> null } + + fun fromJson(value: String): ProductQueryType = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductQueryType value: $value") + + fun fromJsonStrict(value: String): ProductQueryType = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductQueryType input value: $value") } fun toJson(): String = rawValue @@ -917,7 +1027,7 @@ public enum class ProductStatusAndroid(val rawValue: String) { Unknown("unknown") companion object { - fun fromJson(value: String): ProductStatusAndroid = when (value) { + private fun fromKnownJson(value: String): ProductStatusAndroid? = when (value) { "ok" -> ProductStatusAndroid.Ok "OK" -> ProductStatusAndroid.Ok "not-found" -> ProductStatusAndroid.NotFound @@ -926,8 +1036,13 @@ public enum class ProductStatusAndroid(val rawValue: String) { "NO_OFFERS_AVAILABLE" -> ProductStatusAndroid.NoOffersAvailable "unknown" -> ProductStatusAndroid.Unknown "UNKNOWN" -> ProductStatusAndroid.Unknown - else -> ProductStatusAndroid.Unknown + else -> null } + + fun fromJson(value: String): ProductStatusAndroid = fromKnownJson(value) ?: ProductStatusAndroid.Unknown + + fun fromJsonStrict(value: String): ProductStatusAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductStatusAndroid input value: $value") } fun toJson(): String = rawValue @@ -938,15 +1053,20 @@ public enum class ProductType(val rawValue: String) { Subs("subs") companion object { - fun fromJson(value: String): ProductType = when (value) { + private fun fromKnownJson(value: String): ProductType? = when (value) { "in-app" -> ProductType.InApp "IN_APP" -> ProductType.InApp "InApp" -> ProductType.InApp "subs" -> ProductType.Subs "SUBS" -> ProductType.Subs "Subs" -> ProductType.Subs - else -> throw IllegalArgumentException("Unknown ProductType value: $value") + else -> null } + + fun fromJson(value: String): ProductType = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductType value: $value") + + fun fromJsonStrict(value: String): ProductType = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductType input value: $value") } fun toJson(): String = rawValue @@ -967,7 +1087,7 @@ public enum class ProductTypeIOS(val rawValue: String) { SubscriptionSuite("subscription-suite") companion object { - fun fromJson(value: String): ProductTypeIOS = when (value) { + private fun fromKnownJson(value: String): ProductTypeIOS? = when (value) { "consumable" -> ProductTypeIOS.Consumable "CONSUMABLE" -> ProductTypeIOS.Consumable "Consumable" -> ProductTypeIOS.Consumable @@ -986,8 +1106,13 @@ public enum class ProductTypeIOS(val rawValue: String) { "subscription-suite" -> ProductTypeIOS.SubscriptionSuite "SUBSCRIPTION_SUITE" -> ProductTypeIOS.SubscriptionSuite "SubscriptionSuite" -> ProductTypeIOS.SubscriptionSuite - else -> throw IllegalArgumentException("Unknown ProductTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): ProductTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductTypeIOS value: $value") + + fun fromJsonStrict(value: String): ProductTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown ProductTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -999,7 +1124,7 @@ public enum class PurchaseState(val rawValue: String) { Unknown("unknown") companion object { - fun fromJson(value: String): PurchaseState = when (value) { + private fun fromKnownJson(value: String): PurchaseState? = when (value) { "pending" -> PurchaseState.Pending "PENDING" -> PurchaseState.Pending "Pending" -> PurchaseState.Pending @@ -1009,8 +1134,13 @@ public enum class PurchaseState(val rawValue: String) { "unknown" -> PurchaseState.Unknown "UNKNOWN" -> PurchaseState.Unknown "Unknown" -> PurchaseState.Unknown - else -> PurchaseState.Unknown + else -> null } + + fun fromJson(value: String): PurchaseState = fromKnownJson(value) ?: PurchaseState.Unknown + + fun fromJsonStrict(value: String): PurchaseState = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PurchaseState input value: $value") } fun toJson(): String = rawValue @@ -1020,12 +1150,17 @@ public enum class PurchaseVerificationProvider(val rawValue: String) { Iapkit("iapkit") companion object { - fun fromJson(value: String): PurchaseVerificationProvider = when (value) { + private fun fromKnownJson(value: String): PurchaseVerificationProvider? = when (value) { "iapkit" -> PurchaseVerificationProvider.Iapkit "IAPKIT" -> PurchaseVerificationProvider.Iapkit "Iapkit" -> PurchaseVerificationProvider.Iapkit - else -> throw IllegalArgumentException("Unknown PurchaseVerificationProvider value: $value") + else -> null } + + fun fromJson(value: String): PurchaseVerificationProvider = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PurchaseVerificationProvider value: $value") + + fun fromJsonStrict(value: String): PurchaseVerificationProvider = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown PurchaseVerificationProvider input value: $value") } fun toJson(): String = rawValue @@ -1050,15 +1185,20 @@ public enum class SubResponseCodeAndroid(val rawValue: String) { UserIneligible("user-ineligible") companion object { - fun fromJson(value: String): SubResponseCodeAndroid = when (value) { + private fun fromKnownJson(value: String): SubResponseCodeAndroid? = when (value) { "no-applicable-sub-response-code" -> SubResponseCodeAndroid.NoApplicableSubResponseCode "NO_APPLICABLE_SUB_RESPONSE_CODE" -> SubResponseCodeAndroid.NoApplicableSubResponseCode "payment-declined-due-to-insufficient-funds" -> SubResponseCodeAndroid.PaymentDeclinedDueToInsufficientFunds "PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS" -> SubResponseCodeAndroid.PaymentDeclinedDueToInsufficientFunds "user-ineligible" -> SubResponseCodeAndroid.UserIneligible "USER_INELIGIBLE" -> SubResponseCodeAndroid.UserIneligible - else -> throw IllegalArgumentException("Unknown SubResponseCodeAndroid value: $value") + else -> null } + + fun fromJson(value: String): SubResponseCodeAndroid = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubResponseCodeAndroid value: $value") + + fun fromJsonStrict(value: String): SubResponseCodeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubResponseCodeAndroid input value: $value") } fun toJson(): String = rawValue @@ -1079,7 +1219,7 @@ public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) { UpFront("up-front") companion object { - fun fromJson(value: String): SubscriptionBillingPlanTypeIOS = when (value) { + private fun fromKnownJson(value: String): SubscriptionBillingPlanTypeIOS? = when (value) { "unknown" -> SubscriptionBillingPlanTypeIOS.Unknown "UNKNOWN" -> SubscriptionBillingPlanTypeIOS.Unknown "Unknown" -> SubscriptionBillingPlanTypeIOS.Unknown @@ -1089,8 +1229,13 @@ public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) { "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront "UP_FRONT" -> SubscriptionBillingPlanTypeIOS.UpFront "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront - else -> SubscriptionBillingPlanTypeIOS.Unknown + else -> null } + + fun fromJson(value: String): SubscriptionBillingPlanTypeIOS = fromKnownJson(value) ?: SubscriptionBillingPlanTypeIOS.Unknown + + fun fromJsonStrict(value: String): SubscriptionBillingPlanTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -1106,7 +1251,7 @@ public enum class SubscriptionOfferTypeIOS(val rawValue: String) { WinBack("win-back") companion object { - fun fromJson(value: String): SubscriptionOfferTypeIOS = when (value) { + private fun fromKnownJson(value: String): SubscriptionOfferTypeIOS? = when (value) { "introductory" -> SubscriptionOfferTypeIOS.Introductory "INTRODUCTORY" -> SubscriptionOfferTypeIOS.Introductory "Introductory" -> SubscriptionOfferTypeIOS.Introductory @@ -1116,8 +1261,13 @@ public enum class SubscriptionOfferTypeIOS(val rawValue: String) { "win-back" -> SubscriptionOfferTypeIOS.WinBack "WIN_BACK" -> SubscriptionOfferTypeIOS.WinBack "WinBack" -> SubscriptionOfferTypeIOS.WinBack - else -> throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS value: $value") + else -> null } + + fun fromJson(value: String): SubscriptionOfferTypeIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS value: $value") + + fun fromJsonStrict(value: String): SubscriptionOfferTypeIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS input value: $value") } fun toJson(): String = rawValue @@ -1131,7 +1281,7 @@ public enum class SubscriptionPeriodIOS(val rawValue: String) { Empty("empty") companion object { - fun fromJson(value: String): SubscriptionPeriodIOS = when (value) { + private fun fromKnownJson(value: String): SubscriptionPeriodIOS? = when (value) { "day" -> SubscriptionPeriodIOS.Day "DAY" -> SubscriptionPeriodIOS.Day "Day" -> SubscriptionPeriodIOS.Day @@ -1147,8 +1297,13 @@ public enum class SubscriptionPeriodIOS(val rawValue: String) { "empty" -> SubscriptionPeriodIOS.Empty "EMPTY" -> SubscriptionPeriodIOS.Empty "Empty" -> SubscriptionPeriodIOS.Empty - else -> throw IllegalArgumentException("Unknown SubscriptionPeriodIOS value: $value") + else -> null } + + fun fromJson(value: String): SubscriptionPeriodIOS = fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionPeriodIOS value: $value") + + fun fromJsonStrict(value: String): SubscriptionPeriodIOS = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionPeriodIOS input value: $value") } fun toJson(): String = rawValue @@ -1165,7 +1320,7 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { Unknown("unknown") companion object { - fun fromJson(value: String): SubscriptionPeriodUnit = when (value) { + private fun fromKnownJson(value: String): SubscriptionPeriodUnit? = when (value) { "day" -> SubscriptionPeriodUnit.Day "DAY" -> SubscriptionPeriodUnit.Day "Day" -> SubscriptionPeriodUnit.Day @@ -1181,8 +1336,13 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { "unknown" -> SubscriptionPeriodUnit.Unknown "UNKNOWN" -> SubscriptionPeriodUnit.Unknown "Unknown" -> SubscriptionPeriodUnit.Unknown - else -> SubscriptionPeriodUnit.Unknown + else -> null } + + fun fromJson(value: String): SubscriptionPeriodUnit = fromKnownJson(value) ?: SubscriptionPeriodUnit.Unknown + + fun fromJsonStrict(value: String): SubscriptionPeriodUnit = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionPeriodUnit input value: $value") } fun toJson(): String = rawValue @@ -1224,7 +1384,7 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { KeepExisting("keep-existing") companion object { - fun fromJson(value: String): SubscriptionReplacementModeAndroid = when (value) { + private fun fromKnownJson(value: String): SubscriptionReplacementModeAndroid? = when (value) { "unknown-replacement-mode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode "UNKNOWN_REPLACEMENT_MODE" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode "with-time-proration" -> SubscriptionReplacementModeAndroid.WithTimeProration @@ -1239,8 +1399,13 @@ public enum class SubscriptionReplacementModeAndroid(val rawValue: String) { "DEFERRED" -> SubscriptionReplacementModeAndroid.Deferred "keep-existing" -> SubscriptionReplacementModeAndroid.KeepExisting "KEEP_EXISTING" -> SubscriptionReplacementModeAndroid.KeepExisting - else -> SubscriptionReplacementModeAndroid.UnknownReplacementMode + else -> null } + + fun fromJson(value: String): SubscriptionReplacementModeAndroid = fromKnownJson(value) ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode + + fun fromJsonStrict(value: String): SubscriptionReplacementModeAndroid = + fromKnownJson(value) ?: throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid input value: $value") } fun toJson(): String = rawValue @@ -4168,7 +4333,7 @@ public data class BillingProgramInformationDialogParamsAndroid( ) { companion object { fun fromJson(json: Map): BillingProgramInformationDialogParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.BillingChoice + val billingProgram = if (json["billingProgram"] == null) BillingProgramAndroid.BillingChoice else (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") val externalTransactionToken = json["externalTransactionToken"] as? String if (externalTransactionToken == null) return null return BillingProgramInformationDialogParamsAndroid( @@ -4197,8 +4362,8 @@ public data class DeepLinkOptions( companion object { fun fromJson(json: Map): DeepLinkOptions { return DeepLinkOptions( - packageNameAndroid = json["packageNameAndroid"] as? String, - skuAndroid = json["skuAndroid"] as? String, + packageNameAndroid = json["packageNameAndroid"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, + skuAndroid = json["skuAndroid"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, ) } } @@ -4239,10 +4404,10 @@ public data class DeveloperBillingOptionParamsAndroid( ) { companion object { fun fromJson(json: Map): DeveloperBillingOptionParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") - val externalTransactionToken = json["externalTransactionToken"] as? String - val launchMode = (json["launchMode"] as? String)?.let { DeveloperBillingLaunchModeAndroid.fromJson(it) } - val linkUri = json["linkUri"] as? String + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") + val externalTransactionToken = json["externalTransactionToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val launchMode = json["launchMode"]?.let { raw -> (raw as? String)?.let { DeveloperBillingLaunchModeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for DeveloperBillingLaunchModeAndroid") } + val linkUri = json["linkUri"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } return DeveloperBillingOptionParamsAndroid( billingProgram = billingProgram, externalTransactionToken = externalTransactionToken, @@ -4330,9 +4495,9 @@ public data class GetBillingChoiceInfoParamsAndroid( companion object { fun fromJson(json: Map): GetBillingChoiceInfoParamsAndroid { return GetBillingChoiceInfoParamsAndroid( - billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.BillingChoice, - playBillingChoiceImageLayout = (json["playBillingChoiceImageLayout"] as? String)?.let { BillingChoiceImageLayoutAndroid.fromJson(it) } ?: BillingChoiceImageLayoutAndroid.RectangularFourByOne, - userLocale = json["userLocale"] as? String, + billingProgram = if (json["billingProgram"] == null) BillingProgramAndroid.BillingChoice else (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid"), + playBillingChoiceImageLayout = if (json["playBillingChoiceImageLayout"] == null) BillingChoiceImageLayoutAndroid.RectangularFourByOne else (json["playBillingChoiceImageLayout"] as? String)?.let { BillingChoiceImageLayoutAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingChoiceImageLayoutAndroid"), + userLocale = json["userLocale"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, ) } } @@ -4358,7 +4523,7 @@ public data class InAppMessageParamsAndroid( companion object { fun fromJson(json: Map): InAppMessageParamsAndroid { return InAppMessageParamsAndroid( - categories = (json["categories"] as? List<*>)?.mapNotNull { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } ?: InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId } ?: listOf(InAppMessageCategoryAndroid.Transactional), + categories = if (json["categories"] == null) listOf(InAppMessageCategoryAndroid.Transactional) else (json["categories"] as? List<*>)?.map { (it as? String)?.let { InAppMessageCategoryAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for InAppMessageCategoryAndroid") } ?: throw IllegalArgumentException("Invalid input list"), ) } } @@ -4397,8 +4562,8 @@ public data class InitConnectionConfig( companion object { fun fromJson(json: Map): InitConnectionConfig { return InitConnectionConfig( - billingChoiceScreenTypeAndroid = (json["billingChoiceScreenTypeAndroid"] as? String)?.let { BillingChoiceScreenTypeAndroid.fromJson(it) } ?: BillingChoiceScreenTypeAndroid.GoogleRendered, - enableBillingProgramAndroid = (json["enableBillingProgramAndroid"] as? String)?.let { BillingProgramAndroid.fromJson(it) }, + billingChoiceScreenTypeAndroid = if (json["billingChoiceScreenTypeAndroid"] == null) BillingChoiceScreenTypeAndroid.GoogleRendered else (json["billingChoiceScreenTypeAndroid"] as? String)?.let { BillingChoiceScreenTypeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingChoiceScreenTypeAndroid"), + enableBillingProgramAndroid = json["enableBillingProgramAndroid"]?.let { raw -> (raw as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") }, ) } } @@ -4441,10 +4606,10 @@ public data class LaunchExternalLinkParamsAndroid( ) { companion object { fun fromJson(json: Map): LaunchExternalLinkParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for BillingProgramAndroid") - val externalTransactionToken = json["externalTransactionToken"] as? String - val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkLaunchModeAndroid") - val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for ExternalLinkTypeAndroid") + val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for BillingProgramAndroid") + val externalTransactionToken = json["externalTransactionToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val launchMode = (json["launchMode"] as? String)?.let { ExternalLinkLaunchModeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for ExternalLinkLaunchModeAndroid") + val linkType = (json["linkType"] as? String)?.let { ExternalLinkTypeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for ExternalLinkTypeAndroid") val linkUri = json["linkUri"] as? String if (linkUri == null) return null return LaunchExternalLinkParamsAndroid( @@ -4472,9 +4637,8 @@ public data class ProductRequest( ) { companion object { fun fromJson(json: Map): ProductRequest? { - val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } - val type = (json["type"] as? String)?.let { ProductQueryType.fromJson(it) } ?: ProductQueryType.InApp - if (skus == null) return null + val skus = (json["skus"] as? List<*>)?.map { it as? String ?: throw IllegalArgumentException("Invalid String input list element") } ?: throw IllegalArgumentException("Invalid input list") + val type = if (json["type"] == null) ProductQueryType.InApp else (json["type"] as? String)?.let { ProductQueryType.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for ProductQueryType") return ProductRequest( skus = skus, type = type, @@ -4546,9 +4710,9 @@ public data class PurchaseOptions( companion object { fun fromJson(json: Map): PurchaseOptions { return PurchaseOptions( - alsoPublishToEventListenerIOS = json["alsoPublishToEventListenerIOS"] as? Boolean, - includeSuspendedAndroid = json["includeSuspendedAndroid"] as? Boolean, - onlyIncludeActiveItemsIOS = json["onlyIncludeActiveItemsIOS"] as? Boolean, + alsoPublishToEventListenerIOS = json["alsoPublishToEventListenerIOS"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, + includeSuspendedAndroid = json["includeSuspendedAndroid"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, + onlyIncludeActiveItemsIOS = json["onlyIncludeActiveItemsIOS"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, ) } } @@ -4571,7 +4735,7 @@ public data class PurchaseUpdatedListenerOptions( companion object { fun fromJson(json: Map): PurchaseUpdatedListenerOptions { return PurchaseUpdatedListenerOptions( - dedupeTransactionIOS = json["dedupeTransactionIOS"] as? Boolean, + dedupeTransactionIOS = json["dedupeTransactionIOS"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, ) } } @@ -4615,12 +4779,11 @@ public data class RequestPurchaseAndroidProps( companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } - val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean - val obfuscatedAccountId = json["obfuscatedAccountId"] as? String - val obfuscatedProfileId = json["obfuscatedProfileId"] as? String - val offerToken = json["offerToken"] as? String - val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } - if (skus == null) return null + val isOfferPersonalized = json["isOfferPersonalized"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val obfuscatedAccountId = json["obfuscatedAccountId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val obfuscatedProfileId = json["obfuscatedProfileId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val offerToken = json["offerToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val skus = (json["skus"] as? List<*>)?.map { it as? String ?: throw IllegalArgumentException("Invalid String input list element") } ?: throw IllegalArgumentException("Invalid input list") return RequestPurchaseAndroidProps( developerBillingOption = developerBillingOption, isOfferPersonalized = isOfferPersonalized, @@ -4674,10 +4837,10 @@ public data class RequestPurchaseIosProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseIosProps? { - val advancedCommerceData = json["advancedCommerceData"] as? String - val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean - val appAccountToken = json["appAccountToken"] as? String - val quantity = (json["quantity"] as? Number)?.toInt() + val advancedCommerceData = json["advancedCommerceData"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val appAccountToken = json["appAccountToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val quantity = json["quantity"]?.let { raw -> ((raw as? Number)?.let { number -> number.toLong().takeIf { value -> value >= Int.MIN_VALUE.toLong() && value <= Int.MAX_VALUE.toLong() && value.toDouble() == number.toDouble() }?.toInt() }) ?: throw IllegalArgumentException("Invalid Int input value") } val sku = json["sku"] as? String val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } if (sku == null) return null @@ -4847,15 +5010,14 @@ public data class RequestSubscriptionAndroidProps( companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } - val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean - val obfuscatedAccountId = json["obfuscatedAccountId"] as? String - val obfuscatedProfileId = json["obfuscatedProfileId"] as? String - val originalExternalTransactionId = json["originalExternalTransactionId"] as? String - val purchaseToken = json["purchaseToken"] as? String - val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } - val subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } } + val isOfferPersonalized = json["isOfferPersonalized"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val obfuscatedAccountId = json["obfuscatedAccountId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val obfuscatedProfileId = json["obfuscatedProfileId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val originalExternalTransactionId = json["originalExternalTransactionId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val purchaseToken = json["purchaseToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val skus = (json["skus"] as? List<*>)?.map { it as? String ?: throw IllegalArgumentException("Invalid String input list element") } ?: throw IllegalArgumentException("Invalid input list") + val subscriptionOffers = json["subscriptionOffers"]?.let { raw -> (raw as? List<*>)?.map { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for AndroidSubscriptionOfferInput") } ?: throw IllegalArgumentException("Invalid input list") } val subscriptionProductReplacementParams = json["subscriptionProductReplacementParams"]?.let { value -> (value as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for SubscriptionProductReplacementParamsAndroid") } - if (skus == null) return null return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, isOfferPersonalized = isOfferPersonalized, @@ -4928,13 +5090,13 @@ public data class RequestSubscriptionIosProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionIosProps? { - val advancedCommerceData = json["advancedCommerceData"] as? String - val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean - val appAccountToken = json["appAccountToken"] as? String - val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } - val compactJWS = json["compactJWS"] as? String + val advancedCommerceData = json["advancedCommerceData"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val appAccountToken = json["appAccountToken"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val billingPlanType = json["billingPlanType"]?.let { raw -> (raw as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for SubscriptionBillingPlanTypeIOS") } + val compactJWS = json["compactJWS"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } val promotionalOfferJWS = json["promotionalOfferJWS"]?.let { value -> (value as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for PromotionalOfferJWSInputIOS") } - val quantity = (json["quantity"] as? Number)?.toInt() + val quantity = json["quantity"]?.let { raw -> ((raw as? Number)?.let { number -> number.toLong().takeIf { value -> value >= Int.MIN_VALUE.toLong() && value <= Int.MAX_VALUE.toLong() && value.toDouble() == number.toDouble() }?.toInt() }) ?: throw IllegalArgumentException("Invalid Int input value") } val sku = json["sku"] as? String val winBackOffer = json["winBackOffer"]?.let { value -> (value as? Map)?.let { WinBackOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for WinBackOfferInputIOS") } val withOffer = json["withOffer"]?.let { value -> (value as? Map)?.let { DiscountOfferInputIOS.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DiscountOfferInputIOS") } @@ -5040,9 +5202,9 @@ public data class RequestVerifyPurchaseWithIapkitAmazonProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitAmazonProps? { val receiptId = json["receiptId"] as? String - val sandbox = json["sandbox"] as? Boolean - val userId = json["userId"] as? String - val expectedProductId = json["expectedProductId"] as? String + val sandbox = json["sandbox"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } + val userId = json["userId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } + val expectedProductId = json["expectedProductId"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") } if (receiptId == null) return null return RequestVerifyPurchaseWithIapkitAmazonProps( receiptId = receiptId, @@ -5166,11 +5328,11 @@ public data class RequestVerifyPurchaseWithIapkitProps( fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, - apiKey = json["apiKey"] as? String, + apiKey = json["apiKey"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, apple = json["apple"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAppleProps") }, - baseUrl = json["baseUrl"] as? String, + baseUrl = json["baseUrl"]?.let { raw -> (raw as? String) ?: throw IllegalArgumentException("Invalid String input value") }, google = json["google"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitGoogleProps") }, - includeClientPayload = json["includeClientPayload"] as? Boolean, + includeClientPayload = json["includeClientPayload"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, ) } } @@ -5203,7 +5365,7 @@ public data class SubscriptionProductReplacementParamsAndroid( companion object { fun fromJson(json: Map): SubscriptionProductReplacementParamsAndroid? { val oldProductId = json["oldProductId"] as? String - val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode + val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for SubscriptionReplacementModeAndroid") if (oldProductId == null) return null return SubscriptionProductReplacementParamsAndroid( oldProductId = oldProductId, @@ -5276,7 +5438,7 @@ public data class VerifyPurchaseGoogleOptions( companion object { fun fromJson(json: Map): VerifyPurchaseGoogleOptions? { val accessToken = json["accessToken"] as? String - val isSub = json["isSub"] as? Boolean + val isSub = json["isSub"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") } val packageName = json["packageName"] as? String val purchaseToken = json["purchaseToken"] as? String val sku = json["sku"] as? String @@ -5388,7 +5550,7 @@ public data class VerifyPurchaseWithProviderProps( companion object { fun fromJson(json: Map): VerifyPurchaseWithProviderProps? { val iapkit = json["iapkit"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitProps") } - val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJson(it) } ?: throw IllegalArgumentException("Missing required enum value for PurchaseVerificationProvider") + val provider = (json["provider"] as? String)?.let { PurchaseVerificationProvider.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for PurchaseVerificationProvider") return VerifyPurchaseWithProviderProps( iapkit = iapkit, provider = provider, diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index a50a2c5f8..80592484e 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -22,7 +22,7 @@ enum BillingChoiceImageLayoutAndroid { const BillingChoiceImageLayoutAndroid(this.value); final String value; - factory BillingChoiceImageLayoutAndroid.fromJson(String value) { + static BillingChoiceImageLayoutAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'rectangular-four-by-one': @@ -32,9 +32,21 @@ enum BillingChoiceImageLayoutAndroid { case 'rectangular-two-by-two': return BillingChoiceImageLayoutAndroid.RectangularTwoByTwo; } + return null; + } + + factory BillingChoiceImageLayoutAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown BillingChoiceImageLayoutAndroid value: $value'); } + factory BillingChoiceImageLayoutAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown BillingChoiceImageLayoutAndroid input value: $value'); + } + String toJson() => value; } @@ -51,7 +63,7 @@ enum BillingChoiceScreenTypeAndroid { const BillingChoiceScreenTypeAndroid(this.value); final String value; - factory BillingChoiceScreenTypeAndroid.fromJson(String value) { + static BillingChoiceScreenTypeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unspecified': @@ -61,9 +73,21 @@ enum BillingChoiceScreenTypeAndroid { case 'google-rendered': return BillingChoiceScreenTypeAndroid.GoogleRendered; } + return null; + } + + factory BillingChoiceScreenTypeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown BillingChoiceScreenTypeAndroid value: $value'); } + factory BillingChoiceScreenTypeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown BillingChoiceScreenTypeAndroid input value: $value'); + } + String toJson() => value; } @@ -100,7 +124,7 @@ enum BillingProgramAndroid { const BillingProgramAndroid(this.value); final String value; - factory BillingProgramAndroid.fromJson(String value) { + static BillingProgramAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unspecified': @@ -116,9 +140,21 @@ enum BillingProgramAndroid { case 'billing-choice': return BillingProgramAndroid.BillingChoice; } + return null; + } + + factory BillingProgramAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown BillingProgramAndroid value: $value'); } + factory BillingProgramAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown BillingProgramAndroid input value: $value'); + } + String toJson() => value; } @@ -138,7 +174,7 @@ enum DeveloperBillingLaunchModeAndroid { const DeveloperBillingLaunchModeAndroid(this.value); final String value; - factory DeveloperBillingLaunchModeAndroid.fromJson(String value) { + static DeveloperBillingLaunchModeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unspecified': @@ -148,9 +184,21 @@ enum DeveloperBillingLaunchModeAndroid { case 'caller-will-launch-link': return DeveloperBillingLaunchModeAndroid.CallerWillLaunchLink; } + return null; + } + + factory DeveloperBillingLaunchModeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown DeveloperBillingLaunchModeAndroid value: $value'); } + factory DeveloperBillingLaunchModeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown DeveloperBillingLaunchModeAndroid input value: $value'); + } + String toJson() => value; } @@ -167,7 +215,7 @@ enum DeveloperBillingTypeAndroid { const DeveloperBillingTypeAndroid(this.value); final String value; - factory DeveloperBillingTypeAndroid.fromJson(String value) { + static DeveloperBillingTypeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'developer-billing-type-unspecified': @@ -177,9 +225,21 @@ enum DeveloperBillingTypeAndroid { case 'external-link': return DeveloperBillingTypeAndroid.ExternalLink; } + return null; + } + + factory DeveloperBillingTypeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown DeveloperBillingTypeAndroid value: $value'); } + factory DeveloperBillingTypeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown DeveloperBillingTypeAndroid input value: $value'); + } + String toJson() => value; } @@ -196,7 +256,7 @@ enum DiscountOfferType { const DiscountOfferType(this.value); final String value; - factory DiscountOfferType.fromJson(String value) { + static DiscountOfferType? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'introductory': @@ -206,9 +266,21 @@ enum DiscountOfferType { case 'one-time': return DiscountOfferType.OneTime; } + return null; + } + + factory DiscountOfferType.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown DiscountOfferType value: $value'); } + factory DiscountOfferType.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown DiscountOfferType input value: $value'); + } + String toJson() => value; } @@ -253,7 +325,7 @@ enum ErrorCode { const ErrorCode(this.value); final String value; - factory ErrorCode.fromJson(String value) { + static ErrorCode? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unknown': @@ -329,9 +401,21 @@ enum ErrorCode { case 'duplicate-purchase': return ErrorCode.DuplicatePurchase; } + return null; + } + + factory ErrorCode.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return ErrorCode.Unknown; } + factory ErrorCode.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ErrorCode input value: $value'); + } + String toJson() => value; } @@ -350,7 +434,7 @@ enum ExternalLinkLaunchModeAndroid { const ExternalLinkLaunchModeAndroid(this.value); final String value; - factory ExternalLinkLaunchModeAndroid.fromJson(String value) { + static ExternalLinkLaunchModeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unspecified': @@ -360,9 +444,21 @@ enum ExternalLinkLaunchModeAndroid { case 'caller-will-launch-link': return ExternalLinkLaunchModeAndroid.CallerWillLaunchLink; } + return null; + } + + factory ExternalLinkLaunchModeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ExternalLinkLaunchModeAndroid value: $value'); } + factory ExternalLinkLaunchModeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ExternalLinkLaunchModeAndroid input value: $value'); + } + String toJson() => value; } @@ -380,7 +476,7 @@ enum ExternalLinkTypeAndroid { const ExternalLinkTypeAndroid(this.value); final String value; - factory ExternalLinkTypeAndroid.fromJson(String value) { + static ExternalLinkTypeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unspecified': @@ -390,9 +486,21 @@ enum ExternalLinkTypeAndroid { case 'link-to-app-download': return ExternalLinkTypeAndroid.LinkToAppDownload; } + return null; + } + + factory ExternalLinkTypeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ExternalLinkTypeAndroid value: $value'); } + factory ExternalLinkTypeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ExternalLinkTypeAndroid input value: $value'); + } + String toJson() => value; } @@ -407,15 +515,27 @@ enum ExternalPurchaseCustomLinkNoticeTypeIOS { const ExternalPurchaseCustomLinkNoticeTypeIOS(this.value); final String value; - factory ExternalPurchaseCustomLinkNoticeTypeIOS.fromJson(String value) { + static ExternalPurchaseCustomLinkNoticeTypeIOS? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'browser': return ExternalPurchaseCustomLinkNoticeTypeIOS.Browser; } + return null; + } + + factory ExternalPurchaseCustomLinkNoticeTypeIOS.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ExternalPurchaseCustomLinkNoticeTypeIOS value: $value'); } + factory ExternalPurchaseCustomLinkNoticeTypeIOS.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ExternalPurchaseCustomLinkNoticeTypeIOS input value: $value'); + } + String toJson() => value; } @@ -433,7 +553,7 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { const ExternalPurchaseCustomLinkTokenTypeIOS(this.value); final String value; - factory ExternalPurchaseCustomLinkTokenTypeIOS.fromJson(String value) { + static ExternalPurchaseCustomLinkTokenTypeIOS? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'acquisition': @@ -441,9 +561,21 @@ enum ExternalPurchaseCustomLinkTokenTypeIOS { case 'services': return ExternalPurchaseCustomLinkTokenTypeIOS.Services; } + return null; + } + + factory ExternalPurchaseCustomLinkTokenTypeIOS.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ExternalPurchaseCustomLinkTokenTypeIOS value: $value'); } + factory ExternalPurchaseCustomLinkTokenTypeIOS.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ExternalPurchaseCustomLinkTokenTypeIOS input value: $value'); + } + String toJson() => value; } @@ -457,7 +589,7 @@ enum ExternalPurchaseNoticeAction { const ExternalPurchaseNoticeAction(this.value); final String value; - factory ExternalPurchaseNoticeAction.fromJson(String value) { + static ExternalPurchaseNoticeAction? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'continue': @@ -465,9 +597,21 @@ enum ExternalPurchaseNoticeAction { case 'dismissed': return ExternalPurchaseNoticeAction.Dismissed; } + return null; + } + + factory ExternalPurchaseNoticeAction.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ExternalPurchaseNoticeAction value: $value'); } + factory ExternalPurchaseNoticeAction.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ExternalPurchaseNoticeAction input value: $value'); + } + String toJson() => value; } @@ -491,7 +635,7 @@ enum IapEvent { const IapEvent(this.value); final String value; - factory IapEvent.fromJson(String value) { + static IapEvent? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'purchase-updated': @@ -507,9 +651,21 @@ enum IapEvent { case 'subscription-billing-issue': return IapEvent.SubscriptionBillingIssue; } + return null; + } + + factory IapEvent.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown IapEvent value: $value'); } + factory IapEvent.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown IapEvent input value: $value'); + } + String toJson() => value; } @@ -522,7 +678,7 @@ enum IapkitClientPayloadFormat { const IapkitClientPayloadFormat(this.value); final String value; - factory IapkitClientPayloadFormat.fromJson(String value) { + static IapkitClientPayloadFormat? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'toml': @@ -532,9 +688,21 @@ enum IapkitClientPayloadFormat { case 'text': return IapkitClientPayloadFormat.Text; } + return null; + } + + factory IapkitClientPayloadFormat.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown IapkitClientPayloadFormat value: $value'); } + factory IapkitClientPayloadFormat.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown IapkitClientPayloadFormat input value: $value'); + } + String toJson() => value; } @@ -562,7 +730,7 @@ enum IapkitPurchaseState { const IapkitPurchaseState(this.value); final String value; - factory IapkitPurchaseState.fromJson(String value) { + static IapkitPurchaseState? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'entitled': @@ -584,9 +752,21 @@ enum IapkitPurchaseState { case 'inauthentic': return IapkitPurchaseState.Inauthentic; } + return null; + } + + factory IapkitPurchaseState.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return IapkitPurchaseState.Unknown; } + factory IapkitPurchaseState.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown IapkitPurchaseState input value: $value'); + } + String toJson() => value; } @@ -597,7 +777,7 @@ enum IapPlatform { const IapPlatform(this.value); final String value; - factory IapPlatform.fromJson(String value) { + static IapPlatform? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'ios': @@ -605,9 +785,21 @@ enum IapPlatform { case 'android': return IapPlatform.Android; } + return null; + } + + factory IapPlatform.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown IapPlatform value: $value'); } + factory IapPlatform.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown IapPlatform input value: $value'); + } + String toJson() => value; } @@ -621,7 +813,7 @@ enum IapStore { const IapStore(this.value); final String value; - factory IapStore.fromJson(String value) { + static IapStore? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unknown': @@ -635,9 +827,21 @@ enum IapStore { case 'amazon': return IapStore.Amazon; } + return null; + } + + factory IapStore.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return IapStore.Unknown; } + factory IapStore.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown IapStore input value: $value'); + } + String toJson() => value; } @@ -653,7 +857,7 @@ enum InAppMessageCategoryAndroid { const InAppMessageCategoryAndroid(this.value); final String value; - factory InAppMessageCategoryAndroid.fromJson(String value) { + static InAppMessageCategoryAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unknown-in-app-message-category-id': @@ -661,9 +865,21 @@ enum InAppMessageCategoryAndroid { case 'transactional': return InAppMessageCategoryAndroid.Transactional; } + return null; + } + + factory InAppMessageCategoryAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId; } + factory InAppMessageCategoryAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown InAppMessageCategoryAndroid input value: $value'); + } + String toJson() => value; } @@ -679,7 +895,7 @@ enum InAppMessageResponseCodeAndroid { const InAppMessageResponseCodeAndroid(this.value); final String value; - factory InAppMessageResponseCodeAndroid.fromJson(String value) { + static InAppMessageResponseCodeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'no-action-needed': @@ -687,9 +903,21 @@ enum InAppMessageResponseCodeAndroid { case 'subscription-status-updated': return InAppMessageResponseCodeAndroid.SubscriptionStatusUpdated; } + return null; + } + + factory InAppMessageResponseCodeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown InAppMessageResponseCodeAndroid value: $value'); } + factory InAppMessageResponseCodeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown InAppMessageResponseCodeAndroid input value: $value'); + } + String toJson() => value; } @@ -708,7 +936,7 @@ enum PaymentMode { const PaymentMode(this.value); final String value; - factory PaymentMode.fromJson(String value) { + static PaymentMode? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'free-trial': @@ -720,9 +948,21 @@ enum PaymentMode { case 'unknown': return PaymentMode.Unknown; } + return null; + } + + factory PaymentMode.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return PaymentMode.Unknown; } + factory PaymentMode.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown PaymentMode input value: $value'); + } + String toJson() => value; } @@ -735,7 +975,7 @@ enum PaymentModeIOS { const PaymentModeIOS(this.value); final String value; - factory PaymentModeIOS.fromJson(String value) { + static PaymentModeIOS? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'empty': @@ -747,9 +987,21 @@ enum PaymentModeIOS { case 'pay-up-front': return PaymentModeIOS.PayUpFront; } + return null; + } + + factory PaymentModeIOS.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown PaymentModeIOS value: $value'); } + factory PaymentModeIOS.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown PaymentModeIOS input value: $value'); + } + String toJson() => value; } @@ -761,7 +1013,7 @@ enum ProductQueryType { const ProductQueryType(this.value); final String value; - factory ProductQueryType.fromJson(String value) { + static ProductQueryType? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'in-app': @@ -771,9 +1023,21 @@ enum ProductQueryType { case 'all': return ProductQueryType.All; } + return null; + } + + factory ProductQueryType.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ProductQueryType value: $value'); } + factory ProductQueryType.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ProductQueryType input value: $value'); + } + String toJson() => value; } @@ -794,7 +1058,7 @@ enum ProductStatusAndroid { const ProductStatusAndroid(this.value); final String value; - factory ProductStatusAndroid.fromJson(String value) { + static ProductStatusAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'ok': @@ -806,9 +1070,21 @@ enum ProductStatusAndroid { case 'unknown': return ProductStatusAndroid.Unknown; } + return null; + } + + factory ProductStatusAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return ProductStatusAndroid.Unknown; } + factory ProductStatusAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ProductStatusAndroid input value: $value'); + } + String toJson() => value; } @@ -819,7 +1095,7 @@ enum ProductType { const ProductType(this.value); final String value; - factory ProductType.fromJson(String value) { + static ProductType? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'in-app': @@ -827,9 +1103,21 @@ enum ProductType { case 'subs': return ProductType.Subs; } + return null; + } + + factory ProductType.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ProductType value: $value'); } + factory ProductType.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ProductType input value: $value'); + } + String toJson() => value; } @@ -846,7 +1134,7 @@ enum ProductTypeIOS { const ProductTypeIOS(this.value); final String value; - factory ProductTypeIOS.fromJson(String value) { + static ProductTypeIOS? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'consumable': @@ -862,9 +1150,21 @@ enum ProductTypeIOS { case 'subscription-suite': return ProductTypeIOS.SubscriptionSuite; } + return null; + } + + factory ProductTypeIOS.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown ProductTypeIOS value: $value'); } + factory ProductTypeIOS.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown ProductTypeIOS input value: $value'); + } + String toJson() => value; } @@ -876,7 +1176,7 @@ enum PurchaseState { const PurchaseState(this.value); final String value; - factory PurchaseState.fromJson(String value) { + static PurchaseState? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'pending': @@ -886,9 +1186,21 @@ enum PurchaseState { case 'unknown': return PurchaseState.Unknown; } + return null; + } + + factory PurchaseState.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return PurchaseState.Unknown; } + factory PurchaseState.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown PurchaseState input value: $value'); + } + String toJson() => value; } @@ -898,15 +1210,27 @@ enum PurchaseVerificationProvider { const PurchaseVerificationProvider(this.value); final String value; - factory PurchaseVerificationProvider.fromJson(String value) { + static PurchaseVerificationProvider? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'iapkit': return PurchaseVerificationProvider.Iapkit; } + return null; + } + + factory PurchaseVerificationProvider.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown PurchaseVerificationProvider value: $value'); } + factory PurchaseVerificationProvider.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown PurchaseVerificationProvider input value: $value'); + } + String toJson() => value; } @@ -923,7 +1247,7 @@ enum SubResponseCodeAndroid { const SubResponseCodeAndroid(this.value); final String value; - factory SubResponseCodeAndroid.fromJson(String value) { + static SubResponseCodeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'no-applicable-sub-response-code': @@ -933,9 +1257,21 @@ enum SubResponseCodeAndroid { case 'user-ineligible': return SubResponseCodeAndroid.UserIneligible; } + return null; + } + + factory SubResponseCodeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown SubResponseCodeAndroid value: $value'); } + factory SubResponseCodeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown SubResponseCodeAndroid input value: $value'); + } + String toJson() => value; } @@ -950,7 +1286,7 @@ enum SubscriptionBillingPlanTypeIOS { const SubscriptionBillingPlanTypeIOS(this.value); final String value; - factory SubscriptionBillingPlanTypeIOS.fromJson(String value) { + static SubscriptionBillingPlanTypeIOS? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unknown': @@ -960,9 +1296,21 @@ enum SubscriptionBillingPlanTypeIOS { case 'up-front': return SubscriptionBillingPlanTypeIOS.UpFront; } + return null; + } + + factory SubscriptionBillingPlanTypeIOS.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return SubscriptionBillingPlanTypeIOS.Unknown; } + factory SubscriptionBillingPlanTypeIOS.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown SubscriptionBillingPlanTypeIOS input value: $value'); + } + String toJson() => value; } @@ -976,7 +1324,7 @@ enum SubscriptionOfferTypeIOS { const SubscriptionOfferTypeIOS(this.value); final String value; - factory SubscriptionOfferTypeIOS.fromJson(String value) { + static SubscriptionOfferTypeIOS? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'introductory': @@ -986,9 +1334,21 @@ enum SubscriptionOfferTypeIOS { case 'win-back': return SubscriptionOfferTypeIOS.WinBack; } + return null; + } + + factory SubscriptionOfferTypeIOS.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown SubscriptionOfferTypeIOS value: $value'); } + factory SubscriptionOfferTypeIOS.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown SubscriptionOfferTypeIOS input value: $value'); + } + String toJson() => value; } @@ -1002,7 +1362,7 @@ enum SubscriptionPeriodIOS { const SubscriptionPeriodIOS(this.value); final String value; - factory SubscriptionPeriodIOS.fromJson(String value) { + static SubscriptionPeriodIOS? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'day': @@ -1016,9 +1376,21 @@ enum SubscriptionPeriodIOS { case 'empty': return SubscriptionPeriodIOS.Empty; } + return null; + } + + factory SubscriptionPeriodIOS.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; throw ArgumentError('Unknown SubscriptionPeriodIOS value: $value'); } + factory SubscriptionPeriodIOS.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown SubscriptionPeriodIOS input value: $value'); + } + String toJson() => value; } @@ -1033,7 +1405,7 @@ enum SubscriptionPeriodUnit { const SubscriptionPeriodUnit(this.value); final String value; - factory SubscriptionPeriodUnit.fromJson(String value) { + static SubscriptionPeriodUnit? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'day': @@ -1047,9 +1419,21 @@ enum SubscriptionPeriodUnit { case 'unknown': return SubscriptionPeriodUnit.Unknown; } + return null; + } + + factory SubscriptionPeriodUnit.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return SubscriptionPeriodUnit.Unknown; } + factory SubscriptionPeriodUnit.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown SubscriptionPeriodUnit input value: $value'); + } + String toJson() => value; } @@ -1075,7 +1459,7 @@ enum SubscriptionReplacementModeAndroid { const SubscriptionReplacementModeAndroid(this.value); final String value; - factory SubscriptionReplacementModeAndroid.fromJson(String value) { + static SubscriptionReplacementModeAndroid? _fromKnownJson(String value) { final normalized = value.toLowerCase().replaceAll('_', '-'); switch (normalized) { case 'unknown-replacement-mode': @@ -1093,9 +1477,21 @@ enum SubscriptionReplacementModeAndroid { case 'keep-existing': return SubscriptionReplacementModeAndroid.KeepExisting; } + return null; + } + + factory SubscriptionReplacementModeAndroid.fromJson(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; return SubscriptionReplacementModeAndroid.UnknownReplacementMode; } + factory SubscriptionReplacementModeAndroid.fromJsonStrict(String value) { + final known = _fromKnownJson(value); + if (known != null) return known; + throw ArgumentError('Unknown SubscriptionReplacementModeAndroid input value: $value'); + } + String toJson() => value; } @@ -4073,7 +4469,7 @@ class BillingProgramInformationDialogParamsAndroid { factory BillingProgramInformationDialogParamsAndroid.fromJson(Map json) { return BillingProgramInformationDialogParamsAndroid( - billingProgram: json['billingProgram'] != null ? BillingProgramAndroid.fromJson(json['billingProgram'] as String) : BillingProgramAndroid.BillingChoice, + billingProgram: json['billingProgram'] != null ? BillingProgramAndroid.fromJsonStrict(json['billingProgram'] as String) : BillingProgramAndroid.BillingChoice, externalTransactionToken: json['externalTransactionToken'] as String, ); } @@ -4139,9 +4535,9 @@ class DeveloperBillingOptionParamsAndroid { factory DeveloperBillingOptionParamsAndroid.fromJson(Map json) { return DeveloperBillingOptionParamsAndroid( - billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), + billingProgram: BillingProgramAndroid.fromJsonStrict(json['billingProgram'] as String), externalTransactionToken: json['externalTransactionToken'] as String?, - launchMode: json['launchMode'] != null ? DeveloperBillingLaunchModeAndroid.fromJson(json['launchMode'] as String) : null, + launchMode: json['launchMode'] != null ? DeveloperBillingLaunchModeAndroid.fromJsonStrict(json['launchMode'] as String) : null, linkUri: json['linkUri'] as String?, ); } @@ -4215,8 +4611,8 @@ class GetBillingChoiceInfoParamsAndroid { factory GetBillingChoiceInfoParamsAndroid.fromJson(Map json) { return GetBillingChoiceInfoParamsAndroid( - billingProgram: json['billingProgram'] != null ? BillingProgramAndroid.fromJson(json['billingProgram'] as String) : BillingProgramAndroid.BillingChoice, - playBillingChoiceImageLayout: json['playBillingChoiceImageLayout'] != null ? BillingChoiceImageLayoutAndroid.fromJson(json['playBillingChoiceImageLayout'] as String) : BillingChoiceImageLayoutAndroid.RectangularFourByOne, + billingProgram: json['billingProgram'] != null ? BillingProgramAndroid.fromJsonStrict(json['billingProgram'] as String) : BillingProgramAndroid.BillingChoice, + playBillingChoiceImageLayout: json['playBillingChoiceImageLayout'] != null ? BillingChoiceImageLayoutAndroid.fromJsonStrict(json['playBillingChoiceImageLayout'] as String) : BillingChoiceImageLayoutAndroid.RectangularFourByOne, userLocale: json['userLocale'] as String?, ); } @@ -4243,7 +4639,7 @@ class InAppMessageParamsAndroid { factory InAppMessageParamsAndroid.fromJson(Map json) { return InAppMessageParamsAndroid( - categories: (json['categories'] as List?) == null ? const [InAppMessageCategoryAndroid.Transactional] : (json['categories'] as List?)!.map((e) => InAppMessageCategoryAndroid.fromJson(e as String)).toList(), + categories: (json['categories'] as List?) == null ? const [InAppMessageCategoryAndroid.Transactional] : (json['categories'] as List?)!.map((e) => InAppMessageCategoryAndroid.fromJsonStrict(e as String)).toList(), ); } @@ -4281,8 +4677,8 @@ class InitConnectionConfig { factory InitConnectionConfig.fromJson(Map json) { return InitConnectionConfig( - billingChoiceScreenTypeAndroid: json['billingChoiceScreenTypeAndroid'] != null ? BillingChoiceScreenTypeAndroid.fromJson(json['billingChoiceScreenTypeAndroid'] as String) : BillingChoiceScreenTypeAndroid.GoogleRendered, - enableBillingProgramAndroid: json['enableBillingProgramAndroid'] != null ? BillingProgramAndroid.fromJson(json['enableBillingProgramAndroid'] as String) : null, + billingChoiceScreenTypeAndroid: json['billingChoiceScreenTypeAndroid'] != null ? BillingChoiceScreenTypeAndroid.fromJsonStrict(json['billingChoiceScreenTypeAndroid'] as String) : BillingChoiceScreenTypeAndroid.GoogleRendered, + enableBillingProgramAndroid: json['enableBillingProgramAndroid'] != null ? BillingProgramAndroid.fromJsonStrict(json['enableBillingProgramAndroid'] as String) : null, ); } @@ -4322,10 +4718,10 @@ class LaunchExternalLinkParamsAndroid { factory LaunchExternalLinkParamsAndroid.fromJson(Map json) { return LaunchExternalLinkParamsAndroid( - billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String), + billingProgram: BillingProgramAndroid.fromJsonStrict(json['billingProgram'] as String), externalTransactionToken: json['externalTransactionToken'] as String?, - launchMode: ExternalLinkLaunchModeAndroid.fromJson(json['launchMode'] as String), - linkType: ExternalLinkTypeAndroid.fromJson(json['linkType'] as String), + launchMode: ExternalLinkLaunchModeAndroid.fromJsonStrict(json['launchMode'] as String), + linkType: ExternalLinkTypeAndroid.fromJsonStrict(json['linkType'] as String), linkUri: json['linkUri'] as String, ); } @@ -4353,7 +4749,7 @@ class ProductRequest { factory ProductRequest.fromJson(Map json) { return ProductRequest( skus: (json['skus'] as List).map((e) => e as String).toList(), - type: json['type'] != null ? ProductQueryType.fromJson(json['type'] as String) : ProductQueryType.InApp, + type: json['type'] != null ? ProductQueryType.fromJsonStrict(json['type'] as String) : ProductQueryType.InApp, ); } @@ -4765,7 +5161,7 @@ class RequestSubscriptionIosProps { advancedCommerceData: json['advancedCommerceData'] as String?, andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?, appAccountToken: json['appAccountToken'] as String?, - billingPlanType: json['billingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanType'] as String) : null, + billingPlanType: json['billingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJsonStrict(json['billingPlanType'] as String) : null, compactJWS: json['compactJWS'] as String?, promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null, quantity: json['quantity'] as int?, @@ -4977,7 +5373,7 @@ class SubscriptionProductReplacementParamsAndroid { factory SubscriptionProductReplacementParamsAndroid.fromJson(Map json) { return SubscriptionProductReplacementParamsAndroid( oldProductId: json['oldProductId'] as String, - replacementMode: SubscriptionReplacementModeAndroid.fromJson(json['replacementMode'] as String), + replacementMode: SubscriptionReplacementModeAndroid.fromJsonStrict(json['replacementMode'] as String), ); } @@ -5144,7 +5540,7 @@ class VerifyPurchaseWithProviderProps { factory VerifyPurchaseWithProviderProps.fromJson(Map json) { return VerifyPurchaseWithProviderProps( iapkit: json['iapkit'] != null ? RequestVerifyPurchaseWithIapkitProps.fromJson(json['iapkit'] as Map) : null, - provider: PurchaseVerificationProvider.fromJson(json['provider'] as String), + provider: PurchaseVerificationProvider.fromJsonStrict(json['provider'] as String), ); } diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 5515aaff2..482a3baa6 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -730,12 +730,16 @@ class BillingProgramAvailabilityResultAndroid: var enum_str = data["billingProgram"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.billing_program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): + obj.billing_program = enum_str else: obj.billing_program = enum_str if data.has("choiceScreenType") and data["choiceScreenType"] != null: var enum_str = data["choiceScreenType"] if enum_str is String and BILLING_CHOICE_SCREEN_TYPE_ANDROID_FROM_STRING.has(enum_str): obj.choice_screen_type = BILLING_CHOICE_SCREEN_TYPE_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and BILLING_CHOICE_SCREEN_TYPE_ANDROID_VALUES.has(enum_str): + obj.choice_screen_type = enum_str else: obj.choice_screen_type = enum_str if data.has("isExternalLinkAvailable") and data["isExternalLinkAvailable"] != null: @@ -775,6 +779,8 @@ class BillingProgramReportingDetailsAndroid: var enum_str = data["billingProgram"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.billing_program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): + obj.billing_program = enum_str else: obj.billing_program = enum_str if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: @@ -809,6 +815,8 @@ class BillingResultAndroid: var enum_str = data["subResponseCode"] if enum_str is String and SUB_RESPONSE_CODE_ANDROID_FROM_STRING.has(enum_str): obj.sub_response_code = SUB_RESPONSE_CODE_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and SUB_RESPONSE_CODE_ANDROID_VALUES.has(enum_str): + obj.sub_response_code = enum_str else: obj.sub_response_code = enum_str return obj @@ -950,6 +958,8 @@ class DeveloperProvidedBillingProductAndroid: var enum_str = data["type"] if enum_str is String and PRODUCT_TYPE_FROM_STRING.has(enum_str): obj.type = PRODUCT_TYPE_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_TYPE_VALUES.has(enum_str): + obj.type = enum_str else: obj.type = enum_str if data.has("offerToken") and data["offerToken"] != null: @@ -1069,6 +1079,8 @@ class DiscountOffer: var enum_str = data["type"] if enum_str is String and DISCOUNT_OFFER_TYPE_FROM_STRING.has(enum_str): obj.type = DISCOUNT_OFFER_TYPE_FROM_STRING[enum_str] + elif enum_str is int and DISCOUNT_OFFER_TYPE_VALUES.has(enum_str): + obj.type = enum_str else: obj.type = enum_str if data.has("offerTokenAndroid") and data["offerTokenAndroid"] != null: @@ -1262,6 +1274,8 @@ class ExternalPurchaseNoticeResultIOS: var enum_str = data["result"] if enum_str is String and EXTERNAL_PURCHASE_NOTICE_ACTION_FROM_STRING.has(enum_str): obj.result = EXTERNAL_PURCHASE_NOTICE_ACTION_FROM_STRING[enum_str] + elif enum_str is int and EXTERNAL_PURCHASE_NOTICE_ACTION_VALUES.has(enum_str): + obj.result = enum_str else: obj.result = enum_str if data.has("error") and data["error"] != null: @@ -1299,6 +1313,8 @@ class IapkitProductClientPayload: var enum_str = data["format"] if enum_str is String and IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING.has(enum_str): obj.format = IAPKIT_CLIENT_PAYLOAD_FORMAT_FROM_STRING[enum_str] + elif enum_str is int and IAPKIT_CLIENT_PAYLOAD_FORMAT_VALUES.has(enum_str): + obj.format = enum_str else: obj.format = enum_str if data.has("body") and data["body"] != null: @@ -1340,6 +1356,8 @@ class InAppMessageResultAndroid: var enum_str = data["responseCode"] if enum_str is String and IN_APP_MESSAGE_RESPONSE_CODE_ANDROID_FROM_STRING.has(enum_str): obj.response_code = IN_APP_MESSAGE_RESPONSE_CODE_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and IN_APP_MESSAGE_RESPONSE_CODE_ANDROID_VALUES.has(enum_str): + obj.response_code = enum_str else: obj.response_code = enum_str if data.has("purchaseToken") and data["purchaseToken"] != null: @@ -1551,6 +1569,8 @@ class ProductAndroid: var enum_str = data["type"] if enum_str is String and PRODUCT_TYPE_FROM_STRING.has(enum_str): obj.type = PRODUCT_TYPE_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_TYPE_VALUES.has(enum_str): + obj.type = enum_str else: obj.type = enum_str if data.has("displayName") and data["displayName"] != null: @@ -1567,15 +1587,17 @@ class ProductAndroid: var enum_str = data["platform"] if enum_str is String and IAP_PLATFORM_FROM_STRING.has(enum_str): obj.platform = IAP_PLATFORM_FROM_STRING[enum_str] + elif enum_str is int and IAP_PLATFORM_VALUES.has(enum_str): + obj.platform = enum_str else: obj.platform = enum_str if data.has("nameAndroid") and data["nameAndroid"] != null: obj.name_android = data["nameAndroid"] if data.has("productStatusAndroid") and data["productStatusAndroid"] != null: var enum_str = data["productStatusAndroid"] - if enum_str is String: - obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN) - elif enum_str is int: + if enum_str is String and PRODUCT_STATUS_ANDROID_FROM_STRING.has(enum_str): + obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_STATUS_ANDROID_VALUES.has(enum_str): obj.product_status_android = enum_str else: obj.product_status_android = ProductStatusAndroid.UNKNOWN @@ -1706,6 +1728,8 @@ class ProductIOS: var enum_str = data["type"] if enum_str is String and PRODUCT_TYPE_FROM_STRING.has(enum_str): obj.type = PRODUCT_TYPE_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_TYPE_VALUES.has(enum_str): + obj.type = enum_str else: obj.type = enum_str if data.has("displayName") and data["displayName"] != null: @@ -1722,6 +1746,8 @@ class ProductIOS: var enum_str = data["platform"] if enum_str is String and IAP_PLATFORM_FROM_STRING.has(enum_str): obj.platform = IAP_PLATFORM_FROM_STRING[enum_str] + elif enum_str is int and IAP_PLATFORM_VALUES.has(enum_str): + obj.platform = enum_str else: obj.platform = enum_str if data.has("displayNameIOS") and data["displayNameIOS"] != null: @@ -1734,6 +1760,8 @@ class ProductIOS: var enum_str = data["typeIOS"] if enum_str is String and PRODUCT_TYPE_IOS_FROM_STRING.has(enum_str): obj.type_ios = PRODUCT_TYPE_IOS_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_TYPE_IOS_VALUES.has(enum_str): + obj.type_ios = enum_str else: obj.type_ios = enum_str if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: @@ -1857,6 +1885,8 @@ class ProductSubscriptionAndroid: var enum_str = data["type"] if enum_str is String and PRODUCT_TYPE_FROM_STRING.has(enum_str): obj.type = PRODUCT_TYPE_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_TYPE_VALUES.has(enum_str): + obj.type = enum_str else: obj.type = enum_str if data.has("displayName") and data["displayName"] != null: @@ -1873,15 +1903,17 @@ class ProductSubscriptionAndroid: var enum_str = data["platform"] if enum_str is String and IAP_PLATFORM_FROM_STRING.has(enum_str): obj.platform = IAP_PLATFORM_FROM_STRING[enum_str] + elif enum_str is int and IAP_PLATFORM_VALUES.has(enum_str): + obj.platform = enum_str else: obj.platform = enum_str if data.has("nameAndroid") and data["nameAndroid"] != null: obj.name_android = data["nameAndroid"] if data.has("productStatusAndroid") and data["productStatusAndroid"] != null: var enum_str = data["productStatusAndroid"] - if enum_str is String: - obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN) - elif enum_str is int: + if enum_str is String and PRODUCT_STATUS_ANDROID_FROM_STRING.has(enum_str): + obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_STATUS_ANDROID_VALUES.has(enum_str): obj.product_status_android = enum_str else: obj.product_status_android = ProductStatusAndroid.UNKNOWN @@ -2001,6 +2033,8 @@ class ProductSubscriptionIOS: var enum_str = data["type"] if enum_str is String and PRODUCT_TYPE_FROM_STRING.has(enum_str): obj.type = PRODUCT_TYPE_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_TYPE_VALUES.has(enum_str): + obj.type = enum_str else: obj.type = enum_str if data.has("displayName") and data["displayName"] != null: @@ -2017,6 +2051,8 @@ class ProductSubscriptionIOS: var enum_str = data["platform"] if enum_str is String and IAP_PLATFORM_FROM_STRING.has(enum_str): obj.platform = IAP_PLATFORM_FROM_STRING[enum_str] + elif enum_str is int and IAP_PLATFORM_VALUES.has(enum_str): + obj.platform = enum_str else: obj.platform = enum_str if data.has("displayNameIOS") and data["displayNameIOS"] != null: @@ -2029,6 +2065,8 @@ class ProductSubscriptionIOS: var enum_str = data["typeIOS"] if enum_str is String and PRODUCT_TYPE_IOS_FROM_STRING.has(enum_str): obj.type_ios = PRODUCT_TYPE_IOS_FROM_STRING[enum_str] + elif enum_str is int and PRODUCT_TYPE_IOS_VALUES.has(enum_str): + obj.type_ios = enum_str else: obj.type_ios = enum_str if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: @@ -2253,9 +2291,9 @@ class PurchaseAndroid: obj.purchase_token = data["purchaseToken"] if data.has("store") and data["store"] != null: var enum_str = data["store"] - if enum_str is String: - obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - elif enum_str is int: + if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str): + obj.store = IAP_STORE_FROM_STRING[enum_str] + elif enum_str is int and IAP_STORE_VALUES.has(enum_str): obj.store = enum_str else: obj.store = IapStore.UNKNOWN @@ -2263,9 +2301,9 @@ class PurchaseAndroid: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] - if enum_str is String: - obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - elif enum_str is int: + if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str): + obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str] + elif enum_str is int and PURCHASE_STATE_VALUES.has(enum_str): obj.purchase_state = enum_str else: obj.purchase_state = PurchaseState.UNKNOWN @@ -2367,9 +2405,9 @@ class PurchaseError: var obj = PurchaseError.new() if data.has("code") and data["code"] != null: var enum_str = data["code"] - if enum_str is String: - obj.code = ERROR_CODE_FROM_STRING.get(enum_str, ErrorCode.UNKNOWN) - elif enum_str is int: + if enum_str is String and ERROR_CODE_FROM_STRING.has(enum_str): + obj.code = ERROR_CODE_FROM_STRING[enum_str] + elif enum_str is int and ERROR_CODE_VALUES.has(enum_str): obj.code = enum_str else: obj.code = ErrorCode.UNKNOWN @@ -2385,6 +2423,8 @@ class PurchaseError: var enum_str = data["subResponseCodeAndroid"] if enum_str is String and SUB_RESPONSE_CODE_ANDROID_FROM_STRING.has(enum_str): obj.sub_response_code_android = SUB_RESPONSE_CODE_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and SUB_RESPONSE_CODE_ANDROID_VALUES.has(enum_str): + obj.sub_response_code_android = enum_str else: obj.sub_response_code_android = enum_str if data.has("productIds") and data["productIds"] != null: @@ -2501,9 +2541,9 @@ class PurchaseIOS: obj.purchase_token = data["purchaseToken"] if data.has("store") and data["store"] != null: var enum_str = data["store"] - if enum_str is String: - obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - elif enum_str is int: + if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str): + obj.store = IAP_STORE_FROM_STRING[enum_str] + elif enum_str is int and IAP_STORE_VALUES.has(enum_str): obj.store = enum_str else: obj.store = IapStore.UNKNOWN @@ -2511,9 +2551,9 @@ class PurchaseIOS: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] - if enum_str is String: - obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - elif enum_str is int: + if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str): + obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str] + elif enum_str is int and PURCHASE_STATE_VALUES.has(enum_str): obj.purchase_state = enum_str else: obj.purchase_state = PurchaseState.UNKNOWN @@ -2577,9 +2617,9 @@ class PurchaseIOS: obj.renewal_info_ios = data["renewalInfoIOS"] if data.has("billingPlanTypeIOS") and data["billingPlanTypeIOS"] != null: var enum_str = data["billingPlanTypeIOS"] - if enum_str is String: - obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - elif enum_str is int: + if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str): + obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str] + elif enum_str is int and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(enum_str): obj.billing_plan_type_ios = enum_str else: obj.billing_plan_type_ios = SubscriptionBillingPlanTypeIOS.UNKNOWN @@ -2757,9 +2797,9 @@ class RenewalCommitmentInfoIOS: obj.commitment_auto_renew_status = data["commitmentAutoRenewStatus"] if data.has("commitmentRenewalBillingPlanType") and data["commitmentRenewalBillingPlanType"] != null: var enum_str = data["commitmentRenewalBillingPlanType"] - if enum_str is String: - obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - elif enum_str is int: + if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str): + obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str] + elif enum_str is int and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(enum_str): obj.commitment_renewal_billing_plan_type = enum_str else: obj.commitment_renewal_billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN @@ -2841,9 +2881,9 @@ class RenewalInfoIOS: obj.renewal_offer_type = data["renewalOfferType"] if data.has("renewalBillingPlanType") and data["renewalBillingPlanType"] != null: var enum_str = data["renewalBillingPlanType"] - if enum_str is String: - obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - elif enum_str is int: + if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str): + obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str] + elif enum_str is int and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(enum_str): obj.renewal_billing_plan_type = enum_str else: obj.renewal_billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN @@ -2943,9 +2983,9 @@ class RequestVerifyPurchaseWithIapkitResult: var obj = RequestVerifyPurchaseWithIapkitResult.new() if data.has("store") and data["store"] != null: var enum_str = data["store"] - if enum_str is String: - obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - elif enum_str is int: + if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str): + obj.store = IAP_STORE_FROM_STRING[enum_str] + elif enum_str is int and IAP_STORE_VALUES.has(enum_str): obj.store = enum_str else: obj.store = IapStore.UNKNOWN @@ -2955,9 +2995,9 @@ class RequestVerifyPurchaseWithIapkitResult: obj.is_valid = data["isValid"] if data.has("state") and data["state"] != null: var enum_str = data["state"] - if enum_str is String: - obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING.get(enum_str, IapkitPurchaseState.UNKNOWN) - elif enum_str is int: + if enum_str is String and IAPKIT_PURCHASE_STATE_FROM_STRING.has(enum_str): + obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING[enum_str] + elif enum_str is int and IAPKIT_PURCHASE_STATE_VALUES.has(enum_str): obj.state = enum_str else: obj.state = IapkitPurchaseState.UNKNOWN @@ -3087,6 +3127,8 @@ class SubscriptionOffer: var enum_str = data["type"] if enum_str is String and DISCOUNT_OFFER_TYPE_FROM_STRING.has(enum_str): obj.type = DISCOUNT_OFFER_TYPE_FROM_STRING[enum_str] + elif enum_str is int and DISCOUNT_OFFER_TYPE_VALUES.has(enum_str): + obj.type = enum_str else: obj.type = enum_str if data.has("period") and data["period"] != null: @@ -3098,9 +3140,9 @@ class SubscriptionOffer: obj.period_count = data["periodCount"] if data.has("paymentMode") and data["paymentMode"] != null: var enum_str = data["paymentMode"] - if enum_str is String: - obj.payment_mode = PAYMENT_MODE_FROM_STRING.get(enum_str, PaymentMode.UNKNOWN) - elif enum_str is int: + if enum_str is String and PAYMENT_MODE_FROM_STRING.has(enum_str): + obj.payment_mode = PAYMENT_MODE_FROM_STRING[enum_str] + elif enum_str is int and PAYMENT_MODE_VALUES.has(enum_str): obj.payment_mode = enum_str else: obj.payment_mode = PaymentMode.UNKNOWN @@ -3199,9 +3241,9 @@ class SubscriptionPeriod: var obj = SubscriptionPeriod.new() if data.has("unit") and data["unit"] != null: var enum_str = data["unit"] - if enum_str is String: - obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.get(enum_str, SubscriptionPeriodUnit.UNKNOWN) - elif enum_str is int: + if enum_str is String and SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.has(enum_str): + obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING[enum_str] + elif enum_str is int and SUBSCRIPTION_PERIOD_UNIT_VALUES.has(enum_str): obj.unit = enum_str else: obj.unit = SubscriptionPeriodUnit.UNKNOWN @@ -3284,9 +3326,9 @@ class SubscriptionPricingTermsIOS: return null if data.has("billingPlanType") and data["billingPlanType"] != null: var enum_str = data["billingPlanType"] - if enum_str is String: - obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - elif enum_str is int: + if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str): + obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str] + elif enum_str is int and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(enum_str): obj.billing_plan_type = enum_str else: obj.billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN @@ -3669,6 +3711,8 @@ class VerifyPurchaseWithProviderResult: var enum_str = data["provider"] if enum_str is String and PURCHASE_VERIFICATION_PROVIDER_FROM_STRING.has(enum_str): obj.provider = PURCHASE_VERIFICATION_PROVIDER_FROM_STRING[enum_str] + elif enum_str is int and PURCHASE_VERIFICATION_PROVIDER_VALUES.has(enum_str): + obj.provider = enum_str else: obj.provider = enum_str if data.has("iapkit") and data["iapkit"] != null: @@ -3737,6 +3781,12 @@ class AndroidSubscriptionOfferInput: var offer_token: String = "" static func from_dict(data: Dictionary) -> AndroidSubscriptionOfferInput: + if not data.has("sku") or not data["sku"] is String: + push_error("Invalid required AndroidSubscriptionOfferInput.sku value") + return null + if not data.has("offerToken") or not data["offerToken"] is String: + push_error("Invalid required AndroidSubscriptionOfferInput.offerToken value") + return null var obj = AndroidSubscriptionOfferInput.new() if data.has("sku") and data["sku"] != null: obj.sku = data["sku"] @@ -3760,6 +3810,9 @@ class BillingProgramInformationDialogParamsAndroid: var external_transaction_token: String = "" static func from_dict(data: Dictionary, report_errors: bool = true) -> BillingProgramInformationDialogParamsAndroid: + if not data.has("externalTransactionToken") or not data["externalTransactionToken"] is String: + push_error("Invalid required BillingProgramInformationDialogParamsAndroid.externalTransactionToken value") + return null if data.has("billingProgram") and data["billingProgram"] != null and not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): if report_errors: push_error("Invalid BillingProgramInformationDialogParamsAndroid.billingProgram enum value") @@ -3769,8 +3822,11 @@ class BillingProgramInformationDialogParamsAndroid: var enum_str = data["billingProgram"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.billing_program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): obj.billing_program = enum_str + else: + push_error("Invalid BillingProgramAndroid input value") + return null if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: obj.external_transaction_token = data["externalTransactionToken"] return obj @@ -3793,6 +3849,12 @@ class DeepLinkOptions: var package_name_android: Variant = null static func from_dict(data: Dictionary) -> DeepLinkOptions: + if data.has("skuAndroid") and data["skuAndroid"] != null and not data["skuAndroid"] is String: + push_error("Invalid DeepLinkOptions.skuAndroid value") + return null + if data.has("packageNameAndroid") and data["packageNameAndroid"] != null and not data["packageNameAndroid"] is String: + push_error("Invalid DeepLinkOptions.packageNameAndroid value") + return null var obj = DeepLinkOptions.new() if data.has("skuAndroid") and data["skuAndroid"] != null: obj.sku_android = data["skuAndroid"] @@ -3820,6 +3882,12 @@ class DeveloperBillingOptionParamsAndroid: var external_transaction_token: Variant = null static func from_dict(data: Dictionary, report_errors: bool = true) -> DeveloperBillingOptionParamsAndroid: + if data.has("linkUri") and data["linkUri"] != null and not data["linkUri"] is String: + push_error("Invalid DeveloperBillingOptionParamsAndroid.linkUri value") + return null + if data.has("externalTransactionToken") and data["externalTransactionToken"] != null and not data["externalTransactionToken"] is String: + push_error("Invalid DeveloperBillingOptionParamsAndroid.externalTransactionToken value") + return null if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): if report_errors: push_error("Invalid DeveloperBillingOptionParamsAndroid.billingProgram enum value") @@ -3829,16 +3897,22 @@ class DeveloperBillingOptionParamsAndroid: var enum_str = data["billingProgram"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.billing_program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): obj.billing_program = enum_str + else: + push_error("Invalid BillingProgramAndroid input value") + return null if data.has("linkUri") and data["linkUri"] != null: obj.link_uri = data["linkUri"] if data.has("launchMode") and data["launchMode"] != null: var enum_str = data["launchMode"] if enum_str is String and DEVELOPER_BILLING_LAUNCH_MODE_ANDROID_FROM_STRING.has(enum_str): obj.launch_mode = DEVELOPER_BILLING_LAUNCH_MODE_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and DEVELOPER_BILLING_LAUNCH_MODE_ANDROID_VALUES.has(enum_str): obj.launch_mode = enum_str + else: + push_error("Invalid DeveloperBillingLaunchModeAndroid input value") + return null if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: obj.external_transaction_token = data["externalTransactionToken"] return obj @@ -3874,6 +3948,21 @@ class DiscountOfferInputIOS: var timestamp: float = 0.0 static func from_dict(data: Dictionary) -> DiscountOfferInputIOS: + if not data.has("identifier") or not data["identifier"] is String: + push_error("Invalid required DiscountOfferInputIOS.identifier value") + return null + if not data.has("keyIdentifier") or not data["keyIdentifier"] is String: + push_error("Invalid required DiscountOfferInputIOS.keyIdentifier value") + return null + if not data.has("nonce") or not data["nonce"] is String: + push_error("Invalid required DiscountOfferInputIOS.nonce value") + return null + if not data.has("signature") or not data["signature"] is String: + push_error("Invalid required DiscountOfferInputIOS.signature value") + return null + if not data.has("timestamp") or not (data["timestamp"] is int or data["timestamp"] is float): + push_error("Invalid required DiscountOfferInputIOS.timestamp value") + return null var obj = DiscountOfferInputIOS.new() if data.has("identifier") and data["identifier"] != null: obj.identifier = data["identifier"] @@ -3911,6 +4000,9 @@ class GetBillingChoiceInfoParamsAndroid: var user_locale: Variant = null static func from_dict(data: Dictionary, report_errors: bool = true) -> GetBillingChoiceInfoParamsAndroid: + if data.has("userLocale") and data["userLocale"] != null and not data["userLocale"] is String: + push_error("Invalid GetBillingChoiceInfoParamsAndroid.userLocale value") + return null if data.has("billingProgram") and data["billingProgram"] != null and not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): if report_errors: push_error("Invalid GetBillingChoiceInfoParamsAndroid.billingProgram enum value") @@ -3924,14 +4016,20 @@ class GetBillingChoiceInfoParamsAndroid: var enum_str = data["billingProgram"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.billing_program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): obj.billing_program = enum_str + else: + push_error("Invalid BillingProgramAndroid input value") + return null if data.has("playBillingChoiceImageLayout") and data["playBillingChoiceImageLayout"] != null: var enum_str = data["playBillingChoiceImageLayout"] if enum_str is String and BILLING_CHOICE_IMAGE_LAYOUT_ANDROID_FROM_STRING.has(enum_str): obj.play_billing_choice_image_layout = BILLING_CHOICE_IMAGE_LAYOUT_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_CHOICE_IMAGE_LAYOUT_ANDROID_VALUES.has(enum_str): obj.play_billing_choice_image_layout = enum_str + else: + push_error("Invalid BillingChoiceImageLayoutAndroid input value") + return null if data.has("userLocale") and data["userLocale"] != null: obj.user_locale = data["userLocale"] return obj @@ -3963,13 +4061,17 @@ class InAppMessageParamsAndroid: if data["categories"] is Array: var arr: Array[InAppMessageCategoryAndroid] = [] for item in data["categories"]: - if item is String: - arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.get(item, InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID)) + if item is String and IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING.has(item): + arr.append(IN_APP_MESSAGE_CATEGORY_ANDROID_FROM_STRING[item]) elif item is int and IN_APP_MESSAGE_CATEGORY_ANDROID_VALUES.has(item): arr.append(item) - elif item is int: - arr.append(InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID) + else: + push_error("Invalid InAppMessageCategoryAndroid list value for categories") + return null obj.categories = arr + else: + push_error("Invalid input list for categories") + return null return obj func to_dict() -> Dictionary: @@ -3997,14 +4099,20 @@ class InitConnectionConfig: var enum_str = data["enableBillingProgramAndroid"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.enable_billing_program_android = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): obj.enable_billing_program_android = enum_str + else: + push_error("Invalid BillingProgramAndroid input value") + return null if data.has("billingChoiceScreenTypeAndroid") and data["billingChoiceScreenTypeAndroid"] != null: var enum_str = data["billingChoiceScreenTypeAndroid"] if enum_str is String and BILLING_CHOICE_SCREEN_TYPE_ANDROID_FROM_STRING.has(enum_str): obj.billing_choice_screen_type_android = BILLING_CHOICE_SCREEN_TYPE_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_CHOICE_SCREEN_TYPE_ANDROID_VALUES.has(enum_str): obj.billing_choice_screen_type_android = enum_str + else: + push_error("Invalid BillingChoiceScreenTypeAndroid input value") + return null return obj func to_dict() -> Dictionary: @@ -4035,6 +4143,12 @@ class LaunchExternalLinkParamsAndroid: var external_transaction_token: Variant = null static func from_dict(data: Dictionary, report_errors: bool = true) -> LaunchExternalLinkParamsAndroid: + if not data.has("linkUri") or not data["linkUri"] is String: + push_error("Invalid required LaunchExternalLinkParamsAndroid.linkUri value") + return null + if data.has("externalTransactionToken") and data["externalTransactionToken"] != null and not data["externalTransactionToken"] is String: + push_error("Invalid LaunchExternalLinkParamsAndroid.externalTransactionToken value") + return null if not data.has("billingProgram") or not ((data["billingProgram"] is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(data["billingProgram"])) or (data["billingProgram"] is int and BILLING_PROGRAM_ANDROID_VALUES.has(data["billingProgram"]))): if report_errors: push_error("Invalid LaunchExternalLinkParamsAndroid.billingProgram enum value") @@ -4052,20 +4166,29 @@ class LaunchExternalLinkParamsAndroid: var enum_str = data["billingProgram"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.billing_program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): obj.billing_program = enum_str + else: + push_error("Invalid BillingProgramAndroid input value") + return null if data.has("launchMode") and data["launchMode"] != null: var enum_str = data["launchMode"] if enum_str is String and EXTERNAL_LINK_LAUNCH_MODE_ANDROID_FROM_STRING.has(enum_str): obj.launch_mode = EXTERNAL_LINK_LAUNCH_MODE_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and EXTERNAL_LINK_LAUNCH_MODE_ANDROID_VALUES.has(enum_str): obj.launch_mode = enum_str + else: + push_error("Invalid ExternalLinkLaunchModeAndroid input value") + return null if data.has("linkType") and data["linkType"] != null: var enum_str = data["linkType"] if enum_str is String and EXTERNAL_LINK_TYPE_ANDROID_FROM_STRING.has(enum_str): obj.link_type = EXTERNAL_LINK_TYPE_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and EXTERNAL_LINK_TYPE_ANDROID_VALUES.has(enum_str): obj.link_type = enum_str + else: + push_error("Invalid ExternalLinkTypeAndroid input value") + return null if data.has("linkUri") and data["linkUri"] != null: obj.link_uri = data["linkUri"] if data.has("externalTransactionToken") and data["externalTransactionToken"] != null: @@ -4100,6 +4223,9 @@ class ProductRequest: var type: ProductQueryType = ProductQueryType.IN_APP static func from_dict(data: Dictionary) -> ProductRequest: + if not data.has("skus") or not data["skus"] is Array: + push_error("Invalid required ProductRequest.skus value") + return null var obj = ProductRequest.new() if data.has("skus") and data["skus"] != null: if data["skus"] is Array: @@ -4107,13 +4233,22 @@ class ProductRequest: for item in data["skus"]: if item is String: arr.append(str(item)) + else: + push_error("Invalid String list value for skus") + return null obj.skus = arr + else: + push_error("Invalid input list for skus") + return null if data.has("type") and data["type"] != null: var enum_str = data["type"] if enum_str is String and PRODUCT_QUERY_TYPE_FROM_STRING.has(enum_str): obj.type = PRODUCT_QUERY_TYPE_FROM_STRING[enum_str] - else: + elif enum_str is int and PRODUCT_QUERY_TYPE_VALUES.has(enum_str): obj.type = enum_str + else: + push_error("Invalid ProductQueryType input value") + return null return obj func to_dict() -> Dictionary: @@ -4135,6 +4270,12 @@ class PromotionalOfferJWSInputIOS: var jws: String = "" static func from_dict(data: Dictionary) -> PromotionalOfferJWSInputIOS: + if not data.has("offerId") or not data["offerId"] is String: + push_error("Invalid required PromotionalOfferJWSInputIOS.offerId value") + return null + if not data.has("jws") or not data["jws"] is String: + push_error("Invalid required PromotionalOfferJWSInputIOS.jws value") + return null var obj = PromotionalOfferJWSInputIOS.new() if data.has("offerId") and data["offerId"] != null: obj.offer_id = data["offerId"] @@ -4182,9 +4323,9 @@ class PurchaseInput: obj.purchase_token = data["purchaseToken"] if data.has("store") and data["store"] != null: var enum_str = data["store"] - if enum_str is String: - obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN) - elif enum_str is int: + if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str): + obj.store = IAP_STORE_FROM_STRING[enum_str] + elif enum_str is int and IAP_STORE_VALUES.has(enum_str): obj.store = enum_str else: obj.store = IapStore.UNKNOWN @@ -4192,9 +4333,9 @@ class PurchaseInput: obj.quantity = data["quantity"] if data.has("purchaseState") and data["purchaseState"] != null: var enum_str = data["purchaseState"] - if enum_str is String: - obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN) - elif enum_str is int: + if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str): + obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str] + elif enum_str is int and PURCHASE_STATE_VALUES.has(enum_str): obj.purchase_state = enum_str else: obj.purchase_state = PurchaseState.UNKNOWN @@ -4239,6 +4380,15 @@ class PurchaseOptions: var include_suspended_android: Variant = null static func from_dict(data: Dictionary) -> PurchaseOptions: + if data.has("alsoPublishToEventListenerIOS") and data["alsoPublishToEventListenerIOS"] != null and not data["alsoPublishToEventListenerIOS"] is bool: + push_error("Invalid PurchaseOptions.alsoPublishToEventListenerIOS value") + return null + if data.has("onlyIncludeActiveItemsIOS") and data["onlyIncludeActiveItemsIOS"] != null and not data["onlyIncludeActiveItemsIOS"] is bool: + push_error("Invalid PurchaseOptions.onlyIncludeActiveItemsIOS value") + return null + if data.has("includeSuspendedAndroid") and data["includeSuspendedAndroid"] != null and not data["includeSuspendedAndroid"] is bool: + push_error("Invalid PurchaseOptions.includeSuspendedAndroid value") + return null var obj = PurchaseOptions.new() if data.has("alsoPublishToEventListenerIOS") and data["alsoPublishToEventListenerIOS"] != null: obj.also_publish_to_event_listener_ios = data["alsoPublishToEventListenerIOS"] @@ -4263,6 +4413,9 @@ class PurchaseUpdatedListenerOptions: var dedupe_transaction_ios: Variant = null static func from_dict(data: Dictionary) -> PurchaseUpdatedListenerOptions: + if data.has("dedupeTransactionIOS") and data["dedupeTransactionIOS"] != null and not data["dedupeTransactionIOS"] is bool: + push_error("Invalid PurchaseUpdatedListenerOptions.dedupeTransactionIOS value") + return null var obj = PurchaseUpdatedListenerOptions.new() if data.has("dedupeTransactionIOS") and data["dedupeTransactionIOS"] != null: obj.dedupe_transaction_ios = data["dedupeTransactionIOS"] @@ -4289,6 +4442,21 @@ class RequestPurchaseAndroidProps: var developer_billing_option: DeveloperBillingOptionParamsAndroid static func from_dict(data: Dictionary) -> RequestPurchaseAndroidProps: + if not data.has("skus") or not data["skus"] is Array: + push_error("Invalid required RequestPurchaseAndroidProps.skus value") + return null + if data.has("obfuscatedAccountId") and data["obfuscatedAccountId"] != null and not data["obfuscatedAccountId"] is String: + push_error("Invalid RequestPurchaseAndroidProps.obfuscatedAccountId value") + return null + if data.has("obfuscatedProfileId") and data["obfuscatedProfileId"] != null and not data["obfuscatedProfileId"] is String: + push_error("Invalid RequestPurchaseAndroidProps.obfuscatedProfileId value") + return null + if data.has("isOfferPersonalized") and data["isOfferPersonalized"] != null and not data["isOfferPersonalized"] is bool: + push_error("Invalid RequestPurchaseAndroidProps.isOfferPersonalized value") + return null + if data.has("offerToken") and data["offerToken"] != null and not data["offerToken"] is String: + push_error("Invalid RequestPurchaseAndroidProps.offerToken value") + return null var obj = RequestPurchaseAndroidProps.new() if data.has("skus") and data["skus"] != null: if data["skus"] is Array: @@ -4296,7 +4464,13 @@ class RequestPurchaseAndroidProps: for item in data["skus"]: if item is String: arr.append(str(item)) + else: + push_error("Invalid String list value for skus") + return null obj.skus = arr + else: + push_error("Invalid input list for skus") + return null if data.has("obfuscatedAccountId") and data["obfuscatedAccountId"] != null: obj.obfuscated_account_id = data["obfuscatedAccountId"] if data.has("obfuscatedProfileId") and data["obfuscatedProfileId"] != null: @@ -4351,6 +4525,21 @@ class RequestPurchaseIosProps: var advanced_commerce_data: Variant = null static func from_dict(data: Dictionary) -> RequestPurchaseIosProps: + if not data.has("sku") or not data["sku"] is String: + push_error("Invalid required RequestPurchaseIosProps.sku value") + return null + if data.has("andDangerouslyFinishTransactionAutomatically") and data["andDangerouslyFinishTransactionAutomatically"] != null and not data["andDangerouslyFinishTransactionAutomatically"] is bool: + push_error("Invalid RequestPurchaseIosProps.andDangerouslyFinishTransactionAutomatically value") + return null + if data.has("appAccountToken") and data["appAccountToken"] != null and not data["appAccountToken"] is String: + push_error("Invalid RequestPurchaseIosProps.appAccountToken value") + return null + if data.has("quantity") and data["quantity"] != null and not data["quantity"] is int: + push_error("Invalid RequestPurchaseIosProps.quantity value") + return null + if data.has("advancedCommerceData") and data["advancedCommerceData"] != null and not data["advancedCommerceData"] is String: + push_error("Invalid RequestPurchaseIosProps.advancedCommerceData value") + return null var obj = RequestPurchaseIosProps.new() if data.has("sku") and data["sku"] != null: obj.sku = data["sku"] @@ -4422,10 +4611,20 @@ class RequestPurchaseProps: var obj = RequestPurchaseProps.new() if has_purchase: var purchase_value = data["requestPurchase"] - obj.request = RequestPurchasePropsByPlatforms.from_dict(purchase_value) if purchase_value is Dictionary else purchase_value + if not purchase_value is Dictionary: + push_error("requestPurchase must be a dictionary") + return null + obj.request = RequestPurchasePropsByPlatforms.from_dict(purchase_value) + if obj.request == null: + return null else: var subscription_value = data["requestSubscription"] - obj.request_subscription = RequestSubscriptionPropsByPlatforms.from_dict(subscription_value) if subscription_value is Dictionary else subscription_value + if not subscription_value is Dictionary: + push_error("requestSubscription must be a dictionary") + return null + obj.request_subscription = RequestSubscriptionPropsByPlatforms.from_dict(subscription_value) + if obj.request_subscription == null: + return null var expected_type = ProductQueryType.IN_APP if has_purchase else ProductQueryType.SUBS obj.type = expected_type if data.has("type") and data["type"] != null: @@ -4520,6 +4719,24 @@ class RequestSubscriptionAndroidProps: var developer_billing_option: DeveloperBillingOptionParamsAndroid static func from_dict(data: Dictionary) -> RequestSubscriptionAndroidProps: + if not data.has("skus") or not data["skus"] is Array: + push_error("Invalid required RequestSubscriptionAndroidProps.skus value") + return null + if data.has("obfuscatedAccountId") and data["obfuscatedAccountId"] != null and not data["obfuscatedAccountId"] is String: + push_error("Invalid RequestSubscriptionAndroidProps.obfuscatedAccountId value") + return null + if data.has("obfuscatedProfileId") and data["obfuscatedProfileId"] != null and not data["obfuscatedProfileId"] is String: + push_error("Invalid RequestSubscriptionAndroidProps.obfuscatedProfileId value") + return null + if data.has("isOfferPersonalized") and data["isOfferPersonalized"] != null and not data["isOfferPersonalized"] is bool: + push_error("Invalid RequestSubscriptionAndroidProps.isOfferPersonalized value") + return null + if data.has("purchaseToken") and data["purchaseToken"] != null and not data["purchaseToken"] is String: + push_error("Invalid RequestSubscriptionAndroidProps.purchaseToken value") + return null + if data.has("originalExternalTransactionId") and data["originalExternalTransactionId"] != null and not data["originalExternalTransactionId"] is String: + push_error("Invalid RequestSubscriptionAndroidProps.originalExternalTransactionId value") + return null var obj = RequestSubscriptionAndroidProps.new() if data.has("skus") and data["skus"] != null: if data["skus"] is Array: @@ -4527,7 +4744,13 @@ class RequestSubscriptionAndroidProps: for item in data["skus"]: if item is String: arr.append(str(item)) + else: + push_error("Invalid String list value for skus") + return null obj.skus = arr + else: + push_error("Invalid input list for skus") + return null if data.has("obfuscatedAccountId") and data["obfuscatedAccountId"] != null: obj.obfuscated_account_id = data["obfuscatedAccountId"] if data.has("obfuscatedProfileId") and data["obfuscatedProfileId"] != null: @@ -4543,13 +4766,19 @@ class RequestSubscriptionAndroidProps: var arr: Array[AndroidSubscriptionOfferInput] = [] for item in data["subscriptionOffers"]: if item is Dictionary: - arr.append(AndroidSubscriptionOfferInput.from_dict(item)) + var decoded_android_subscription_offer_input = AndroidSubscriptionOfferInput.from_dict(item) + if decoded_android_subscription_offer_input == null: + return null + arr.append(decoded_android_subscription_offer_input) elif item is AndroidSubscriptionOfferInput: arr.append(item) else: push_error("Invalid AndroidSubscriptionOfferInput list value for subscriptionOffers") return null obj.subscription_offers = arr + else: + push_error("Invalid input list for subscriptionOffers") + return null if data.has("subscriptionProductReplacementParams") and data["subscriptionProductReplacementParams"] != null: if data["subscriptionProductReplacementParams"] is Dictionary: var decoded_subscription_product_replacement_params = SubscriptionProductReplacementParamsAndroid.from_dict(data["subscriptionProductReplacementParams"]) @@ -4625,6 +4854,24 @@ class RequestSubscriptionIosProps: var advanced_commerce_data: Variant = null static func from_dict(data: Dictionary) -> RequestSubscriptionIosProps: + if not data.has("sku") or not data["sku"] is String: + push_error("Invalid required RequestSubscriptionIosProps.sku value") + return null + if data.has("andDangerouslyFinishTransactionAutomatically") and data["andDangerouslyFinishTransactionAutomatically"] != null and not data["andDangerouslyFinishTransactionAutomatically"] is bool: + push_error("Invalid RequestSubscriptionIosProps.andDangerouslyFinishTransactionAutomatically value") + return null + if data.has("appAccountToken") and data["appAccountToken"] != null and not data["appAccountToken"] is String: + push_error("Invalid RequestSubscriptionIosProps.appAccountToken value") + return null + if data.has("quantity") and data["quantity"] != null and not data["quantity"] is int: + push_error("Invalid RequestSubscriptionIosProps.quantity value") + return null + if data.has("compactJWS") and data["compactJWS"] != null and not data["compactJWS"] is String: + push_error("Invalid RequestSubscriptionIosProps.compactJWS value") + return null + if data.has("advancedCommerceData") and data["advancedCommerceData"] != null and not data["advancedCommerceData"] is String: + push_error("Invalid RequestSubscriptionIosProps.advancedCommerceData value") + return null var obj = RequestSubscriptionIosProps.new() if data.has("sku") and data["sku"] != null: obj.sku = data["sku"] @@ -4666,12 +4913,13 @@ class RequestSubscriptionIosProps: return null if data.has("billingPlanType") and data["billingPlanType"] != null: var enum_str = data["billingPlanType"] - if enum_str is String: - obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - elif enum_str is int: + if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str): + obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str] + elif enum_str is int and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(enum_str): obj.billing_plan_type = enum_str else: - obj.billing_plan_type = SubscriptionBillingPlanTypeIOS.UNKNOWN + push_error("Invalid SubscriptionBillingPlanTypeIOS input value") + return null if data.has("compactJWS") and data["compactJWS"] != null: obj.compact_jws = data["compactJWS"] if data.has("advancedCommerceData") and data["advancedCommerceData"] != null: @@ -4770,6 +5018,18 @@ class RequestVerifyPurchaseWithIapkitAmazonProps: var sandbox: Variant = null static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitAmazonProps: + if data.has("expectedProductId") and data["expectedProductId"] != null and not data["expectedProductId"] is String: + push_error("Invalid RequestVerifyPurchaseWithIapkitAmazonProps.expectedProductId value") + return null + if data.has("userId") and data["userId"] != null and not data["userId"] is String: + push_error("Invalid RequestVerifyPurchaseWithIapkitAmazonProps.userId value") + return null + if not data.has("receiptId") or not data["receiptId"] is String: + push_error("Invalid required RequestVerifyPurchaseWithIapkitAmazonProps.receiptId value") + return null + if data.has("sandbox") and data["sandbox"] != null and not data["sandbox"] is bool: + push_error("Invalid RequestVerifyPurchaseWithIapkitAmazonProps.sandbox value") + return null var obj = RequestVerifyPurchaseWithIapkitAmazonProps.new() if data.has("expectedProductId") and data["expectedProductId"] != null: obj.expected_product_id = data["expectedProductId"] @@ -4798,6 +5058,9 @@ class RequestVerifyPurchaseWithIapkitAppleProps: var jws: String = "" static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitAppleProps: + if not data.has("jws") or not data["jws"] is String: + push_error("Invalid required RequestVerifyPurchaseWithIapkitAppleProps.jws value") + return null var obj = RequestVerifyPurchaseWithIapkitAppleProps.new() if data.has("jws") and data["jws"] != null: obj.jws = data["jws"] @@ -4814,6 +5077,9 @@ class RequestVerifyPurchaseWithIapkitGoogleProps: var purchase_token: String = "" static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitGoogleProps: + if not data.has("purchaseToken") or not data["purchaseToken"] is String: + push_error("Invalid required RequestVerifyPurchaseWithIapkitGoogleProps.purchaseToken value") + return null var obj = RequestVerifyPurchaseWithIapkitGoogleProps.new() if data.has("purchaseToken") and data["purchaseToken"] != null: obj.purchase_token = data["purchaseToken"] @@ -4841,6 +5107,15 @@ class RequestVerifyPurchaseWithIapkitProps: var amazon: RequestVerifyPurchaseWithIapkitAmazonProps static func from_dict(data: Dictionary) -> RequestVerifyPurchaseWithIapkitProps: + if data.has("apiKey") and data["apiKey"] != null and not data["apiKey"] is String: + push_error("Invalid RequestVerifyPurchaseWithIapkitProps.apiKey value") + return null + if data.has("baseUrl") and data["baseUrl"] != null and not data["baseUrl"] is String: + push_error("Invalid RequestVerifyPurchaseWithIapkitProps.baseUrl value") + return null + if data.has("includeClientPayload") and data["includeClientPayload"] != null and not data["includeClientPayload"] is bool: + push_error("Invalid RequestVerifyPurchaseWithIapkitProps.includeClientPayload value") + return null var obj = RequestVerifyPurchaseWithIapkitProps.new() if data.has("apiKey") and data["apiKey"] != null: obj.api_key = data["apiKey"] @@ -4913,17 +5188,24 @@ class SubscriptionProductReplacementParamsAndroid: var replacement_mode: SubscriptionReplacementModeAndroid = SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE static func from_dict(data: Dictionary) -> SubscriptionProductReplacementParamsAndroid: + if not data.has("oldProductId") or not data["oldProductId"] is String: + push_error("Invalid required SubscriptionProductReplacementParamsAndroid.oldProductId value") + return null + if not data.has("replacementMode") or not ((data["replacementMode"] is String and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING.has(data["replacementMode"])) or (data["replacementMode"] is int and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_VALUES.has(data["replacementMode"]))): + push_error("Invalid required SubscriptionProductReplacementParamsAndroid.replacementMode value") + return null var obj = SubscriptionProductReplacementParamsAndroid.new() if data.has("oldProductId") and data["oldProductId"] != null: obj.old_product_id = data["oldProductId"] if data.has("replacementMode") and data["replacementMode"] != null: var enum_str = data["replacementMode"] - if enum_str is String: - obj.replacement_mode = SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING.get(enum_str, SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE) - elif enum_str is int: + if enum_str is String and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING.has(enum_str): + obj.replacement_mode = SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_FROM_STRING[enum_str] + elif enum_str is int and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_VALUES.has(enum_str): obj.replacement_mode = enum_str else: - obj.replacement_mode = SubscriptionReplacementModeAndroid.UNKNOWN_REPLACEMENT_MODE + push_error("Invalid SubscriptionReplacementModeAndroid input value") + return null return obj func to_dict() -> Dictionary: @@ -4943,6 +5225,9 @@ class VerifyPurchaseAppleOptions: var sku: String = "" static func from_dict(data: Dictionary) -> VerifyPurchaseAppleOptions: + if not data.has("sku") or not data["sku"] is String: + push_error("Invalid required VerifyPurchaseAppleOptions.sku value") + return null var obj = VerifyPurchaseAppleOptions.new() if data.has("sku") and data["sku"] != null: obj.sku = data["sku"] @@ -4968,6 +5253,21 @@ class VerifyPurchaseGoogleOptions: var is_sub: Variant = null static func from_dict(data: Dictionary) -> VerifyPurchaseGoogleOptions: + if not data.has("sku") or not data["sku"] is String: + push_error("Invalid required VerifyPurchaseGoogleOptions.sku value") + return null + if not data.has("packageName") or not data["packageName"] is String: + push_error("Invalid required VerifyPurchaseGoogleOptions.packageName value") + return null + if not data.has("purchaseToken") or not data["purchaseToken"] is String: + push_error("Invalid required VerifyPurchaseGoogleOptions.purchaseToken value") + return null + if not data.has("accessToken") or not data["accessToken"] is String: + push_error("Invalid required VerifyPurchaseGoogleOptions.accessToken value") + return null + if data.has("isSub") and data["isSub"] != null and not data["isSub"] is bool: + push_error("Invalid VerifyPurchaseGoogleOptions.isSub value") + return null var obj = VerifyPurchaseGoogleOptions.new() if data.has("sku") and data["sku"] != null: obj.sku = data["sku"] @@ -5005,6 +5305,15 @@ class VerifyPurchaseHorizonOptions: var access_token: String = "" static func from_dict(data: Dictionary) -> VerifyPurchaseHorizonOptions: + if not data.has("sku") or not data["sku"] is String: + push_error("Invalid required VerifyPurchaseHorizonOptions.sku value") + return null + if not data.has("userId") or not data["userId"] is String: + push_error("Invalid required VerifyPurchaseHorizonOptions.userId value") + return null + if not data.has("accessToken") or not data["accessToken"] is String: + push_error("Invalid required VerifyPurchaseHorizonOptions.accessToken value") + return null var obj = VerifyPurchaseHorizonOptions.new() if data.has("sku") and data["sku"] != null: obj.sku = data["sku"] @@ -5100,8 +5409,11 @@ class VerifyPurchaseWithProviderProps: var enum_str = data["provider"] if enum_str is String and PURCHASE_VERIFICATION_PROVIDER_FROM_STRING.has(enum_str): obj.provider = PURCHASE_VERIFICATION_PROVIDER_FROM_STRING[enum_str] - else: + elif enum_str is int and PURCHASE_VERIFICATION_PROVIDER_VALUES.has(enum_str): obj.provider = enum_str + else: + push_error("Invalid PurchaseVerificationProvider input value") + return null if data.has("iapkit") and data["iapkit"] != null: if data["iapkit"] is Dictionary: var decoded_iapkit = RequestVerifyPurchaseWithIapkitProps.from_dict(data["iapkit"]) @@ -5134,6 +5446,9 @@ class WinBackOfferInputIOS: var offer_id: String = "" static func from_dict(data: Dictionary) -> WinBackOfferInputIOS: + if not data.has("offerId") or not data["offerId"] is String: + push_error("Invalid required WinBackOfferInputIOS.offerId value") + return null var obj = WinBackOfferInputIOS.new() if data.has("offerId") and data["offerId"] != null: obj.offer_id = data["offerId"] @@ -5720,7 +6035,13 @@ class Query: for item in data["subscriptionIds"]: if item is String: arr.append(str(item)) + else: + push_error("Invalid String list value for subscriptionIds") + return null obj.subscription_ids = arr + else: + push_error("Invalid input list for subscriptionIds") + return null return obj func to_dict() -> Dictionary: @@ -5746,7 +6067,13 @@ class Query: for item in data["subscriptionIds"]: if item is String: arr.append(str(item)) + else: + push_error("Invalid String list value for subscriptionIds") + return null obj.subscription_ids = arr + else: + push_error("Invalid input list for subscriptionIds") + return null return obj func to_dict() -> Dictionary: @@ -5807,8 +6134,11 @@ class Query: var enum_str = data["tokenType"] if enum_str is String and EXTERNAL_PURCHASE_CUSTOM_LINK_TOKEN_TYPE_IOS_FROM_STRING.has(enum_str): obj.token_type = EXTERNAL_PURCHASE_CUSTOM_LINK_TOKEN_TYPE_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and EXTERNAL_PURCHASE_CUSTOM_LINK_TOKEN_TYPE_IOS_VALUES.has(enum_str): obj.token_type = enum_str + else: + push_error("Invalid ExternalPurchaseCustomLinkTokenTypeIOS input value") + return null return obj func to_dict() -> Dictionary: @@ -6255,8 +6585,11 @@ class Mutation: var enum_str = data["noticeType"] if enum_str is String and EXTERNAL_PURCHASE_CUSTOM_LINK_NOTICE_TYPE_IOS_FROM_STRING.has(enum_str): obj.notice_type = EXTERNAL_PURCHASE_CUSTOM_LINK_NOTICE_TYPE_IOS_FROM_STRING[enum_str] - else: + elif enum_str is int and EXTERNAL_PURCHASE_CUSTOM_LINK_NOTICE_TYPE_IOS_VALUES.has(enum_str): obj.notice_type = enum_str + else: + push_error("Invalid ExternalPurchaseCustomLinkNoticeTypeIOS input value") + return null return obj func to_dict() -> Dictionary: @@ -6322,8 +6655,11 @@ class Mutation: var enum_str = data["program"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): obj.program = enum_str + else: + push_error("Invalid BillingProgramAndroid input value") + return null return obj func to_dict() -> Dictionary: @@ -6350,14 +6686,20 @@ class Mutation: var enum_str = data["program"] if enum_str is String and BILLING_PROGRAM_ANDROID_FROM_STRING.has(enum_str): obj.program = BILLING_PROGRAM_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and BILLING_PROGRAM_ANDROID_VALUES.has(enum_str): obj.program = enum_str + else: + push_error("Invalid BillingProgramAndroid input value") + return null if data.has("developerBillingType") and data["developerBillingType"] != null: var enum_str = data["developerBillingType"] if enum_str is String and DEVELOPER_BILLING_TYPE_ANDROID_FROM_STRING.has(enum_str): obj.developer_billing_type = DEVELOPER_BILLING_TYPE_ANDROID_FROM_STRING[enum_str] - else: + elif enum_str is int and DEVELOPER_BILLING_TYPE_ANDROID_VALUES.has(enum_str): obj.developer_billing_type = enum_str + else: + push_error("Invalid DeveloperBillingTypeAndroid input value") + return null return obj func to_dict() -> Dictionary: diff --git a/packages/kit/CONVENTION.md b/packages/kit/CONVENTION.md index 9dbb60f27..d815ce406 100644 --- a/packages/kit/CONVENTION.md +++ b/packages/kit/CONVENTION.md @@ -277,7 +277,7 @@ platform)`, inserts the row, schedules the worker via updates don't re-toast. 6. **HTTP**: `POST .../sync/...` returns 202 with `{ jobId, deduped }`; `GET .../sync/jobs/{jobId}` polls; `POST -.../sync/jobs/{jobId}/cancel` cancels. Clients backoff at ~3 s +.../sync/jobs/{jobId}/cancel` cancels. Clients use ~3 s backoff intervals. Failures arrays should pass through `truncateFailures` (cap 200, diff --git a/packages/kit/src/index.css b/packages/kit/src/index.css index b7928b9e4..e252aa3f0 100644 --- a/packages/kit/src/index.css +++ b/packages/kit/src/index.css @@ -605,7 +605,6 @@ .contained-action-card__text { @apply min-w-0 flex-1; overflow-wrap: anywhere; - word-break: break-word; } .contained-action-card__actions { diff --git a/scripts/audit-non-godot-parity.mjs b/scripts/audit-non-godot-parity.mjs index bcb0997f8..96d095ff3 100644 --- a/scripts/audit-non-godot-parity.mjs +++ b/scripts/audit-non-godot-parity.mjs @@ -2268,7 +2268,12 @@ function checkFlutter() { ); expectIncludes( "libraries/expo-iap/ios/ExpoIapHelper.swift", - ["else {\n return .inApp"], + [ + "static func parseProductQueryType(_ rawValue: Any?)", + "if rawValue == nil || rawValue is NSNull {\n return .inApp", + "guard let stringValue = rawValue as? String", + 'parseProductQueryType(payload["type"])', + ], "Expo iOS ProductRequest default", ); expectIncludes( From 9a8557788bc49c1d50bce1d8c16d91973cab0925 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 04:39:35 +0900 Subject: [PATCH 06/12] fix: close remaining purchase validation gaps --- libraries/expo-iap/src/__tests__/index.test.ts | 5 ++++- .../expo-iap/src/__tests__/native-log-redaction.test.js | 1 + libraries/expo-iap/src/index.ts | 5 ++++- libraries/godot-iap/Example/tests/test_godot_iap.gd | 1 + libraries/godot-iap/addons/godot-iap/godot_iap.gd | 2 ++ .../src/main/java/dev/hyo/godotiap/GodotIapHelper.kt | 3 +++ .../src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt | 1 + libraries/maui-iap/src/OpenIap.Maui/Types.cs | 2 ++ .../maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs | 9 +++++++++ packages/gql/codegen/plugins/csharp.ts | 2 ++ packages/gql/src/enum-decoder-compatibility.test.ts | 1 + packages/gql/src/generated/Types.cs | 2 ++ 12 files changed, 32 insertions(+), 2 deletions(-) diff --git a/libraries/expo-iap/src/__tests__/index.test.ts b/libraries/expo-iap/src/__tests__/index.test.ts index 0a949dc67..b0574563e 100644 --- a/libraries/expo-iap/src/__tests__/index.test.ts +++ b/libraries/expo-iap/src/__tests__/index.test.ts @@ -738,7 +738,10 @@ describe('Public API (index.ts)', () => { request: {google: {skus: ['x']}} as any, type: 'all' as any, }), - ).rejects.toThrow(/only supported for product queries/); + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: expect.stringMatching(/only supported for product queries/), + }); }); it('Android subscription requests require skus array', async () => { diff --git a/libraries/expo-iap/src/__tests__/native-log-redaction.test.js b/libraries/expo-iap/src/__tests__/native-log-redaction.test.js index 6926e3cb3..1619b078e 100644 --- a/libraries/expo-iap/src/__tests__/native-log-redaction.test.js +++ b/libraries/expo-iap/src/__tests__/native-log-redaction.test.js @@ -112,6 +112,7 @@ describe('native log redaction', () => { purchaseBlock.indexOf('try await ensureObserverRegistered()'), ); expect(onsideModule).toContain('switch (request.type, request.request)'); + expect(fetchBlock).toContain('decodeProductRequest(from: params)'); expect( fetchBlock.indexOf('decodeProductRequest(from: params)'), ).toBeLessThan(fetchBlock.indexOf('try await ensureObserverRegistered()')); diff --git a/libraries/expo-iap/src/index.ts b/libraries/expo-iap/src/index.ts index 1e0d33b30..4a89823f6 100644 --- a/libraries/expo-iap/src/index.ts +++ b/libraries/expo-iap/src/index.ts @@ -986,7 +986,10 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( const {request, type} = args; const {canonical, native} = normalizeProductType(type as ProductTypeInput); if (canonical === 'all') { - throw new Error('Product type all is only supported for product queries.'); + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); } const isInAppPurchase = canonical === 'in-app'; diff --git a/libraries/godot-iap/Example/tests/test_godot_iap.gd b/libraries/godot-iap/Example/tests/test_godot_iap.gd index c85850082..9ba3f5ec2 100644 --- a/libraries/godot-iap/Example/tests/test_godot_iap.gd +++ b/libraries/godot-iap/Example/tests/test_godot_iap.gd @@ -272,6 +272,7 @@ func test_android_purchase_lists_fail_closed() -> void: GodotIapPlugin._platform = "Android" var invalid_google_payloads = [ + {"skus": []}, {"skus": ["monthly", 7]}, {"skus": ["coins"], "offerToken": 7}, {"skus": ["monthly"], "offerToken": "one-time-token"}, diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index 65978360c..5c780efb4 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -529,6 +529,8 @@ func request_purchase(props) -> Variant: func _is_nonempty_string_array(value) -> bool: if not value is Array: return false + if value.is_empty(): + return false for item in value: if not item is String or item.is_empty(): return false diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt index 7100bd771..7f01835b8 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt @@ -111,6 +111,9 @@ internal object GodotIapHelper { } skus.add(sku) } + if (skus.isEmpty()) { + throw IllegalArgumentException("skus must contain at least one product id") + } val obfuscatedAccountId = json.optStringOrNull("obfuscatedAccountId") val obfuscatedProfileId = json.optStringOrNull("obfuscatedProfileId") diff --git a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt index 8a0cc9bc3..392b3a498 100644 --- a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt +++ b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt @@ -118,6 +118,7 @@ class GodotIapHelperTest { fun `purchase lists reject malformed explicit entries`() { listOf( """{"type":"subs","skus":"monthly"}""", + """{"type":"subs","skus":[]}""", """{"type":"subs","skus":["monthly",7]}""", """{"type":"subs","skus":[""]}""", """{"type":"subs","skus":["monthly"],"obfuscatedAccountId":7}""", diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index 79f56d862..726a73d14 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -27,6 +27,7 @@ public sealed class StrictEnumJsonConverter : JsonConverter : JsonCon public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType != JsonTokenType.String) throw new JsonException($"Expected a string {typeof(TEnum).Name} input value."); var raw = reader.GetString(); if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value; throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}"); diff --git a/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs b/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs index b4e18552a..6f9aea65c 100644 --- a/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs +++ b/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs @@ -131,6 +131,15 @@ public void InAppMessageInput_RejectsUnknownAndMalformedCategories() """{"categories":"transactional"}""")); } + [Fact] + public void RequestEnumInputs_RejectNonStringTokensAsJsonErrors() + { + Assert.Throws(() => JsonSerializer.Deserialize( + """{"billingProgram":7}""")); + Assert.Throws(() => JsonSerializer.Deserialize( + """{"enableBillingProgramAndroid":true}""")); + } + [Theory] [InlineData(ProductType.InApp, "in-app")] [InlineData(ProductType.Subs, "subs")] diff --git a/packages/gql/codegen/plugins/csharp.ts b/packages/gql/codegen/plugins/csharp.ts index bef21fef3..a4142c179 100644 --- a/packages/gql/codegen/plugins/csharp.ts +++ b/packages/gql/codegen/plugins/csharp.ts @@ -261,6 +261,7 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(''); this.emit(' public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)'); this.emit(' {'); + this.emit(' if (reader.TokenType != JsonTokenType.String) throw new JsonException($"Expected a string {typeof(TEnum).Name} input value.");'); this.emit(' var raw = reader.GetString();'); this.emit(' if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value;'); this.emit(' throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}");'); @@ -278,6 +279,7 @@ export class CSharpPlugin extends CodegenPlugin { this.emit(''); this.emit(' public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)'); this.emit(' {'); + this.emit(' if (reader.TokenType != JsonTokenType.String) throw new JsonException($"Expected a string {typeof(TEnum).Name} input value.");'); this.emit(' var raw = reader.GetString();'); this.emit(' if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value;'); this.emit(' throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}");'); diff --git a/packages/gql/src/enum-decoder-compatibility.test.ts b/packages/gql/src/enum-decoder-compatibility.test.ts index 18e6a8063..7b46c93fc 100644 --- a/packages/gql/src/enum-decoder-compatibility.test.ts +++ b/packages/gql/src/enum-decoder-compatibility.test.ts @@ -360,6 +360,7 @@ describe('unknown enum decoder compatibility', () => { expect(output).not.toContain('InputPayloadNullableJsonConverter'); expect(output).toContain('[JsonConverter(typeof(PayloadNullableJsonConverter))]'); expect(output).toContain('public sealed class StrictEnumJsonConverter'); + expect(output.match(/reader\.TokenType != JsonTokenType\.String/g)).toHaveLength(2); expect(output).toContain( '[JsonConverter(typeof(StrictEnumJsonConverter))]', ); diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index 79f56d862..726a73d14 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -27,6 +27,7 @@ public sealed class StrictEnumJsonConverter : JsonConverter : JsonCon public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType != JsonTokenType.String) throw new JsonException($"Expected a string {typeof(TEnum).Name} input value."); var raw = reader.GetString(); if (raw is not null && Converter.TryReadRaw(raw, out var value)) return value; throw new JsonException($"Unknown {typeof(TEnum).Name} input value: {raw}"); From df03e2da48aae82c90bf41c20a6d4921a6f323e9 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 04:42:31 +0900 Subject: [PATCH 07/12] fix: classify query-only purchase errors --- libraries/expo-iap/src/__tests__/index.kepler.test.ts | 6 +++++- libraries/expo-iap/src/index.kepler.ts | 6 +++++- .../react-native-iap/src/__tests__/index.kepler.test.ts | 6 +++++- libraries/react-native-iap/src/__tests__/index.test.ts | 5 ++++- libraries/react-native-iap/src/index.kepler.ts | 7 ++++++- libraries/react-native-iap/src/index.ts | 7 ++++--- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/libraries/expo-iap/src/__tests__/index.kepler.test.ts b/libraries/expo-iap/src/__tests__/index.kepler.test.ts index 9bc10cd6f..346446006 100644 --- a/libraries/expo-iap/src/__tests__/index.kepler.test.ts +++ b/libraries/expo-iap/src/__tests__/index.kepler.test.ts @@ -5,6 +5,7 @@ import { requestPurchase, } from '../index.kepler'; import * as Kepler from '../index.kepler'; +import {ErrorCode} from '../types'; import {getVegaIapModule} from '../vega'; jest.mock('../vega', () => ({ @@ -78,7 +79,10 @@ describe('Amazon Vega public API', () => { request: {google: {skus: ['coins']}}, type: 'all' as any, }), - ).rejects.toThrow(/only supported for product queries/); + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: expect.stringMatching(/only supported for product queries/), + }); expect(requestPurchaseNative).not.toHaveBeenCalled(); }); diff --git a/libraries/expo-iap/src/index.kepler.ts b/libraries/expo-iap/src/index.kepler.ts index e6f286885..3efae3527 100644 --- a/libraries/expo-iap/src/index.kepler.ts +++ b/libraries/expo-iap/src/index.kepler.ts @@ -13,6 +13,7 @@ import type { RequestSubscriptionPropsByPlatforms, } from './types'; import {decodeAndroidPurchases} from './utils/availablePurchases'; +import {createPurchaseError} from './utils/errorMapping'; export * from './types'; export * from './vega'; @@ -150,7 +151,10 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ) => { const type = normalizeProductType(args.type); if (type === 'all') { - throw new Error('Product type all is only supported for product queries.'); + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); } const androidRequest = getAndroidRequest(args.request); if (!androidRequest?.skus?.length) { diff --git a/libraries/react-native-iap/src/__tests__/index.kepler.test.ts b/libraries/react-native-iap/src/__tests__/index.kepler.test.ts index 6cfe4629a..03cfabc33 100644 --- a/libraries/react-native-iap/src/__tests__/index.kepler.test.ts +++ b/libraries/react-native-iap/src/__tests__/index.kepler.test.ts @@ -1,4 +1,5 @@ import * as IAP from '../index.kepler'; +import {ErrorCode} from '../types'; import type {RequestPurchaseProps} from '../types'; import {getVegaIapModule} from '../vega'; @@ -72,7 +73,10 @@ describe('Amazon Vega public API', () => { request: {google: {skus: ['coins']}}, type: 'all' as any, }), - ).rejects.toThrow(/only supported for product queries/); + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: expect.stringMatching(/only supported for product queries/), + }); expect(requestPurchaseNative).not.toHaveBeenCalled(); }); diff --git a/libraries/react-native-iap/src/__tests__/index.test.ts b/libraries/react-native-iap/src/__tests__/index.test.ts index c30ca7ea8..79d0e0e62 100644 --- a/libraries/react-native-iap/src/__tests__/index.test.ts +++ b/libraries/react-native-iap/src/__tests__/index.test.ts @@ -755,7 +755,10 @@ describe('Public API (src/index.ts)', () => { request: {google: {skus: ['p1']}}, type: 'all' as any, }), - ).rejects.toThrow(/only supported for product queries/); + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: expect.stringMatching(/only supported for product queries/), + }); expect(mockIap.requestPurchase).not.toHaveBeenCalled(); }); diff --git a/libraries/react-native-iap/src/index.kepler.ts b/libraries/react-native-iap/src/index.kepler.ts index 93d20f92c..337dc2cf1 100644 --- a/libraries/react-native-iap/src/index.kepler.ts +++ b/libraries/react-native-iap/src/index.kepler.ts @@ -1,4 +1,5 @@ import {getVegaIapModule} from './vega'; +import {ErrorCode} from './types'; import type { MutationField, Product, @@ -27,6 +28,7 @@ import { validateNitroPurchase, } from './utils/type-bridge'; import {convertAndroidPurchasesOrThrow} from './utils/available-purchases'; +import {createPurchaseError} from './utils/errorMapping'; export * from './types'; export * from './utils/error'; @@ -137,7 +139,10 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ) => { const type = normalizeProductQueryType(request.type); if (type === 'all') { - throw new Error('Product type all is only supported for product queries.'); + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); } const perPlatformRequest = request.request as | RequestPurchasePropsByPlatforms diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index 5390a3505..3262b61fa 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -1679,9 +1679,10 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( const {request: platformRequest, type} = request; const normalizedType = normalizeProductQueryType(type ?? 'in-app'); if (normalizedType === 'all') { - throw new Error( - 'Product type all is only supported for product queries.', - ); + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); } const isSubs = isSubscriptionQuery(normalizedType); const perPlatformRequest = platformRequest as From ebbcebbd54905b21a085b7f29379a5e4ef8ab17c Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 06:08:01 +0900 Subject: [PATCH 08/12] fix: reject invalid message categories --- .../AndroidInappPurchasePlugin.kt | 16 +++++++---- .../ValidatedFlutterInAppMessageParams.kt | 6 ++++ .../InAppMessageParamsValidationTest.kt | 28 +++++++++++++++++++ .../main/java/dev/hyo/godotiap/GodotIap.kt | 11 ++------ .../java/dev/hyo/godotiap/GodotIapHelper.kt | 4 +++ .../dev/hyo/godotiap/GodotIapHelperTest.kt | 19 +++++++++++++ 6 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt create mode 100644 libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt index d2f16c264..21231c325 100644 --- a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt @@ -21,8 +21,6 @@ import dev.hyo.openiap.FetchProductsResultAll import dev.hyo.openiap.FetchProductsResultProducts import dev.hyo.openiap.FetchProductsResultSubscriptions import dev.hyo.openiap.GetBillingChoiceInfoParamsAndroid -import dev.hyo.openiap.InAppMessageCategoryAndroid -import dev.hyo.openiap.InAppMessageParamsAndroid import dev.hyo.openiap.InitConnectionConfig import dev.hyo.openiap.LaunchExternalLinkParamsAndroid import dev.hyo.openiap.OpenIapError @@ -863,7 +861,16 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } "showInAppMessagesAndroid" -> { - val categories = call.argument?>("categories") + val params = try { + validateFlutterInAppMessageParams(call.argument("categories")) + } catch (e: IllegalArgumentException) { + safe.error( + OpenIapError.DeveloperError.CODE, + OpenIapError.DeveloperError.MESSAGE, + e.message, + ) + return + } scope.launch { try { val iap = openIap @@ -876,9 +883,6 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, "Activity not available") return@launch } - val params = InAppMessageParamsAndroid( - categories = categories?.map { InAppMessageCategoryAndroid.fromJson(it) } - ) val result = iap.showInAppMessages(act, params) val response = JSONObject().apply { put("responseCode", result.responseCode.toJson()) diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt new file mode 100644 index 000000000..502a21a2e --- /dev/null +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt @@ -0,0 +1,6 @@ +package io.github.hyochan.flutter_inapp_purchase + +import dev.hyo.openiap.InAppMessageParamsAndroid + +internal fun validateFlutterInAppMessageParams(categories: Any?): InAppMessageParamsAndroid = + InAppMessageParamsAndroid.fromJson(mapOf("categories" to categories)) diff --git a/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt b/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt new file mode 100644 index 000000000..6f72b8753 --- /dev/null +++ b/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt @@ -0,0 +1,28 @@ +package io.github.hyochan.flutter_inapp_purchase + +import dev.hyo.openiap.InAppMessageCategoryAndroid +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class InAppMessageParamsValidationTest { + @Test + fun `known message categories are accepted`() { + val params = validateFlutterInAppMessageParams(listOf("transactional")) + + assertEquals(InAppMessageCategoryAndroid.Transactional, params.categories?.single()) + } + + @Test + fun `malformed message categories are rejected atomically`() { + listOf( + listOf("future-category"), + listOf(999), + "transactional", + ).forEach { categories -> + assertThrows(IllegalArgumentException::class.java) { + validateFlutterInAppMessageParams(categories) + } + } + } +} diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt index ea37000b0..6f2299e90 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt @@ -1077,17 +1077,10 @@ class GodotIap(godot: Godot) : GodotPlugin(godot) { return runBlocking { try { - val json = JSONObject(paramsJson.ifBlank { "{}" }) - val categoriesJson = json.optJSONArray("categories") - val categories = categoriesJson?.let { array -> - (0 until array.length()).mapNotNull { index -> - array.optString(index, "").takeIf { it.isNotBlank() } - ?.let { InAppMessageCategoryAndroid.fromJson(it) } - } - } + val params = GodotIapHelper.parseInAppMessageParams(paramsJson) val result = store.showInAppMessages( activity, - InAppMessageParamsAndroid(categories = categories) + params ) JSONObject().apply { put("success", true) diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt index 7f01835b8..559102c60 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt @@ -2,6 +2,7 @@ package dev.hyo.godotiap import dev.hyo.openiap.AndroidSubscriptionOfferInput import dev.hyo.openiap.DeveloperBillingOptionParamsAndroid +import dev.hyo.openiap.InAppMessageParamsAndroid import dev.hyo.openiap.ProductQueryType import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid import dev.hyo.openiap.SubscriptionReplacementModeAndroid @@ -222,6 +223,9 @@ internal object GodotIapHelper { ) } + fun parseInAppMessageParams(paramsJson: String): InAppMessageParamsAndroid = + InAppMessageParamsAndroid.fromJson(jsonObjectToMap(JSONObject(paramsJson.ifBlank { "{}" }))) + /** * Parse subscription replacement mode from string. * Maps string values to SubscriptionReplacementModeAndroid enum. diff --git a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt index 392b3a498..6d0416c24 100644 --- a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt +++ b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt @@ -1,6 +1,7 @@ package dev.hyo.godotiap import dev.hyo.openiap.BillingProgramAndroid +import dev.hyo.openiap.InAppMessageCategoryAndroid import dev.hyo.openiap.ProductQueryType import dev.hyo.openiap.SubscriptionReplacementModeAndroid import org.junit.Assert.assertEquals @@ -8,6 +9,24 @@ import org.junit.Assert.assertThrows import org.junit.Test class GodotIapHelperTest { + @Test + fun `in-app message categories reject malformed input`() { + val params = GodotIapHelper.parseInAppMessageParams( + """{"categories":["transactional"]}""", + ) + assertEquals(InAppMessageCategoryAndroid.Transactional, params.categories?.single()) + + listOf( + """{"categories":["future-category"]}""", + """{"categories":[999]}""", + """{"categories":"transactional"}""", + ).forEach { json -> + assertThrows(IllegalArgumentException::class.java) { + GodotIapHelper.parseInAppMessageParams(json) + } + } + } + @Test fun `canonical product query types preserve their exact meaning`() { assertEquals( From a3bc4e8a56cfa6de244c39f6fcbbe4b84d19e002 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 08:50:52 +0900 Subject: [PATCH 09/12] fix: validate message inputs locally --- .../ValidatedFlutterInAppMessageParams.kt | 20 +++++++++++-- .../InAppMessageParamsValidationTest.kt | 20 +++++++++++-- .../main/java/dev/hyo/godotiap/GodotIap.kt | 2 +- .../java/dev/hyo/godotiap/GodotIapHelper.kt | 4 --- .../ValidatedGodotInAppMessageParams.kt | 28 +++++++++++++++++++ .../dev/hyo/godotiap/GodotIapHelperTest.kt | 22 ++++++++++++--- 6 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/ValidatedGodotInAppMessageParams.kt diff --git a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt index 502a21a2e..ceda1acd3 100644 --- a/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt @@ -1,6 +1,22 @@ package io.github.hyochan.flutter_inapp_purchase +import dev.hyo.openiap.InAppMessageCategoryAndroid import dev.hyo.openiap.InAppMessageParamsAndroid -internal fun validateFlutterInAppMessageParams(categories: Any?): InAppMessageParamsAndroid = - InAppMessageParamsAndroid.fromJson(mapOf("categories" to categories)) +internal fun validateFlutterInAppMessageParams(categories: Any?): InAppMessageParamsAndroid { + if (categories == null) return InAppMessageParamsAndroid() + require(categories is List<*>) { "categories must be a list" } + return InAppMessageParamsAndroid( + categories = categories.map(::parseFlutterInAppMessageCategory), + ) +} + +private fun parseFlutterInAppMessageCategory(value: Any?): InAppMessageCategoryAndroid = + when (value) { + "unknown-in-app-message-category-id", + "UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID", + "UnknownInAppMessageCategoryId", + -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId + "transactional", "TRANSACTIONAL", "Transactional" -> InAppMessageCategoryAndroid.Transactional + else -> throw IllegalArgumentException("Unknown in-app message category input: $value") + } diff --git a/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt b/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt index 6f72b8753..e882b39d0 100644 --- a/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt +++ b/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt @@ -6,11 +6,27 @@ import org.junit.Assert.assertThrows import org.junit.Test class InAppMessageParamsValidationTest { + @Test + fun `omitted categories preserve the transactional default`() { + assertEquals( + listOf(InAppMessageCategoryAndroid.Transactional), + validateFlutterInAppMessageParams(null).categories, + ) + } + @Test fun `known message categories are accepted`() { - val params = validateFlutterInAppMessageParams(listOf("transactional")) + val params = validateFlutterInAppMessageParams( + listOf("transactional", "unknown-in-app-message-category-id"), + ) - assertEquals(InAppMessageCategoryAndroid.Transactional, params.categories?.single()) + assertEquals( + listOf( + InAppMessageCategoryAndroid.Transactional, + InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId, + ), + params.categories, + ) } @Test diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt index 6f2299e90..482b8d388 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt @@ -1077,7 +1077,7 @@ class GodotIap(godot: Godot) : GodotPlugin(godot) { return runBlocking { try { - val params = GodotIapHelper.parseInAppMessageParams(paramsJson) + val params = validateGodotInAppMessageParams(paramsJson) val result = store.showInAppMessages( activity, params diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt index 559102c60..7f01835b8 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt @@ -2,7 +2,6 @@ package dev.hyo.godotiap import dev.hyo.openiap.AndroidSubscriptionOfferInput import dev.hyo.openiap.DeveloperBillingOptionParamsAndroid -import dev.hyo.openiap.InAppMessageParamsAndroid import dev.hyo.openiap.ProductQueryType import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid import dev.hyo.openiap.SubscriptionReplacementModeAndroid @@ -223,9 +222,6 @@ internal object GodotIapHelper { ) } - fun parseInAppMessageParams(paramsJson: String): InAppMessageParamsAndroid = - InAppMessageParamsAndroid.fromJson(jsonObjectToMap(JSONObject(paramsJson.ifBlank { "{}" }))) - /** * Parse subscription replacement mode from string. * Maps string values to SubscriptionReplacementModeAndroid enum. diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/ValidatedGodotInAppMessageParams.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/ValidatedGodotInAppMessageParams.kt new file mode 100644 index 000000000..3bd9c13b4 --- /dev/null +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/ValidatedGodotInAppMessageParams.kt @@ -0,0 +1,28 @@ +package dev.hyo.godotiap + +import dev.hyo.openiap.InAppMessageCategoryAndroid +import dev.hyo.openiap.InAppMessageParamsAndroid +import org.json.JSONArray +import org.json.JSONObject + +internal fun validateGodotInAppMessageParams(paramsJson: String): InAppMessageParamsAndroid { + val json = JSONObject(paramsJson.ifBlank { "{}" }) + if (!json.has("categories") || json.isNull("categories")) return InAppMessageParamsAndroid() + val categories = json.opt("categories") as? JSONArray + ?: throw IllegalArgumentException("categories must be an array") + return InAppMessageParamsAndroid( + categories = List(categories.length()) { index -> + parseGodotInAppMessageCategory(categories.opt(index)) + }, + ) +} + +private fun parseGodotInAppMessageCategory(value: Any?): InAppMessageCategoryAndroid = + when (value) { + "unknown-in-app-message-category-id", + "UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID", + "UnknownInAppMessageCategoryId", + -> InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId + "transactional", "TRANSACTIONAL", "Transactional" -> InAppMessageCategoryAndroid.Transactional + else -> throw IllegalArgumentException("Unknown in-app message category input: $value") + } diff --git a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt index 6d0416c24..4887b305e 100644 --- a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt +++ b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt @@ -9,12 +9,26 @@ import org.junit.Assert.assertThrows import org.junit.Test class GodotIapHelperTest { + @Test + fun `omitted in-app message categories preserve the transactional default`() { + assertEquals( + listOf(InAppMessageCategoryAndroid.Transactional), + validateGodotInAppMessageParams("{}").categories, + ) + } + @Test fun `in-app message categories reject malformed input`() { - val params = GodotIapHelper.parseInAppMessageParams( - """{"categories":["transactional"]}""", + val params = validateGodotInAppMessageParams( + """{"categories":["transactional","unknown-in-app-message-category-id"]}""", + ) + assertEquals( + listOf( + InAppMessageCategoryAndroid.Transactional, + InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId, + ), + params.categories, ) - assertEquals(InAppMessageCategoryAndroid.Transactional, params.categories?.single()) listOf( """{"categories":["future-category"]}""", @@ -22,7 +36,7 @@ class GodotIapHelperTest { """{"categories":"transactional"}""", ).forEach { json -> assertThrows(IllegalArgumentException::class.java) { - GodotIapHelper.parseInAppMessageParams(json) + validateGodotInAppMessageParams(json) } } } From 5000cef06c8dfd09f050039b879de412f34e3767 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 08:51:06 +0900 Subject: [PATCH 10/12] docs: add Record Scanner to showcase --- .../docs/public/showcase/record-scanner.webp | Bin 0 -> 9540 bytes packages/docs/showcase-apps.json | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 packages/docs/public/showcase/record-scanner.webp diff --git a/packages/docs/public/showcase/record-scanner.webp b/packages/docs/public/showcase/record-scanner.webp new file mode 100644 index 0000000000000000000000000000000000000000..26d1734b27f6cec7ef608e04b50d30c36bbaf3d2 GIT binary patch literal 9540 zcmV-KCA->ENk&FIB>(_dMM6+kP&il$0000G000300093006|PpNa_#(00F>9ZJ#5j zv3^z6-6M%*NlQ+`LT8b15O zB1$3%j3k6K-Cb3`b7p#krl-5=ravMA7HwNqs&&j~Qh9H7K!kmM;s1>oA>@DZP{WAL z7)#FA&0$|-+f-(E%nKUpoi~-PRM|~XUUx!5q z5FPW#uUK#2w6XHdE&xo&`#jZ%wiJlfZ{H1I=$PjkL>&3+j-mcQ2&IfMBW|YYNr^E= zDIrAJqnqAQ?LQ?8@fbkz?@Zm)BnP348Kz}fra_zk&dm6`G8m&o!3drNgfhdp#Se0% zUFXi3xqbWO^cz0BUrwLgoa~-m+eB!SZ#Dj1q(spql{+yb=^3rNW%nEZWI@5q)oYip z-15;!TUIV#vufe=X;U7!uYdPe8EKZmsB#ycZfN!bp@tPP6X-hN;pgUW-o9&h*;j|Z z|LLbQRTnENtLy5jD=Vrh&!73_)ZzVKfBw%~SHC)We16vi%!p-BqHGAI32&fvkK2bn z`pmqyKiPM|!KCf_iP8_>nfJ`2LvQcVI$>W*iISf}V$eA0d6O6IJW|upXuAUaFv(rJ zv7zS3&P9{+(!CE#i28`$0%}+m#I60%sq>3I`?|ai6OFr$Hsa-?gSPi@+qUC!E~HF)UJCBIj%{;{n|-k8I55`DM$%h*xx?v=%Vnlbh^)ANO6|egB2--od8yr*572UOLD9iW=rIuty#KR}XAhMu9s&SH#30Jz*X^Tv4xd(Z#7X$J zNeqZd;n<$Dch!4S26poP6f4T_x>h`6@v%l*U~tW;+52iHKs;lnpr=U0=((fio_`cq z$lxKl>-h;gzJ2DX49`Ulq8bwYDm%U=s3zR_$E({hDXR?w0HO@&vkgswGLUn(`b;+} z%HGTbFiffjm;qA~5eetoK+m|I)7r6h4|WCsgR0@O1M!YRWRi0fUljB`n*;fL8fSj|vnq!ZqU{;=#H3t-^l6mr_WZPouNLL`bduHStz#=59uLi#z^=>^%mt#0hau>1kt+Bris|ymFPk*7T z7DGCZO?=$iJugInSRwQ_D8QhZ-y({r#1NaX5%JrF_r^UC61uG{Z)+n;j$t>+1!74* z7P5`Ri1ASIbwm{5M@Yo_k`Vx=5uO=J06P8gQ^cq%Pyr^~2E=Wn+J(sjF$na0?lZdmu(QU<@E<$tgr3lmQBXcxdT>_%R7bKHJD$az63*Mj5k>02mR zfp}$EPGJ2HLQLqiv;rkp862^ENhUD~31}Hx+viUpw^3>42>kK&4gfTmIc@UjvADCX zHYB1r@fZnY0tUbXC5SdE&uqlv2LOx!mNU?P#qnIO^6(s*;yfC-a)=0p{E{2DC}vPv z;y1S41=I>w>I^z8LTr>$g-992hS?o}1u1b#q2&wv5nWWIxrqB-XbF@C=)qcyDZ9zJ zP$h!PhO$xVEI0!L=>Kk`r*uP|12%3M5TL{jNS}TY)oM~;?ZPxL12doTS6`uY)jBv5 z5B*gJ01L)E8CdE_6lz7FuuBJ$K&+ySX3ecbS4!2ca!awPqRmFRyaSl*<|IvsP=D#8UGj1S9rJIZHDl=7y5HPLYI}ly9 z;~?%H-ogNyBA2V(wBua$aCFbC?*+h8v^{T+%~7Zwi4Ol^bZQ`s{AOy6r#`vj2L~Gq zT2V?;Qo~@#`;OEAE;?KCNeU*!0yh70LInfWZ<0rbLNaFd={Za{$xtkujq| z+NcS$k-rLH`Ca1Bxezg`3!)M?bT^pKb!*ceERpEw0vF}(i5WnMpYx>sy2}!|E)a+n zE7}oCeJyXx-ity?T_B~5?&<|TLSo|)r;%%8it!^@J|ccNeRMiH7F>~<;p!_LeawBD z07KueMIrQo5Lo;EP#;|n3&_;N9OGN`u>twvsRS$^%Nys{AV;kj$C~*75&uT%?;%F@ zLKN}6biYOfsCoA;L|ZS|h$VNK)SsOxKLOLX;ui}I_JLN5r!`kl8EFnlU<71O1&Ua;L)Mr4-DYZ8gwLT#-xKakNUGfC`6uh zq^%!p={y$!N_^R~>%6F*M$vg)ec6!#&41TMS3kIDzdexpvB?_y94$ zoK(t@qjrp=*#Erg!}S+pg7w--sUrl+$^{WH{UEaXDx%O25;3|t4Xos`AbqQdKXf#~ z?dkr|>R%9D9pNJWb88;}6S{wn=;#Osad$T`eJFH*e&57Knqp6O^7YV3Z0WuO`r@m8 zgeD6Gh&*saU#ky~k0kARGk`WzP9bs~!4bcIvK;_Rnl96R))zk&bn=7D8E5pxk1uuf zgX~2Y!h{q;hKbYDdnSzmkh7{%U!%&^xd2Ae zBBBV*ned0=(n>_3mZ$VtQ5mX)>Z7QfgmTx0&F_+-^1IgL`a#ZWo%!9Xa*~Dt^qO^6 zU-_;Ji?V$wcX&l(y9t^X_2!aldWrf7aLPe`)2@b@%P4 ztLyG!Yq~$AZH^*JEmb?Xy4tULCkv{ijwT%C$=57DFu;7_fIzMzcRkZ?t`cCiT3N=>;JwB{oi+^|NE}= zf8U+{@4M9deYgJ&xbHfm|NHLs-|m8+?QXnsRWt35$SVaM0hq7tu9RyAJnd_{D?lmq z+7*>LX?I89-fZx<-67I$MTJIOZ;tZvy7t6R2!V`=yEWo^Iw*@DNCSV{UuQ=54Je}; zak>gS`Q7@s1@J1@hwE+2h8YGh-y0qN^B<)qR!fxi|1}Q$Z+DXR-FO|5YvS1#=Jo); zM-;#sbwD5Ot{xiGtojB%Tz_d(RIi7;hmcs(0aDTKFrhCUm&mp7xHOM}MN-i2HW^gp zpbpyImd<;FQ_>qZp|n*&BRV>0cf4jwi`4W(%K%3Iz#UZlAduU8@&HUo4ZYT%an;}N z20u2fZHoF`pkTBZUW(`_Kjj@+rkSeF$4oF+x$5tC31E#gtSGqC{(+uYI=H*bO*q|qDH0WKZL->clyQV1Ejkl zV^mp#RA+*c^P6R(TQEukrMu$A{R+Jb!x0a>*b*oWnC^~w4QQ)UcSj@QB9HElfQgLj zw@snCOX^GVfm*@S-IAwJ-7O{kC;$nf?wVp%>aN*&AAk`=-93anc0#C7caM8yBv69{ zQ+H9vH-1FnC~ltPv1eKV(17Z0qP>=sqtg7ZxMx(kqyt0<39Rm_oMl%KRR-dFyF(Rd`nf2nn>_!a8_JlxhVv?>pa5t@;6^a<@g^G6GX?EArKx&!Mv-mS~CzY>qcEfri|Y}}gX)!iB@r~z>A!hIfZH|)PR zxNp`V02)FW;ue9l7x&hBO8Ua=Y+&u)7c+obq19bH_U$UfFzYUsSifa-Xmu9@iBK>G z&Di52q719xUia(8LuhQCgj#p?#L_w*V%^mf(ty(N>+YWY700l>taNRz2Ho92j0ixN z<>6|lC$ZGyyb^SmC!Fy@sJlHjoL-n^5@qPFpPGOat}BAS&2|y-%M}x|J-X}lth@i_ zx#FkVd35(HCGLnOG9Y$H|wzS>}bm3y-59^1UiGk=Sh--g8?05a2dcVq(wCw~ZPc9Q8P;riI zDHxq)0)Vop{ctz{8M?US(78rWtMWmKy97OU>0nX8;D{&unAn_hXgcmD8tE`*?(X=} z5Hl;*SKxmj6W4u1}E;CbEWHF{mVN|Z>cX)UffM4Gd-t< zt`Dx;d-`H+>f`RIxmdny<-{(Y`i}-9YK%K8;ozio$i8LVMXq0Q7x7!hU36Rf7ESp0 zN~6iR(da3+eSi3?vfaD3 zZ=V0$!vk&=cbP8Yt^=UVu+lPGb?<-Q15>6=U$|;bV%%+OSI;accyfHd>~5_y(j%s! z>bTqdjJu9T+H~!ios*jwcU|Ay%+8(LMW7jR*QuAC$;aJin3iQW|9DbwSbi#wyYI=w z1#QP&_{O_^4)&$SD@i8qMrFqx`E)ck;)qvu9CxMXIRBaO#@%^oK`%|y_p@sl zw=W)OMS$u!?pB)jw|0z^ucdn{Kj?U7(DcGBt0u+PQ9VN`FmK$w^nV<27l*4N7BSfW z_~Xw0e^~}rP&gng6aWCwa{!$IDgXfh0X{JpibNtIp&}xZYT!Ty31e>A0BA+aD?j^} z;h&Efmg%31KayRcTwUJ&oYPh6C%C_V{=56T{Uhm*sP8bp(tq(^*1xg;%l^OIlguaU z_x)e*e(0a+Kg|0w^Z@-~>o@+F>H+*;`8TCc{V(ue1%I&r@_)^K|NR7iCH|BC_w)a_ z1JpbC2U?fnkLrJd4&wgf-rnVxlF9Wq{lFLJU)a0%HC(};_7C^mlzxlq9sRHUXWWm{ zulE1^Jj{M`{+s_t`X7K_W><^kQ^k*J^{%3VcS#> zMrTM4{+Q#p>~=6<1nat<&Wd~v(+6RI8;upL_Ew~j`*>R}ishx@h-!5` zLg+QJkuTaSurYO`#?+!*O*}Z;31~?$Qd@y`$o#?> z=oV~ZbB0tPC-8?{>aw7SE!p+wT>ZgWFy?g}jdSA^dH%Gb z#xu$a^z`_K7ht%sQ(>lyA_4^LZ3sIlT1KmvOPNcYZE67mv>`2_fXd7m;0g?Qnv(BU z!6Qu*3Gjka@xe;<+kJVzO399alTu0bzlqqdM(;^N5S5d=A8?Wa3M2YOj@6Q~H=nLQ z?=P2R#ht5vG5p|kgpdIK{?Htr0U#$~ABWaVS&`%SPIRrqPxH(-yQB=a%yKP!Fz5{XE>Ci8AeuRHPkjIY z2M%kmx;uy8_5NuYhI)l9}3R$HW9k3x1#F52FY+{+!fFY1fiNF;=J>GR5uY;5JPd!gwK`| z@~Ql&ZW{@X4Q;Ml?*Xi&^h43R|ExBn_;=IZ7E(>T@#3jqqv>G1p@AJ4=`{Y|LOATT z@cuU~j{8r;l8S5xDB1b`g6#U34feZgAOSAZk`zkTes&lqm}pbW>XvOjX~9(KLmOx@ zK~}XNZhUJ;P{ny$e*g{1JPjXQTe@}mf4BHc@QyDyzwZ|(8hV@LJGSu`>a$R{cRoA> zDB^tk(d&VT-@dmOId!%%lY&bL8MowbjBtI5fK7CL*@b5A2*W>7p}BrK%Rme01dBSW zqu>!9a_k0PQHF{{egB~jQ6lBT8fInBBBQEMhItxDL!A#RBG1bEl(qh=f@RSy+I>#N z8FQ$Nq$d1Cmjc1z_@d0QMB0Lq(a%1jE2)wFJeaKO5za;0Nlnk&t$F@Y`_wDaz+Loo zim2xNs%rMurjLIT@g;Uf64u>_-3Vr6hQD-EkA&;+p0QiU?Yed*AjP5&TuJ_L{93Xu zyR!pCk*%plTG=U*1#Lu4H=OWj+}k3PJQM+qJ-&=SO0gb4dUrdlhTy*iVWIQ2X8(5G z#;@&tUIOU)(sPBY1#%jmP9K;e2g03AVVy=x?F!(tA@jt4$^LLoVyDNGIkga~Qo?Tl z=6``S0!^D2flNT7V_V2*HRhh;j?NL8YPI|p*& z7somo61`DPlpLQwCm}`4ZJ}=wl;i&gMv5yZMi*dShjv-j#J3MisRtq#8hKm>7vVXQ zfOA9g=~icXt8%USGy>EwnD+8&q+)NXB4 ze-V7@V^i3eanfaIK9`db+k(ZsMGicShA;GBjnu&#PYUe7{-8UEqvXq}Aeds_L&zHW ze}Ort9GZk3fpaRDq4xf%ZN4E?Hn#WdGO*kr0#m-(iA}2>j>J`)igrHfbT5qBLn%+6 z^0pq_UCdt~V$5fccHD7s_peP1oI5(|AJP|X93zsWEjRigAHkmU?w^Che?z#Q?gKAi^9QZ2LW?15OEP?-Y9G5 zJ-^lNSvk0a4Il{BK_bYaM*j_RDj#*v|Mu*E;RE|7?TBSccgdk>B2R+!ttw2tP|EsNySQ?6EfW5BN9AQ7p(EEa(_@{+*h8|-#~ z>_TmnI?3W4+tcUqxPjmz)WYonkkAAqwFyLdbJ+(V{H;6_Ule}K->d58a_oQOJyf`a zLq7BrcDtWTu$cq?I{B%R^RdjvPC@_gm+$khfM#6(U(f19>nWH=x66`kVB2Kx7|Y-+ zmL4fz2EymRy)nJo8FbpkeF9soZt-#!U9zSpYU0blZ)BOgJy>7)6vHJ9(zLeWr-sY#>N zAh{#iypT@CB7S(Ky_~VHfu=j~J6^Z}@SLfwgq$kln!P2O2v;@6hobq6LuvkmX_9j326WAqbR~yj`V7MRVe;o!)*u5= zaPc!b?YX6VA6(0+0B|&u_!_9t87lA*OfD;iA4`|{BekKFP(0Xlfm-Or@|IZ)Y?M)t z?S*u`c?~2ajNLtAc?PDEQpshd=KewtBhFcK-ir_1vk&|a1$8mI1SGG}HtFfoW0 z;GXGaY`#}VUg+Rc%UCDE6nQlzk~g921YVl|uDd?=A{P>4uDD(`74oP-p6A_xpekV8 zZeF;tr=+8uDr4~*1fcK2SNSQOgZUYpS*_2we;|3{e#JgD2TS^VBD=k#ZSs!gy68O! zZ#+eUJit-5nDRWP2u-OE2AFjlTjDm{=&JC-`MOU>E-+hm;SDL(mN3EEM|E9I42cIem7q_C+8$Jfv>Nn!Ztf9dhlQ`4f9DC=?Rk zvp^hqi__9>7*YW?g*}I8f$5ECtu~`!lU0yKqu^FE0Mof>XxJ^Z!+^&Lg-h|$PMp|? z^i|ckP+1JGPU9R)+NC`5mkRjjBOvs1iCK+;?|u|}<%4eUF$3DW*u+$X7dK>=DYHCy)=@&9zMcFm>_#wz9= zIkNf#o%g@tUW|J#v8IFob}g=zeptgwz#7SEEAbG8EOl|oZ-bX25D;$;nF4JsIiRxJy}_{qSh@RdOL1|K17 zgG6O}*=LpfU0hB%GDP|Vc`PVg+cI?P#?>PEG~A?#I9-46W+E<96q~rihaz^t2FJwC zSZw2c>juGHvvMV50cV&$1f*Hj?RDT7#O%+~lQ+zil_T_Su0;6;IgizKgqI$N^fWVkLt0cmT>Le?;sP!uD z5tnUg<1^m5RPk2RqjQMB*=+PZt|h_p30m241@@(gJTH~wf; zw39~$eCzoQ8LeA`yt;$=?4KBAR4iB7wpt2{M8~F^SG#Zu{-ficBxTd4(YQNOR^*e4 zg`CX(J7%L5!f2kmuK^bR|JGokz&Y6xiR;~duFxeI@;F#B1oiK_ke1ENkU!+O;0J1m zpHEw}2P2eibXt|lMC!P-w2fXR2ug6%c6x4C$SO*bxMm+&p<{IK@>+=s_Yc=-TS-lm zMam&qM6pHlmkV#BbpvUwGq}ms$6=^m&ge9PA_Fx=6n%Yaf^CPgt*Fr;{`?vN7Z+K> zd{-oAazA12#SfvT?_2Gs_Foz>#+4@Qf-$=V%g#4|$CxCDCEoc<=hnLOf*5vB-}BIL z+3r0D3=SNm;TSq>T@4$E(pU7M{1a`4JJFvcgLVQyafc~e*z4s724z;O#vT63Lg9*{cJOFC0qfP5I9|i5sLD60r*rqTj;5n7*LZPdu3@k8@u3 zS2>Q&DUe|Ev|oVzrJwaifCM-FJ3nD|_tjhq@uyw%IA}RXLC=iloi3di*=l%fe1^JzyiYuE3f)O-qy#G{GPW#(IuUoPDL3UI7adeXC!)lS{A88@mRFSkp~}(Av=} zy*h2zp!laB0p?z`U9Lntx;yxp%6>u?bxxYbmF&`nvAfC@- zJ((P=HQPb&vKF;2;%}Gh?be}ISt$hL5)!u@^J9qPjVXU^^$x>q$fm;9Y>H~Yk;FA>*Z z<3T5%^{*rSuOlVX7KLDvKn{+t-dz)4_zf4VsJ?td#q?2_bw|xd`m+o8ogAd^^+@OR z7OyRqC&^|KEV~qs`%h3zBLe*6PJB)4LNpgnIq2xlNi6q9lMR&;9=`v(uoRH4g#Z8m00030Hqj#h literal 0 HcmV?d00001 diff --git a/packages/docs/showcase-apps.json b/packages/docs/showcase-apps.json index ff3918a77..7563e9aa8 100644 --- a/packages/docs/showcase-apps.json +++ b/packages/docs/showcase-apps.json @@ -7,7 +7,7 @@ "library": "expo-iap", "ios": "https://apps.apple.com/us/app/sudoku-rabbit-daily-puzzles/id6742900571", "android": "https://play.google.com/store/apps/details?id=com.bustedout.sudokurabbit", - "ratings": 147, + "ratings": 148, "installs": 1000 }, { @@ -47,7 +47,18 @@ "ios": "https://apps.apple.com/us/app/boxa-shadow-boxing-combos/id6503469976", "android": "https://play.google.com/store/apps/details?id=shadowboxing.app", "web": "https://boxa.app", - "ratings": 439, + "ratings": 441, + "installs": 100000 + }, + { + "name": "Record Scanner", + "tagline": "Scan, value, and organize vinyl, CDs, and cassettes", + "logo": "/showcase/record-scanner.webp", + "library": "react-native-iap", + "ios": "https://apps.apple.com/us/app/record-scanner-vinyl-cd/id1507752204", + "android": "https://play.google.com/store/apps/details?id=com.digdig", + "web": "https://recordscanner.com/", + "ratings": 11501, "installs": 100000 } ] From 23a10898b2b49fa264b335dfff6d685c8acb6315 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 08:54:34 +0900 Subject: [PATCH 11/12] fix(godot): reject malformed old product ids --- .../android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt | 2 +- .../src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt index 7f01835b8..10c32ce91 100644 --- a/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt +++ b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIapHelper.kt @@ -133,7 +133,7 @@ internal object GodotIapHelper { ) { val paramsObj = json.optJSONObject("subscriptionProductReplacementParams") ?: throw IllegalArgumentException("subscriptionProductReplacementParams must be an object") - val oldProductId = paramsObj.optString("oldProductId").takeIf { it.isNotEmpty() } + val oldProductId = paramsObj.optStringOrNull("oldProductId")?.takeIf { it.isNotBlank() } ?: throw IllegalArgumentException("subscriptionProductReplacementParams.oldProductId is required") val mode = paramsObj.optString("replacementMode").takeIf { it.isNotEmpty() } val parsedMode = parseSubscriptionReplacementMode(mode) diff --git a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt index 4887b305e..b4e3bd1c1 100644 --- a/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt +++ b/libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapHelperTest.kt @@ -79,6 +79,8 @@ class GodotIapHelperTest { fun `subscription replacement modes require complete concrete params`() { val invalidParams = listOf( """{"type":"subs","skus":["new"],"subscriptionProductReplacementParams":{}}""", + """{"type":"subs","skus":["new"],"subscriptionProductReplacementParams":{"oldProductId":7,"replacementMode":"without-proration"}}""", + """{"type":"subs","skus":["new"],"subscriptionProductReplacementParams":{"oldProductId":" ","replacementMode":"without-proration"}}""", """{"type":"subs","skus":["new"],"subscriptionProductReplacementParams":{"oldProductId":"old"}}""", """{"type":"subs","skus":["new"],"subscriptionProductReplacementParams":{"oldProductId":"old","replacementMode":"future-mode"}}""", """{"type":"subs","skus":["new"],"subscriptionProductReplacementParams":{"oldProductId":"old","replacementMode":"unknown-replacement-mode"}}""", From 7c46a580495da64d0fa61ff0960d61dcf01632dc Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 14 Aug 2026 08:57:41 +0900 Subject: [PATCH 12/12] fix(godot): record Android template version --- libraries/godot-iap/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/godot-iap/Makefile b/libraries/godot-iap/Makefile index c53394ada..958818239 100644 --- a/libraries/godot-iap/Makefile +++ b/libraries/godot-iap/Makefile @@ -271,6 +271,7 @@ prepare-android-export-template: rm -rf "$(EXAMPLE_DIR)/android/build"; \ mkdir -p "$(EXAMPLE_DIR)/android/build"; \ unzip -q "$$template_zip" -d "$(EXAMPLE_DIR)/android/build"; \ + printf '%s\n' "$(GODOT_VERSION).stable" > "$(EXAMPLE_DIR)/android/.build_version"; \ config="$(EXAMPLE_DIR)/android/build/config.gradle"; \ perl -0pi -e "s/androidGradlePlugin:\\s*'[^']+'/androidGradlePlugin: '$(ANDROID_EXPORT_AGP_VERSION)'/" "$$config"; \ grep -q "androidGradlePlugin: '$(ANDROID_EXPORT_AGP_VERSION)'" "$$config" || \