diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt index e5ef9ccf0..064a7ce8d 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt @@ -75,6 +75,14 @@ object ExpoIapHelper { } } + fun parsePurchaseProductQueryType(rawType: String?): ProductQueryType { + val type = parseProductQueryType(rawType) + require(type != ProductQueryType.All) { + "Product type all is only supported for product queries." + } + return type + } + internal fun parseDeepLinkSubscriptionParams(params: Map): DeepLinkSubscriptionParams { return DeepLinkSubscriptionParams( sku = params["skuAndroid"] as? String, @@ -87,43 +95,93 @@ object ExpoIapHelper { // before parsing the native request. val effective: Map = run { - val request = params["request"] as? Map<*, *> - if (request != null) { - val nested = request["google"] as? Map<*, *> - if (nested != null) { - val flat = mutableMapOf() - // Carry over top-level fields such as the purchase type. - for ((k, v) in params) { - if (k != "request") flat[k] = v - } - // Overlay platform-specific fields - for ((k, v) in nested) { - if (k is String) flat[k] = v - } - flat - } else { - params + if (params.containsKey("request")) { + val request = params["request"] + require(request is Map<*, *>) { "request must be an object" } + val nested = request["google"] + require(nested is Map<*, *> && nested.keys.all { it is String }) { + "request.google must be an object with string keys" + } + require(!nested.containsKey("type")) { + "type must be provided only at the purchase envelope" + } + val flat = mutableMapOf() + // Carry over top-level fields such as the purchase type. + for ((k, v) in params) { + if (k != "request") flat[k] = v + } + // Overlay platform-specific fields. + for ((k, v) in nested) { + flat[k as String] = v } + flat } else { params } } + require(effective["type"] == null || effective["type"] is String) { + "type must be a string" + } + effective["skus"]?.let { rawSkus -> + require(rawSkus is List<*> && rawSkus.all { it is String && it.isNotBlank() }) { + "skus must contain only non-empty strings" + } + } + for (key in listOf( + "obfuscatedAccountId", + "obfuscatedProfileId", + "purchaseToken", + "originalExternalTransactionId", + "offerToken", + )) { + require(effective[key] == null || effective[key] is String) { "$key must be a string" } + } + require(effective["isOfferPersonalized"] == null || effective["isOfferPersonalized"] is Boolean) { + "isOfferPersonalized must be a boolean" + } + for (key in listOf("developerBillingOption", "subscriptionProductReplacementParams")) { + effective[key]?.let { value -> + require(value is Map<*, *> && value.keys.all { it is String }) { + "$key must be an object with string keys" + } + } + } + effective["subscriptionOffers"]?.let { rawOffers -> + require(rawOffers is List<*>) { "subscriptionOffers must be a list" } + require(rawOffers.all { rawOffer -> + val offer = rawOffer as? Map<*, *> ?: return@all false + val sku = offer["sku"] as? String + val token = offer["offerToken"] as? String + offer.keys.all { it is String } && !sku.isNullOrBlank() && !token.isNullOrBlank() + }) { "subscriptionOffers must contain valid sku and offerToken strings" } + } + val type = effective["type"] as? String - val skus = (effective["skus"] as? List<*>)?.filterIsInstance() ?: emptyList() + val purchaseType = parsePurchaseProductQueryType(type) + val subscriptionOnlyFields = listOf( + "subscriptionOffers", + "subscriptionProductReplacementParams", + "purchaseToken", + "originalExternalTransactionId", + ) + require( + purchaseType != ProductQueryType.InApp || + subscriptionOnlyFields.none { effective[it] != null }, + ) { "Subscription options require product type subs" } + require(purchaseType != ProductQueryType.Subs || effective["offerToken"] == null) { + "offerToken requires product type in-app" + } + val skus = (effective["skus"] as? List<*>)?.map { it as String } ?: emptyList() val obfuscatedAccountId = effective["obfuscatedAccountId"] as? String val obfuscatedProfileId = effective["obfuscatedProfileId"] as? String val isOfferPersonalized = effective["isOfferPersonalized"] as? Boolean ?: false val explicitSubscriptionOffers = - (effective["subscriptionOffers"] as? List<*>)?.mapNotNull { rawOffer -> - val offerMap = rawOffer as? Map<*, *> ?: return@mapNotNull null - val sku = offerMap["sku"] as? String - val offerToken = offerMap["offerToken"] as? String - if (sku.isNullOrEmpty() || offerToken.isNullOrEmpty()) { - null - } else { - AndroidSubscriptionOfferInput(offerToken = offerToken, sku = sku) - } + (effective["subscriptionOffers"] as? List<*>)?.map { rawOffer -> + val offerMap = rawOffer as Map<*, *> + val sku = offerMap["sku"] as String + val offerToken = offerMap["offerToken"] as String + AndroidSubscriptionOfferInput(offerToken = offerToken, sku = sku) } ?: emptyList() val purchaseToken = effective["purchaseToken"] as? String val originalExternalTransactionId = effective["originalExternalTransactionId"] as? String @@ -131,23 +189,13 @@ object ExpoIapHelper { (effective["developerBillingOption"] as? Map<*, *>)?.let { optionMap -> val json = optionMap.entries - .mapNotNull { (key, value) -> - (key as? String)?.let { it to value } - }.toMap() + .associate { (key, value) -> key as String to value } DeveloperBillingOptionParamsAndroid.fromJson(json) } val subscriptionProductReplacementParams = (effective["subscriptionProductReplacementParams"] as? Map<*, *>)?.let { paramsMap -> - val oldProductId = paramsMap["oldProductId"] as? String - val replacementModeStr = paramsMap["replacementMode"] as? String - if (oldProductId.isNullOrEmpty() || replacementModeStr.isNullOrEmpty()) { - null - } else { - SubscriptionProductReplacementParamsAndroid( - oldProductId = oldProductId, - replacementMode = parseSubscriptionReplacementMode(replacementModeStr), - ) - } + val json = paramsMap.entries.associate { (key, value) -> key as String to value } + SubscriptionProductReplacementParamsAndroid.fromJson(json) } // offerToken for one-time purchase discounts (Android 8.0+) val offerToken = effective["offerToken"] as? String diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt index bc5e30cb4..09d9916bf 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt @@ -374,7 +374,7 @@ class ExpoIapModule : Module() { val parsedParams = ExpoIapHelper.parseRequestPurchaseParams(params) val productType = - when (ExpoIapHelper.parseProductQueryType(parsedParams.type)) { + when (ExpoIapHelper.parsePurchaseProductQueryType(parsedParams.type)) { ProductQueryType.Subs -> ProductQueryType.Subs else -> ProductQueryType.InApp } diff --git a/libraries/expo-iap/android/src/test/java/expo/modules/iap/ExpoIapHelperTest.kt b/libraries/expo-iap/android/src/test/java/expo/modules/iap/ExpoIapHelperTest.kt index 9e24e593e..28013e2c2 100644 --- a/libraries/expo-iap/android/src/test/java/expo/modules/iap/ExpoIapHelperTest.kt +++ b/libraries/expo-iap/android/src/test/java/expo/modules/iap/ExpoIapHelperTest.kt @@ -1,13 +1,23 @@ package expo.modules.iap import dev.hyo.openiap.OpenIapError +import dev.hyo.openiap.ProductQueryType import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Test class ExpoIapHelperTest { + @Test + fun `purchase parser rejects all`() { + assertThrows(IllegalArgumentException::class.java) { + ExpoIapHelper.parsePurchaseProductQueryType("all") + } + assertEquals(ProductQueryType.All, ExpoIapHelper.parseProductQueryType("all")) + } + @Test fun `deep link parser uses canonical keys`() { val parsed = @@ -72,8 +82,8 @@ class ExpoIapHelperTest { } @Test - fun `request parser ignores the removed android request alias`() { - val parsed = + fun `request parser rejects the removed android request alias`() { + assertThrows(IllegalArgumentException::class.java) { ExpoIapHelper.parseRequestPurchaseParams( mapOf( "type" to "in-app", @@ -84,8 +94,7 @@ class ExpoIapHelperTest { ), ), ) - - assertTrue(parsed.skus.isEmpty()) + } } @Test @@ -117,6 +126,76 @@ class ExpoIapHelperTest { assertTrue(parsed.explicitSubscriptionOffers.isEmpty()) } + @Test + fun `purchase parser rejects mixed SKU lists`() { + assertThrows(IllegalArgumentException::class.java) { + ExpoIapHelper.parseRequestPurchaseParams( + mapOf("type" to "in-app", "skus" to listOf("coins", 7)), + ) + } + } + + @Test + fun `purchase parser rejects malformed offers atomically`() { + assertThrows(IllegalArgumentException::class.java) { + ExpoIapHelper.parseRequestPurchaseParams( + mapOf( + "type" to "subs", + "skus" to listOf("premium"), + "subscriptionOffers" to listOf( + mapOf("sku" to "premium", "offerToken" to "known"), + mapOf("sku" to "premium"), + ), + ), + ) + } + } + + @Test + fun `purchase parser rejects a nested type discriminator`() { + assertThrows(IllegalArgumentException::class.java) { + ExpoIapHelper.parseRequestPurchaseParams( + mapOf( + "type" to "in-app", + "request" to mapOf( + "google" to mapOf( + "type" to "subs", + "skus" to listOf("premium"), + ), + ), + ), + ) + } + } + + @Test + fun `purchase parser rejects branch mismatched options`() { + listOf( + mapOf( + "type" to "in-app", + "skus" to listOf("coins"), + "subscriptionOffers" to emptyList(), + ), + mapOf( + "type" to "in-app", + "skus" to listOf("coins"), + "subscriptionProductReplacementParams" to mapOf( + "oldProductId" to "old", + "replacementMode" to "without-proration", + ), + ), + mapOf( + "type" to "subs", + "skus" to listOf("premium"), + "offerToken" to "one-time-token", + ), + ).forEach { request -> + assertThrows(IllegalArgumentException::class.java) { + ExpoIapHelper.parseRequestPurchaseParams(request) + } + } + } + @Test fun `end connection preserves false and still cleans up`() = runBlocking { diff --git a/libraries/expo-iap/ios/ExpoIapHelper.swift b/libraries/expo-iap/ios/ExpoIapHelper.swift index 2b107fb8a..ee02fc24b 100644 --- a/libraries/expo-iap/ios/ExpoIapHelper.swift +++ b/libraries/expo-iap/ios/ExpoIapHelper.swift @@ -97,9 +97,18 @@ enum ExpoIapHelper { } } - static func parseProductQueryType(_ rawValue: String?) throws -> ProductQueryType { - guard let raw = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty - else { + static func parseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType { + if rawValue == nil || rawValue is NSNull { + return .inApp + } + guard let stringValue = rawValue as? String else { + throw PurchaseError.make( + code: .developerError, + message: "Product type must be a string" + ) + } + let raw = stringValue.trimmingCharacters(in: .whitespacesAndNewlines) + if raw.isEmpty { return .inApp } switch raw.lowercased() { @@ -117,9 +126,26 @@ enum ExpoIapHelper { } } + static func parsePurchaseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType { + if let rawValue, !(rawValue is String), !(rawValue is NSNull) { + throw PurchaseError.make( + code: .developerError, + message: "Purchase type must be a string" + ) + } + let type = try parseProductQueryType(rawValue) + guard type != .all else { + throw PurchaseError.make( + code: .developerError, + message: "Product type all is only supported for product queries." + ) + } + return type + } + static func decodeProductRequest(from payload: [String: Any]) throws -> ProductRequest { if let skus = payload["skus"] as? [String], !skus.isEmpty { - let type = try parseProductQueryType(payload["type"] as? String) + let type = try parseProductQueryType(payload["type"]) return try OpenIapSerialization.productRequest(skus: skus, type: type) } @@ -143,6 +169,7 @@ enum ExpoIapHelper { static func decodeRequestPurchaseProps(from payload: [String: Any]) throws -> RequestPurchaseProps { + let parsedType = try parsePurchaseProductQueryType(payload["type"]) if payload.keys.contains("requestPurchase"), payload["requestPurchase"] != nil { @@ -157,10 +184,8 @@ enum ExpoIapHelper { } if payload.keys.contains("request"), let request = payload["request"] { - let parsedType = try parseProductQueryType(payload["type"] as? String) - let purchaseType: ProductQueryType = parsedType == .all ? .inApp : parsedType - var normalized: [String: Any] = ["type": purchaseType.rawValue] - switch purchaseType { + var normalized: [String: Any] = ["type": parsedType.rawValue] + switch parsedType { case .subs: normalized["requestSubscription"] = request case .inApp: diff --git a/libraries/expo-iap/ios/onside/OnsideIapModule.swift b/libraries/expo-iap/ios/onside/OnsideIapModule.swift index 370edbb30..d30dcba80 100644 --- a/libraries/expo-iap/ios/onside/OnsideIapModule.swift +++ b/libraries/expo-iap/ios/onside/OnsideIapModule.swift @@ -105,6 +105,10 @@ public final class ExpoIapOnsideModule: Module { AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any]] in ExpoIapLog.payload("fetchProductsOnside", payload: params) + let request = try ExpoIapHelper.decodeProductRequest(from: params) + guard !request.skus.isEmpty else { + throw OnsideBridgeError.emptySkuList + } try await ensureObserverRegistered() var storefront = await Onside.defaultPaymentQueue().storefront @@ -165,11 +169,6 @@ public final class ExpoIapOnsideModule: Module { } } - let request = try ExpoIapHelper.decodeProductRequest(from: params) - guard !request.skus.isEmpty else { - throw OnsideBridgeError.emptySkuList - } - let response = try await productFetcher.fetch(identifiers: Set(request.skus)) if !response.invalidProductIdentifiers.isEmpty { @@ -199,14 +198,10 @@ public final class ExpoIapOnsideModule: Module { AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in ExpoIapLog.payload("requestPurchaseOnside", payload: payload) - try await ensureObserverRegistered() + let purchaseRequest = try ExpoIapHelper.decodeRequestPurchaseProps(from: payload) + let sku = try resolveSku(from: purchaseRequest) - let sku: String = try await MainActor.run { - guard let s = resolveSku(from: payload) else { - throw OnsideBridgeError.emptySkuList - } - return s - } + try await ensureObserverRegistered() try await ensureProductAvailable(sku: sku) @@ -656,47 +651,23 @@ public final class ExpoIapOnsideModule: Module { } } - private func resolveSku(from payload: [String: Any]) -> String? { - if let sku = payload["sku"] as? String, !sku.isEmpty { - return sku - } - - if let request = payload["request"] as? [String: Any] { - if let apple = resolveAppleRequest(from: request) { - if let sku = apple["sku"] as? String, !sku.isEmpty { - return sku - } - if let skus = apple["skus"] as? [String], let first = skus.first, !first.isEmpty { - return first - } - } - } - - if - let requestPurchase = payload["requestPurchase"] as? [String: Any], - let apple = resolveAppleRequest(from: requestPurchase), - let sku = apple["sku"] as? String, !sku.isEmpty - { - return sku - } - - if - let requestSubscription = payload["requestSubscription"] as? [String: Any], - let apple = resolveAppleRequest(from: requestSubscription), - let sku = apple["sku"] as? String, !sku.isEmpty - { - return sku + private func resolveSku(from request: RequestPurchaseProps) throws -> String { + let sku: String? + switch (request.type, request.request) { + case (.inApp, .purchase(let platforms)): + sku = platforms.apple?.sku + case (.subs, .subscription(let platforms)): + sku = platforms.apple?.sku + default: + throw PurchaseError.make( + code: .developerError, + message: "Purchase type must match the request branch." + ) } - - if let skus = payload["skus"] as? [String], let first = skus.first, !first.isEmpty { - return first + guard let sku, !sku.isEmpty else { + throw OnsideBridgeError.emptySkuList } - - return nil - } - - private func resolveAppleRequest(from request: [String: Any]) -> [String: Any]? { - request["apple"] as? [String: Any] + return sku } } diff --git a/libraries/expo-iap/src/__tests__/canonical-key-presence.test.js b/libraries/expo-iap/src/__tests__/canonical-key-presence.test.js index 797140bc8..931d95b36 100644 --- a/libraries/expo-iap/src/__tests__/canonical-key-presence.test.js +++ b/libraries/expo-iap/src/__tests__/canonical-key-presence.test.js @@ -24,6 +24,7 @@ describe('native canonical-key presence contract', () => { )?.[0]; expect(parser).toBeDefined(); + expect(parser).toContain('guard let stringValue = rawValue as? String'); expect(parser).toContain('default:\n throw PurchaseError.make('); expect(parser).toContain('code: .developerError'); }); @@ -31,7 +32,7 @@ describe('native canonical-key presence contract', () => { it('uses only the canonical Apple key in Onside', () => { const onside = readExpoFile('ios/onside/OnsideIapModule.swift'); - expect(onside).toContain('request["apple"] as? [String: Any]'); + expect(onside).toContain('platforms.apple?.sku'); expect(onside).not.toContain('request["ios"]'); }); @@ -40,7 +41,10 @@ describe('native canonical-key presence contract', () => { 'android/src/main/java/expo/modules/iap/ExpoIapHelper.kt', ); - expect(helper).toContain('request["google"] as? Map<*, *>'); + expect(helper).toContain('val nested = request["google"]'); + expect(helper).toContain( + 'request.google must be an object with string keys', + ); expect(helper).toContain('effective["skus"] as? List<*>'); expect(helper).toContain('effective["subscriptionOffers"] as? List<*>'); expect(helper).not.toContain('effective["skuArr"]'); diff --git a/libraries/expo-iap/src/__tests__/index.kepler.test.ts b/libraries/expo-iap/src/__tests__/index.kepler.test.ts index f87b81bce..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', () => ({ @@ -72,6 +73,19 @@ describe('Amazon Vega public API', () => { expect(requestPurchaseNative).not.toHaveBeenCalled(); }); + it('rejects all without dispatching a purchase', async () => { + await expect( + requestPurchase({ + request: {google: {skus: ['coins']}}, + type: 'all' as any, + }), + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: expect.stringMatching(/only supported for product queries/), + }); + expect(requestPurchaseNative).not.toHaveBeenCalled(); + }); + it('rejects an Apple purchase returned by the Vega bridge', async () => { getAvailablePurchasesNative.mockResolvedValueOnce([ { diff --git a/libraries/expo-iap/src/__tests__/index.test.ts b/libraries/expo-iap/src/__tests__/index.test.ts index f0f697d28..b0574563e 100644 --- a/libraries/expo-iap/src/__tests__/index.test.ts +++ b/libraries/expo-iap/src/__tests__/index.test.ts @@ -569,7 +569,7 @@ describe('Public API (index.ts)', () => { expect(res).toEqual({id: 'x'}); }); - it('throws on unsupported iOS product type', async () => { + it('rejects query-only all on iOS', async () => { (Platform as any).OS = 'ios'; (ExpoIapModule.requestPurchase as jest.Mock) = jest.fn(); await expect( @@ -577,7 +577,7 @@ describe('Public API (index.ts)', () => { request: {apple: {sku: 'skuX'}}, type: 'all', } as any), - ).rejects.toThrow(/Unsupported product type/); + ).rejects.toThrow(/only supported for product queries/); expect(ExpoIapModule.requestPurchase).not.toHaveBeenCalled(); }); @@ -681,6 +681,32 @@ describe('Public API (index.ts)', () => { ); }); + it.each([ + ['in-app', {skus: ['coins'], subscriptionOffers: []}], + [ + 'in-app', + { + skus: ['coins'], + subscriptionProductReplacementParams: { + oldProductId: 'old', + replacementMode: 'without-proration', + }, + }, + ], + ['subs', {skus: ['premium'], offerToken: 'one-time-token'}], + ])( + 'rejects branch-mismatched Android options for %s without native dispatch', + async (type, google) => { + (Platform as any).OS = 'android'; + (ExpoIapModule.requestPurchase as jest.Mock) = jest.fn(); + + await expect( + requestPurchase({request: {google} as any, type: type as any}), + ).rejects.toThrow(/must match the selected product type/); + expect(ExpoIapModule.requestPurchase).not.toHaveBeenCalled(); + }, + ); + it('iOS rejects when sku missing', async () => { (Platform as any).OS = 'ios'; await expect( @@ -712,7 +738,10 @@ describe('Public API (index.ts)', () => { request: {google: {skus: ['x']}} as any, type: 'all' as any, }), - ).rejects.toThrow(/valid request object/); + ).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 544a2d4b5..1619b078e 100644 --- a/libraries/expo-iap/src/__tests__/native-log-redaction.test.js +++ b/libraries/expo-iap/src/__tests__/native-log-redaction.test.js @@ -81,6 +81,43 @@ describe('native log redaction', () => { expect(onsideModule).toContain('code: .serviceError'); }); + it('rejects query-only and malformed purchase types on both iOS paths', () => { + const iosHelper = readRepoFile('ios/ExpoIapHelper.swift'); + const onsideModule = readRepoFile('ios/onside/OnsideIapModule.swift'); + const purchaseBlock = onsideModule.slice( + onsideModule.indexOf('AsyncFunction("requestPurchase")'), + onsideModule.indexOf('AsyncFunction("finishTransaction")'), + ); + const fetchBlock = onsideModule.slice( + onsideModule.indexOf('AsyncFunction("fetchProducts")'), + onsideModule.indexOf('AsyncFunction("requestPurchase")'), + ); + + expect(iosHelper).toContain( + 'static func parsePurchaseProductQueryType(_ rawValue: Any?)', + ); + expect(iosHelper).toContain('!(rawValue is String)'); + expect(iosHelper).toContain('!(rawValue is NSNull)'); + expect(iosHelper).toContain('guard type != .all'); + expect(iosHelper).toContain( + 'parsePurchaseProductQueryType(payload["type"])', + ); + expect(purchaseBlock).toContain( + 'decodeRequestPurchaseProps(from: payload)', + ); + expect(purchaseBlock).not.toContain('resolveSku(from: payload)'); + expect( + purchaseBlock.indexOf('decodeRequestPurchaseProps(from: payload)'), + ).toBeLessThan( + 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()')); + }); + it('keeps Onside purchases aligned with the canonical iOS payload', () => { const onsideModule = readRepoFile('ios/onside/OnsideIapModule.swift'); const iosHelper = readRepoFile('ios/ExpoIapHelper.swift'); @@ -135,9 +172,8 @@ describe('native log redaction', () => { expect(onsideModule).toMatch(/case \.failed:\s+return "unknown"/); expect(onsideModule).not.toContain('return "restored"'); expect(onsideModule).not.toContain('return "failed"'); - expect(iosHelper).toMatch( - /guard let raw = rawValue\?\.trimmingCharacters[\s\S]*?else \{\s+return \.inApp\s+\}/, - ); + expect(iosHelper).toContain('guard let stringValue = rawValue as? String'); + expect(iosHelper).toContain('if raw.isEmpty {\n return .inApp'); }); it('does not log raw IAPKit request bodies in the Apple core package', () => { diff --git a/libraries/expo-iap/src/index.kepler.ts b/libraries/expo-iap/src/index.kepler.ts index 17d6420c7..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'; @@ -148,6 +149,13 @@ export const fetchProducts = async ( export const requestPurchase: MutationField<'requestPurchase'> = async ( args, ) => { + const type = normalizeProductType(args.type); + if (type === 'all') { + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); + } const androidRequest = getAndroidRequest(args.request); if (!androidRequest?.skus?.length) { throw new Error( @@ -158,7 +166,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( return normalizePurchaseArray( await getModule().requestPurchase({ skus: androidRequest.skus, - type: normalizeProductType(args.type), + type, }), ); }; diff --git a/libraries/expo-iap/src/index.ts b/libraries/expo-iap/src/index.ts index e9feee8b5..4a89823f6 100644 --- a/libraries/expo-iap/src/index.ts +++ b/libraries/expo-iap/src/index.ts @@ -929,6 +929,31 @@ function normalizeRequestProps( return request.google; } +function validateAndroidPurchaseBranchOptions( + request: unknown, + type: ProductQueryType, +): void { + if (request == null || typeof request !== 'object') return; + const fields = request as Record; + const subscriptionOnlyFields = [ + 'subscriptionOffers', + 'subscriptionProductReplacementParams', + 'purchaseToken', + 'originalExternalTransactionId', + ]; + if ( + (type === 'in-app' && + subscriptionOnlyFields.some((field) => fields[field] != null)) || + (type === 'subs' && fields.offerToken != null) + ) { + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: + 'Invalid request for Android. Purchase options must match the selected product type.', + }); + } +} + /** * Initiate a purchase or subscription flow. The result is delivered through * `purchaseUpdatedListener` — NOT the return value. @@ -960,6 +985,12 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( ) => { const {request, type} = args; const {canonical, native} = normalizeProductType(type as ProductTypeInput); + if (canonical === 'all') { + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); + } const isInAppPurchase = canonical === 'in-app'; if (Platform.OS === 'ios') { @@ -1015,6 +1046,14 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( } if (isAndroidStoreRuntime()) { + validateAndroidPurchaseBranchOptions( + ( + request as + | RequestPurchasePropsByPlatforms + | RequestSubscriptionPropsByPlatforms + )?.google, + canonical, + ); if (isInAppPurchase) { const normalizedRequest = normalizeRequestProps( request as RequestPurchasePropsByPlatforms, @@ -1024,7 +1063,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( if (!normalizedRequest?.skus?.length) { throw createPurchaseError({ message: - 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' + + 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' + 'Expected format:\n' + ' requestPurchase({\n' + ' request: {\n' + @@ -1081,7 +1120,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( if (!normalizedRequest?.skus?.length) { throw createPurchaseError({ message: - 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' + + 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' + 'Expected format:\n' + ' requestPurchase({\n' + ' request: {\n' + diff --git a/libraries/flutter_inapp_purchase/android/build.gradle b/libraries/flutter_inapp_purchase/android/build.gradle index e88f3d830..717e7fdb8 100644 --- a/libraries/flutter_inapp_purchase/android/build.gradle +++ b/libraries/flutter_inapp_purchase/android/build.gradle @@ -200,6 +200,7 @@ dependencies { implementation "androidx.annotation:annotation:${readRequiredAndroidGradleProperty(projectDir, 'openIapAndroidAnnotationVersion')}" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${readRequiredAndroidGradleProperty(projectDir, 'openIapKotlinxCoroutinesVersion')}" + testImplementation "junit:junit:${readRequiredAndroidGradleProperty(projectDir, 'openIapJunitVersion')}" // Google Play Billing comes transitively from openiap-google so its // version stays centralized in packages/google. 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 ddd581ff4..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 @@ -82,11 +80,6 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } } - private fun parsePurchaseType(raw: String?): ProductQueryType { - val type = parseQueryType(raw) - return if (type == ProductQueryType.Subs) ProductQueryType.Subs else ProductQueryType.InApp - } - private fun serializeOpenIapError(error: OpenIapError): Map = error.toJSON().toMutableMap().apply { when (error) { @@ -513,12 +506,21 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act } "requestPurchase" -> { - val params = call.arguments as? Map<*, *> ?: emptyMap() - val typeStr = params["type"] as? String - val purchaseType = parsePurchaseType(typeStr) + val validated = try { + validateFlutterPurchaseParams(call.arguments) + } catch (e: IllegalArgumentException) { + safe.error( + OpenIapError.DeveloperError.CODE, + OpenIapError.DeveloperError.MESSAGE, + e.message, + ) + return + } + val params = validated.values + val purchaseType = validated.type val skus: List = (params["skus"] as? List<*>) - ?.filterIsInstance() + ?.map { it as String } ?: emptyList() val skusNormalized = skus.filter { it.isNotBlank() } val obfuscatedAccountId = params["obfuscatedAccountId"] as? String @@ -613,10 +615,10 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act return@launch } val offers = - (params["subscriptionOffers"] as? List<*>)?.mapNotNull { entry -> - val map = entry as? Map<*, *> ?: return@mapNotNull null - val sku = map["sku"] as? String ?: return@mapNotNull null - val token = map["offerToken"] as? String ?: return@mapNotNull null + (params["subscriptionOffers"] as? List<*>)?.map { entry -> + val map = entry as Map<*, *> + val sku = map["sku"] as String + val token = map["offerToken"] as String AndroidSubscriptionOfferInput(sku = sku, offerToken = token) } ?: emptyList() @@ -859,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 @@ -872,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..ceda1acd3 --- /dev/null +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterInAppMessageParams.kt @@ -0,0 +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 { + 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/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterPurchaseParams.kt b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterPurchaseParams.kt new file mode 100644 index 000000000..c67bd5f8d --- /dev/null +++ b/libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/ValidatedFlutterPurchaseParams.kt @@ -0,0 +1,80 @@ +package io.github.hyochan.flutter_inapp_purchase + +import dev.hyo.openiap.ProductQueryType +import java.util.Locale + +internal data class ValidatedFlutterPurchaseParams( + val values: Map<*, *>, + val type: ProductQueryType, +) + +internal fun validateFlutterPurchaseParams(raw: Any?): ValidatedFlutterPurchaseParams { + val params = + raw as? Map<*, *> + ?: throw IllegalArgumentException("Purchase parameters must be an object") + val rawType = params["type"] + require(rawType == null || rawType is String) { "type must be a string" } + val type = + when ((rawType as? String)?.trim()?.lowercase(Locale.ROOT)) { + null, "", "in-app" -> ProductQueryType.InApp + "subs" -> ProductQueryType.Subs + "all" -> throw IllegalArgumentException( + "Product type all is only supported for product queries.", + ) + else -> throw IllegalArgumentException( + "Unsupported product type: $rawType. Use in-app or subs.", + ) + } + + params["skus"]?.let { rawSkus -> + require(rawSkus is List<*> && rawSkus.all { it is String && it.isNotBlank() }) { + "skus must contain only non-empty strings" + } + } + for (key in listOf( + "obfuscatedAccountId", + "obfuscatedProfileId", + "purchaseToken", + "originalExternalTransactionId", + "offerToken", + )) { + require(params[key] == null || params[key] is String) { "$key must be a string" } + } + require(params["isOfferPersonalized"] == null || params["isOfferPersonalized"] is Boolean) { + "isOfferPersonalized must be a boolean" + } + for (key in listOf( + "developerBillingOption", + "subscriptionProductReplacementParams", + )) { + params[key]?.let { value -> + require(value is Map<*, *> && value.keys.all { it is String }) { + "$key must be an object with string keys" + } + } + } + params["subscriptionOffers"]?.let { rawOffers -> + require(rawOffers is List<*>) { "subscriptionOffers must be a list" } + require( + rawOffers.all { entry -> + val offer = entry as? Map<*, *> ?: return@all false + val sku = offer["sku"] as? String + val token = offer["offerToken"] as? String + offer.keys.all { it is String } && !sku.isNullOrBlank() && !token.isNullOrBlank() + }, + ) { "subscriptionOffers must contain valid sku and offerToken strings" } + } + val subscriptionOnlyFields = listOf( + "subscriptionOffers", + "subscriptionProductReplacementParams", + "purchaseToken", + "originalExternalTransactionId", + ) + require(type != ProductQueryType.InApp || subscriptionOnlyFields.none { params[it] != null }) { + "Subscription options require product type subs" + } + require(type != ProductQueryType.Subs || params["offerToken"] == null) { + "offerToken requires product type in-app" + } + return ValidatedFlutterPurchaseParams(params, type) +} 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..e882b39d0 --- /dev/null +++ b/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/InAppMessageParamsValidationTest.kt @@ -0,0 +1,44 @@ +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 `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", "unknown-in-app-message-category-id"), + ) + + assertEquals( + listOf( + InAppMessageCategoryAndroid.Transactional, + InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId, + ), + params.categories, + ) + } + + @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/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/PurchaseParamsValidationTest.kt b/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/PurchaseParamsValidationTest.kt new file mode 100644 index 000000000..451116e93 --- /dev/null +++ b/libraries/flutter_inapp_purchase/android/src/test/kotlin/io/github/hyochan/flutter_inapp_purchase/PurchaseParamsValidationTest.kt @@ -0,0 +1,60 @@ +package io.github.hyochan.flutter_inapp_purchase + +import dev.hyo.openiap.ProductQueryType +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class PurchaseParamsValidationTest { + @Test + fun `all is query only`() { + assertThrows(IllegalArgumentException::class.java) { + validateFlutterPurchaseParams(mapOf("type" to "all", "skus" to listOf("coins"))) + } + assertEquals( + ProductQueryType.InApp, + validateFlutterPurchaseParams(mapOf("type" to "in-app", "skus" to listOf("coins"))).type, + ) + } + + @Test + fun `mixed SKU lists are rejected`() { + assertThrows(IllegalArgumentException::class.java) { + validateFlutterPurchaseParams(mapOf("type" to "in-app", "skus" to listOf("coins", 7))) + } + } + + @Test + fun `malformed subscription offers are rejected atomically`() { + val malformed = + listOf( + mapOf("sku" to "premium", "offerToken" to "known"), + mapOf("sku" to "premium"), + ) + assertThrows(IllegalArgumentException::class.java) { + validateFlutterPurchaseParams( + mapOf("type" to "subs", "skus" to listOf("premium"), "subscriptionOffers" to malformed), + ) + } + } + + @Test + fun `branch specific options cannot cross purchase types`() { + listOf( + mapOf( + "type" to "in-app", + "skus" to listOf("coins"), + "subscriptionOffers" to emptyList(), + ), + mapOf( + "type" to "subs", + "skus" to listOf("premium"), + "offerToken" to "one-time-token", + ), + ).forEach { request -> + assertThrows(IllegalArgumentException::class.java) { + validateFlutterPurchaseParams(request) + } + } + } +} diff --git a/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift b/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift index 89425b88b..43a5a57df 100644 --- a/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift +++ b/libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift @@ -96,10 +96,18 @@ enum FlutterIapHelper { // MARK: - Parsing helpers - static func parseProductQueryType(_ rawValue: String?) throws -> ProductQueryType { - guard let raw = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { + static func parseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType { + if rawValue == nil || rawValue is NSNull { return .all } + guard let stringValue = rawValue as? String else { + throw PurchaseError.make( + code: .developerError, + message: "Product type must be a string" + ) + } + let raw = stringValue.trimmingCharacters(in: .whitespacesAndNewlines) + if raw.isEmpty { return .all } switch raw.lowercased() { case ProductQueryType.inApp.rawValue: return .inApp @@ -115,9 +123,31 @@ enum FlutterIapHelper { } } + static func parsePurchaseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType { + if let rawValue, !(rawValue is String), !(rawValue is NSNull) { + throw PurchaseError.make( + code: .developerError, + message: "Purchase type must be a string" + ) + } + guard !(rawValue is NSNull), + let raw = (rawValue as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), + !raw.isEmpty else { + return .inApp + } + let type = try parseProductQueryType(raw) + guard type != .all else { + throw PurchaseError.make( + code: .developerError, + message: "Product type all is only supported for product queries." + ) + } + return type + } + static func decodeProductRequest(from payload: [String: Any]) throws -> ProductRequest { if let skus = payload["skus"] as? [String], !skus.isEmpty { - let type = try parseProductQueryType(payload["type"] as? String) + let type = try parseProductQueryType(payload["type"]) return try OpenIapSerialization.productRequest(skus: skus, type: type) } @@ -142,8 +172,7 @@ enum FlutterIapHelper { } if let sku = payload["sku"] as? String, !sku.isEmpty { - let parsedType = try parseProductQueryType(payload["type"] as? String) - let purchaseType: ProductQueryType = parsedType == .subs ? .subs : .inApp + let purchaseType = try parsePurchaseProductQueryType(payload["type"]) let normalized: [String: Any] = [ "type": purchaseType.rawValue, purchaseType == .subs ? "requestSubscription" : "requestPurchase": [ diff --git a/libraries/flutter_inapp_purchase/lib/builders.dart b/libraries/flutter_inapp_purchase/lib/builders.dart index b6a9a9b1c..7c0ac92f9 100644 --- a/libraries/flutter_inapp_purchase/lib/builders.dart +++ b/libraries/flutter_inapp_purchase/lib/builders.dart @@ -160,13 +160,18 @@ class RequestPurchaseBuilder { return; } if (value is String) { - final normalized = value.toLowerCase(); - if (normalized.contains('sub')) { - _type = ProductQueryType.Subs; - } else { - _type = ProductQueryType.InApp; + switch (value.trim().toLowerCase()) { + case 'in-app': + _type = ProductQueryType.InApp; + return; + case 'subs': + _type = ProductQueryType.Subs; + return; + default: + throw ArgumentError( + 'Unsupported purchase type: $value. Use in-app or subs.', + ); } - return; } throw ArgumentError('Unsupported type assignment: $value'); } diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 13e69dde8..80592484e 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/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,7 +401,19 @@ enum ErrorCode { case 'duplicate-purchase': return ErrorCode.DuplicatePurchase; } - throw ArgumentError('Unknown ErrorCode value: $value'); + 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,7 +752,19 @@ enum IapkitPurchaseState { case 'inauthentic': return IapkitPurchaseState.Inauthentic; } - throw ArgumentError('Unknown IapkitPurchaseState value: $value'); + 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,7 +827,19 @@ enum IapStore { case 'amazon': return IapStore.Amazon; } - throw ArgumentError('Unknown IapStore value: $value'); + 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,7 +865,19 @@ enum InAppMessageCategoryAndroid { case 'transactional': return InAppMessageCategoryAndroid.Transactional; } - throw ArgumentError('Unknown InAppMessageCategoryAndroid value: $value'); + 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,7 +948,19 @@ enum PaymentMode { case 'unknown': return PaymentMode.Unknown; } - throw ArgumentError('Unknown PaymentMode value: $value'); + 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,7 +1070,19 @@ enum ProductStatusAndroid { case 'unknown': return ProductStatusAndroid.Unknown; } - throw ArgumentError('Unknown ProductStatusAndroid value: $value'); + 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,7 +1186,19 @@ enum PurchaseState { case 'unknown': return PurchaseState.Unknown; } - throw ArgumentError('Unknown PurchaseState value: $value'); + 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,7 +1296,19 @@ enum SubscriptionBillingPlanTypeIOS { case 'up-front': return SubscriptionBillingPlanTypeIOS.UpFront; } - throw ArgumentError('Unknown SubscriptionBillingPlanTypeIOS value: $value'); + 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,7 +1419,19 @@ enum SubscriptionPeriodUnit { case 'unknown': return SubscriptionPeriodUnit.Unknown; } - throw ArgumentError('Unknown SubscriptionPeriodUnit value: $value'); + 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,7 +1477,19 @@ enum SubscriptionReplacementModeAndroid { case 'keep-existing': return SubscriptionReplacementModeAndroid.KeepExisting; } - throw ArgumentError('Unknown SubscriptionReplacementModeAndroid value: $value'); + 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; @@ -1264,7 +1660,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 +2488,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 +3780,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 +4006,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', @@ -4053,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, ); } @@ -4119,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?, ); } @@ -4195,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?, ); } @@ -4223,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(), ); } @@ -4261,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, ); } @@ -4302,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, ); } @@ -4333,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, ); } @@ -4745,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?, @@ -4957,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), ); } @@ -5124,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/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift index 89425b88b..43a5a57df 100644 --- a/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift +++ b/libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterIapHelper.swift @@ -96,10 +96,18 @@ enum FlutterIapHelper { // MARK: - Parsing helpers - static func parseProductQueryType(_ rawValue: String?) throws -> ProductQueryType { - guard let raw = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { + static func parseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType { + if rawValue == nil || rawValue is NSNull { return .all } + guard let stringValue = rawValue as? String else { + throw PurchaseError.make( + code: .developerError, + message: "Product type must be a string" + ) + } + let raw = stringValue.trimmingCharacters(in: .whitespacesAndNewlines) + if raw.isEmpty { return .all } switch raw.lowercased() { case ProductQueryType.inApp.rawValue: return .inApp @@ -115,9 +123,31 @@ enum FlutterIapHelper { } } + static func parsePurchaseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType { + if let rawValue, !(rawValue is String), !(rawValue is NSNull) { + throw PurchaseError.make( + code: .developerError, + message: "Purchase type must be a string" + ) + } + guard !(rawValue is NSNull), + let raw = (rawValue as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), + !raw.isEmpty else { + return .inApp + } + let type = try parseProductQueryType(raw) + guard type != .all else { + throw PurchaseError.make( + code: .developerError, + message: "Product type all is only supported for product queries." + ) + } + return type + } + static func decodeProductRequest(from payload: [String: Any]) throws -> ProductRequest { if let skus = payload["skus"] as? [String], !skus.isEmpty { - let type = try parseProductQueryType(payload["type"] as? String) + let type = try parseProductQueryType(payload["type"]) return try OpenIapSerialization.productRequest(skus: skus, type: type) } @@ -142,8 +172,7 @@ enum FlutterIapHelper { } if let sku = payload["sku"] as? String, !sku.isEmpty { - let parsedType = try parseProductQueryType(payload["type"] as? String) - let purchaseType: ProductQueryType = parsedType == .subs ? .subs : .inApp + let purchaseType = try parsePurchaseProductQueryType(payload["type"]) let normalized: [String: Any] = [ "type": purchaseType.rawValue, purchaseType == .subs ? "requestSubscription" : "requestPurchase": [ diff --git a/libraries/flutter_inapp_purchase/test/builders_unit_test.dart b/libraries/flutter_inapp_purchase/test/builders_unit_test.dart index aafb99eed..ebf9d382c 100644 --- a/libraries/flutter_inapp_purchase/test/builders_unit_test.dart +++ b/libraries/flutter_inapp_purchase/test/builders_unit_test.dart @@ -180,13 +180,21 @@ void main() { test('type setter accepts String', () { final builder = RequestPurchaseBuilder()..ios.sku = 'product'; - builder.type = 'subscription'; + builder.type = 'subs'; expect(builder.type, ProductQueryType.Subs); - builder.type = 'inapp'; + builder.type = 'in-app'; expect(builder.type, ProductQueryType.InApp); }); + test('type setter rejects query-only and unknown strings', () { + final builder = RequestPurchaseBuilder(); + + expect(() => builder.type = 'all', throwsArgumentError); + expect(() => builder.type = 'subscription', throwsArgumentError); + expect(() => builder.type = 'inapp', throwsArgumentError); + }); + test('type setter throws for ProductQueryType.All', () { final builder = RequestPurchaseBuilder(); diff --git a/libraries/flutter_inapp_purchase/test/generated_enum_input_test.dart b/libraries/flutter_inapp_purchase/test/generated_enum_input_test.dart new file mode 100644 index 000000000..fae511d7d --- /dev/null +++ b/libraries/flutter_inapp_purchase/test/generated_enum_input_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_inapp_purchase/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('in-app message input rejects unknown and malformed categories', () { + final known = InAppMessageParamsAndroid.fromJson({ + 'categories': ['unknown-in-app-message-category-id'], + }); + expect( + known.categories, + [InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId], + ); + + for (final input in [ + { + 'categories': ['future-category'] + }, + { + 'categories': [999] + }, + {'categories': 'transactional'}, + ]) { + expect( + () => InAppMessageParamsAndroid.fromJson(input), + throwsA(anyOf(isA(), isA())), + ); + } + }); +} diff --git a/libraries/flutter_inapp_purchase/test/native_wire_contract_test.dart b/libraries/flutter_inapp_purchase/test/native_wire_contract_test.dart index 3e7a2b425..3de5809b1 100644 --- a/libraries/flutter_inapp_purchase/test/native_wire_contract_test.dart +++ b/libraries/flutter_inapp_purchase/test/native_wire_contract_test.dart @@ -55,6 +55,37 @@ void main() { expect( productTypeHelper, contains('case ProductQueryType.inApp.rawValue:')); expect(productTypeHelper, isNot(contains('case "inapp":'))); + expect( + productTypeHelper, + contains('guard let stringValue = rawValue as? String'), + ); + expect( + helperSource, + contains('parseProductQueryType(payload["type"])'), + ); + expect( + helperSource, + isNot(contains('parseProductQueryType(payload["type"] as? String)')), + ); + + final purchaseTypeStart = helperSource.indexOf( + 'static func parsePurchaseProductQueryType', + ); + final purchaseTypeEnd = helperSource.indexOf( + 'static func decodeProductRequest', + purchaseTypeStart, + ); + final purchaseTypeHelper = helperSource.substring( + purchaseTypeStart, + purchaseTypeEnd, + ); + expect(purchaseTypeHelper, contains('return .inApp')); + expect(purchaseTypeHelper, contains('guard type != .all')); + expect(purchaseTypeHelper, contains('!(rawValue is NSNull)')); + expect( + helperSource, + contains('parsePurchaseProductQueryType(payload["type"])'), + ); } }); } diff --git a/libraries/godot-iap/Example/tests/generated_nullable_enum_list_types.gd b/libraries/godot-iap/Example/tests/generated_nullable_enum_list_types.gd new file mode 100644 index 000000000..f0d78d100 --- /dev/null +++ b/libraries/godot-iap/Example/tests/generated_nullable_enum_list_types.gd @@ -0,0 +1,147 @@ +# ============================================================================ +# AUTO-GENERATED TYPES — DO NOT EDIT DIRECTLY +# Refresh this file with the generated-types workflow documented for your checkout. +# ============================================================================ +# Generated from OpenIAP GraphQL schema (https://openiap.dev) +# Usage: const Types = preload("types.gd") +# var store: Types.IapStore = Types.IapStore.APPLE +# ============================================================================ + +# ============================================================================ +# Enums +# ============================================================================ + +enum TestStatus { + UNKNOWN = 0, + ACTIVE = 1, +} + +enum StrictStatus { + ACTIVE = 0, +} + +# ============================================================================ +# Types +# ============================================================================ + +class EnumListHolder: + ## Status values from the schema. Preserves every documentation line. @see https://openiap.dev/docs/types + var statuses: Array[TestStatus] = [] + var strict_statuses: Array[StrictStatus] = [] + var nullable_strict_statuses: Array[Variant] = [] + var nullable_labels: Array[Variant] = [] + + static func from_dict(data: Dictionary, report_errors: bool = true) -> EnumListHolder: + var obj = EnumListHolder.new() + if data.has("statuses") and data["statuses"] != null: + if data["statuses"] is Array: + var arr: Array[TestStatus] = [] + for item in data["statuses"]: + if item is String and TEST_STATUS_FROM_STRING.has(item): + arr.append(TEST_STATUS_FROM_STRING[item]) + elif item is int and TEST_STATUS_VALUES.has(item): + arr.append(item) + else: + arr.append(TestStatus.UNKNOWN) + obj.statuses = arr + if data.has("strictStatuses") and data["strictStatuses"] != null: + if data["strictStatuses"] is Array: + var arr: Array[StrictStatus] = [] + for item in data["strictStatuses"]: + if item is String and STRICT_STATUS_FROM_STRING.has(item): + arr.append(STRICT_STATUS_FROM_STRING[item]) + elif item is int and STRICT_STATUS_VALUES.has(item): + arr.append(item) + else: + if report_errors: + push_error("Invalid StrictStatus list value for strictStatuses") + return null + obj.strict_statuses = arr + if data.has("nullableStrictStatuses") and data["nullableStrictStatuses"] != null: + if data["nullableStrictStatuses"] is Array: + var arr: Array[Variant] = [] + for item in data["nullableStrictStatuses"]: + if item == null: + arr.append(null) + elif item is String and STRICT_STATUS_FROM_STRING.has(item): + arr.append(STRICT_STATUS_FROM_STRING[item]) + elif item is int and STRICT_STATUS_VALUES.has(item): + arr.append(item) + else: + arr.append(null) + obj.nullable_strict_statuses = arr + if data.has("nullableLabels") and data["nullableLabels"] != null: + if data["nullableLabels"] is Array: + var arr: Array[Variant] = [] + for item in data["nullableLabels"]: + if item == null: + arr.append(null) + elif item is String: + arr.append(str(item)) + obj.nullable_labels = arr + return obj + + func to_dict() -> Dictionary: + var dict = {} + if statuses != null: + var arr = [] + for item in statuses: + if TEST_STATUS_VALUES.has(item): + arr.append(TEST_STATUS_VALUES[item]) + else: + arr.append(item) + dict["statuses"] = arr + else: + dict["statuses"] = null + if strict_statuses != null: + var arr = [] + for item in strict_statuses: + if STRICT_STATUS_VALUES.has(item): + arr.append(STRICT_STATUS_VALUES[item]) + else: + arr.append(item) + dict["strictStatuses"] = arr + else: + dict["strictStatuses"] = null + if nullable_strict_statuses != null: + var arr = [] + for item in nullable_strict_statuses: + if STRICT_STATUS_VALUES.has(item): + arr.append(STRICT_STATUS_VALUES[item]) + else: + arr.append(item) + dict["nullableStrictStatuses"] = arr + else: + dict["nullableStrictStatuses"] = null + dict["nullableLabels"] = nullable_labels + return dict + +# ============================================================================ +# Input Types +# ============================================================================ + +# ============================================================================ +# Enum String Helpers +# ============================================================================ + +const TEST_STATUS_VALUES = { + TestStatus.UNKNOWN: "unknown", + TestStatus.ACTIVE: "active" +} + +const STRICT_STATUS_VALUES = { + StrictStatus.ACTIVE: "active" +} + +# ============================================================================ +# Enum Reverse Lookup (string -> enum for deserialization) +# ============================================================================ + +const TEST_STATUS_FROM_STRING = { + "unknown": TestStatus.UNKNOWN, + "active": TestStatus.ACTIVE +} + +const STRICT_STATUS_FROM_STRING = { + "active": StrictStatus.ACTIVE +} diff --git a/libraries/godot-iap/Example/tests/test_envelope_parsing.gd b/libraries/godot-iap/Example/tests/test_envelope_parsing.gd index ee9f8d3ef..bdc77b4ff 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,60 @@ 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"), true, "Null replacement params should behave as absent") + var null_replacement_payload = JSON.parse_string(fake.last_args[0]) + _assert_false( + null_replacement_payload.has("subscriptionProductReplacementParams"), + "Null replacement params should 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"), true, "Null developer billing options should behave as absent") + var null_billing_payload = JSON.parse_string(fake.last_args[0]) + _assert_false( + null_billing_payload.has("developerBillingOption"), + "Null developer billing options should not reach the native plugin" + ) + + fake.last_method = "" + 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 +1129,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_godot_iap.gd b/libraries/godot-iap/Example/tests/test_godot_iap.gd index 90835b752..9ba3f5ec2 100644 --- a/libraries/godot-iap/Example/tests/test_godot_iap.gd +++ b/libraries/godot-iap/Example/tests/test_godot_iap.gd @@ -135,6 +135,7 @@ func _run_all_tests() -> void: # Purchase tests await test_billing_choice_android_payloads() + test_android_purchase_lists_fail_closed() await test_ios_async_result_cache() await test_get_available_purchases_mock() await test_android_purchase_options_bridge() @@ -265,6 +266,71 @@ func test_billing_choice_android_payloads() -> void: GodotIapPlugin._is_connected = false +func test_android_purchase_lists_fail_closed() -> void: + var fake = FakeAndroidPlugin.new() + GodotIapPlugin._native_plugin = fake + GodotIapPlugin._platform = "Android" + + var invalid_google_payloads = [ + {"skus": []}, + {"skus": ["monthly", 7]}, + {"skus": ["coins"], "offerToken": 7}, + {"skus": ["monthly"], "offerToken": "one-time-token"}, + {"skus": ["monthly"], "subscriptionOffers": [{"offerToken": "token"}]}, + {"skus": ["monthly"], "subscriptionOffers": [{"sku": "monthly"}]}, + {"skus": ["monthly"], "subscriptionOffers": [7]}, + {"skus": ["monthly"], "subscriptionOffers": [{"sku": "yearly", "offerToken": "token"}]}, + ] + for google_payload in invalid_google_payloads: + fake.last_purchase = {} + var result = GodotIapPlugin._request_purchase_raw({ + "type": "subs", + "requestSubscription": {"google": google_payload}, + }) + _assert_false(result.get("success", true), "Malformed purchase lists should fail") + _assert_true(fake.last_purchase.is_empty(), "Malformed purchase lists must not reach native billing") + + var invalid_in_app_payloads = [ + {"skus": ["coins"], "subscriptionOffers": []}, + {"skus": ["coins"], "purchaseToken": "subscription-token"}, + {"skus": ["coins"], "originalExternalTransactionId": "external-id"}, + ] + for google_payload in invalid_in_app_payloads: + fake.last_purchase = {} + var result = GodotIapPlugin._request_purchase_raw({ + "type": "in-app", + "requestPurchase": {"google": google_payload}, + }) + _assert_false(result.get("success", true), "Subscription-only fields should fail for in-app purchases") + _assert_true(fake.last_purchase.is_empty(), "Branch-mismatched fields must not reach native billing") + + fake.last_purchase = {} + var nullable_offer = GodotIapPlugin._request_purchase_raw({ + "type": "in-app", + "requestPurchase": {"google": {"skus": ["coins"], "offerToken": null}}, + }) + _assert_true(nullable_offer.get("success", false), "Null one-time offerToken should behave as absent") + _assert_false(fake.last_purchase.has("offerToken"), "Null one-time offerToken should not reach billing") + + fake.last_purchase = {} + var nullable_subscription_options = GodotIapPlugin._request_purchase_raw({ + "type": "subs", + "requestSubscription": {"google": { + "skus": ["monthly"], + "developerBillingOption": null, + "subscriptionOffers": null, + "subscriptionProductReplacementParams": null, + }}, + }) + _assert_true(nullable_subscription_options.get("success", false), "Nullable options should behave as absent") + _assert_false(fake.last_purchase.has("subscriptionOffers"), "Null offers should be omitted") + _assert_false(fake.last_purchase.has("developerBillingOption"), "Null billing options should be omitted") + _assert_false(fake.last_purchase.has("subscriptionProductReplacementParams"), "Null replacement params should be omitted") + + GodotIapPlugin._native_plugin = null + GodotIapPlugin._platform = "" + + func test_end_connection_mock() -> void: var result = await GodotIapPlugin.end_connection() _assert_true(result, "end_connection should return true in mock mode") @@ -347,6 +413,7 @@ func test_product_variant_mapping() -> void: "displayPrice": "$9.99", "currency": "USD", "type": "subs", + "typeIOS": "auto-renewable-subscription", "subscriptionOffers": [{ "id": "intro", "displayPrice": "Free", diff --git a/libraries/godot-iap/Example/tests/test_types_only.gd b/libraries/godot-iap/Example/tests/test_types_only.gd index 86893f9bc..445a2029c 100644 --- a/libraries/godot-iap/Example/tests/test_types_only.gd +++ b/libraries/godot-iap/Example/tests/test_types_only.gd @@ -3,6 +3,7 @@ extends SceneTree ## Run with: godot --headless --script tests/test_types_only.gd const Types = preload("res://addons/godot-iap/types.gd") +const NullableEnumListTypes = preload("res://tests/generated_nullable_enum_list_types.gd") var _total_passed := 0 var _total_failed := 0 @@ -50,6 +51,7 @@ func _run_all_tests() -> void: # Enum tests _test_enums() + _test_enum_list_decoding() # New types tests (v1.3.12) _test_discount_offer() @@ -98,19 +100,19 @@ func _test_product_request() -> void: _assert_equal(dict["skus"][0], "product_1", "to_dict should preserve sku") _assert_equal(dict["type"], "in-app", "to_dict should convert type to string") - # JSON.parse_string returns an untyped Array; from_dict must rebuild Array[String]. + # Request-authored lists reject malformed entries instead of silently dropping them. var from_dict_data = { "skus": ["sku_from_dict", null, {"invalid": true}], "type": "subs" } var parsed = Types.ProductRequest.from_dict(from_dict_data) - _assert_equal(parsed.skus[0], "sku_from_dict", "from_dict should parse skus") - _assert_equal(parsed.skus.size(), 1, "from_dict should skip malformed scalar list items") + _assert_equal(parsed, null, "ProductRequest should reject malformed SKU entries") 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", @@ -258,6 +295,56 @@ func _test_request_purchase_props() -> void: }) _assert_equal(parsed.type, Types.ProductQueryType.SUBS, "Subscription branch should infer SUBS type") + var invalid_billing_option = { + "skus": ["product"], + "developerBillingOption": {"billingProgram": "future-program"} + } + _assert_equal( + Types.RequestPurchaseProps.from_dict({ + "requestPurchase": {"google": invalid_billing_option}, + "type": "in-app" + }), + null, + "Purchase branch should propagate nested input decode failures" + ) + _assert_equal( + Types.RequestPurchaseProps.from_dict({ + "requestSubscription": {"google": invalid_billing_option}, + "type": "subs" + }), + null, + "Subscription branch should propagate nested input decode failures" + ) + _assert_equal( + Types.RequestSubscriptionAndroidProps.from_dict({"skus": ["monthly", 7]}), + null, + "Request SKU lists should reject malformed entries" + ) + _assert_equal( + Types.RequestSubscriptionAndroidProps.from_dict({ + "skus": ["monthly"], + "obfuscatedAccountId": 7, + }), + null, + "Optional request scalars should reject malformed values" + ) + _assert_equal( + Types.RequestSubscriptionAndroidProps.from_dict({ + "skus": ["monthly"], + "subscriptionOffers": [{"offerToken": "token"}], + }), + null, + "Subscription offers should require a SKU" + ) + _assert_equal( + Types.RequestSubscriptionAndroidProps.from_dict({ + "skus": ["monthly"], + "subscriptionOffers": [{"sku": "monthly"}], + }), + null, + "Subscription offers should require an offer token" + ) + # ============================================ # IAPKit Product Client Payload Tests @@ -384,6 +471,38 @@ 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 params = Types.InAppMessageParamsAndroid.from_dict( + {"categories": ["unknown-in-app-message-category-id"]} + ) + _assert_equal(params.categories.size(), 1, "Known neutral enum input values should be retained") + _assert_equal( + params.categories[0], + Types.InAppMessageCategoryAndroid.UNKNOWN_IN_APP_MESSAGE_CATEGORY_ID, + "Known neutral enum input values should decode" + ) + _assert_equal( + Types.InAppMessageParamsAndroid.from_dict({"categories": ["future-category"]}), + null, + "Unknown enum input strings should fail decoding" + ) + _assert_equal( + Types.InAppMessageParamsAndroid.from_dict({"categories": [999]}), + null, + "Out-of-range enum input integers should fail decoding" + ) + var nullable_response = NullableEnumListTypes.EnumListHolder.from_dict({ + "statuses": ["active"], + "strictStatuses": ["active"], + "nullableStrictStatuses": ["active", "future-status", null], + "nullableLabels": [], + }) + _assert_equal(nullable_response.nullable_strict_statuses[0], 0, "Known nullable response enum elements should decode") + _assert_equal(nullable_response.nullable_strict_statuses[1], null, "Future nullable response enum elements should become null") + _assert_equal(nullable_response.nullable_strict_statuses[2], null, "Explicit nullable response enum elements should stay null") + + # ============================================ # DiscountOffer Tests (v1.3.12) # ============================================ @@ -941,6 +1060,22 @@ func _test_purchase_input_round_trip() -> void: _assert_equal(parsed.purchase_state, Types.PurchaseState.PENDING, "purchaseState should parse back to the enum") _assert_equal(parsed.purchase_token, "token-3", "purchaseToken should survive the wire round trip") + var future = Types.PurchaseInput.from_dict({ + "id": "txn-future", + "productId": "sku.future", + "purchaseState": "future-purchase-state", + "quantity": 1, + "store": "future-store", + "transactionDate": 1720000000000.0, + }) + _assert_equal(future.store, Types.IapStore.UNKNOWN, "PurchaseInput should tolerate future stores") + _assert_equal( + future.purchase_state, + Types.PurchaseState.UNKNOWN, + "PurchaseInput should tolerate future purchase states" + ) + _assert_equal(future.to_dict()["store"], "unknown", "Future stores should remain serializable") + # ============================================ # Test Utilities 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" || \ diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index 73f25afd6..5c780efb4 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -526,6 +526,45 @@ func request_purchase(props) -> Variant: return null +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 + return true + + +func _is_valid_subscription_offer_array(value, requested_skus: Array) -> bool: + if not value is Array: + return false + if value.is_empty(): + return true + var requested := {} + for sku in requested_skus: + requested[sku] = true + var offered := {} + for item in value: + if typeof(item) == TYPE_OBJECT and item.has_method("to_dict"): + item = item.to_dict() + if not item is Dictionary: + return false + if not item.get("sku") is String or item.get("sku").is_empty(): + return false + if not item.get("offerToken") is String or item.get("offerToken").is_empty(): + return false + var sku = item["sku"] + if not requested.has(sku): + return false + offered[sku] = true + for sku in requested: + if not offered.has(sku): + return false + return true + + func _purchase_failure(code: String, message: String, details: Dictionary = {}) -> Dictionary: var error_payload := { "code": code, @@ -586,30 +625,90 @@ func _request_purchase_raw(args: Dictionary) -> Dictionary: "developer-error", "Invalid request: google payload must be a Dictionary" ) + if not google_props.has("skus") or not _is_nonempty_string_array(google_props["skus"]): + return _purchase_failure( + "developer-error", + "Invalid request: skus must contain only non-empty strings" + ) var offer_token = google_props.get("offerToken", "") + if offer_token == null: + offer_token = "" + elif not offer_token is String: + return _purchase_failure( + "developer-error", + "Invalid request: offerToken must be a string" + ) + if google_props.has("offerToken") and google_props["offerToken"] != null and purchase_type != "in-app": + return _purchase_failure( + "developer-error", + "Invalid request: offerToken requires type `in-app`" + ) + var has_subscription_offers = ( + google_props.has("subscriptionOffers") and google_props["subscriptionOffers"] != null + ) var subscription_offers = google_props.get("subscriptionOffers", []) + if subscription_offers == null: + subscription_offers = [] + if not _is_valid_subscription_offer_array(subscription_offers, google_props["skus"]): + return _purchase_failure( + "developer-error", + "Invalid request: subscriptionOffers must contain sku and offerToken strings" + ) + if has_subscription_offers and purchase_type != "subs": + return _purchase_failure( + "developer-error", + "Invalid request: subscriptionOffers requires type `subs`" + ) + var has_replacement_params = ( + google_props.has("subscriptionProductReplacementParams") + and google_props["subscriptionProductReplacementParams"] != null + ) var replacement_params = google_props.get("subscriptionProductReplacementParams", 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") and google_props["developerBillingOption"] != null + ) var developer_billing_option = google_props.get("developerBillingOption", null) if typeof(developer_billing_option) == TYPE_OBJECT and developer_billing_option.has_method("to_dict"): developer_billing_option = developer_billing_option.to_dict() + for field in ["purchaseToken", "originalExternalTransactionId"]: + var value = google_props.get(field, null) + if value != null and not value is String: + return _purchase_failure( + "developer-error", + "Invalid request: %s must be a string" % field + ) + if google_props.has(field) and value != null and purchase_type != "subs": + return _purchase_failure( + "developer-error", + "Invalid request: %s requires type `subs`" % field + ) var params = { "type": purchase_type, "skus": google_props.get("skus", []), "obfuscatedAccountId": google_props.get("obfuscatedAccountId", ""), "obfuscatedProfileId": google_props.get("obfuscatedProfileId", ""), "isOfferPersonalized": google_props.get("isOfferPersonalized", false), - "subscriptionOffers": subscription_offers, - "purchaseToken": google_props.get("purchaseToken", ""), - "originalExternalTransactionId": google_props.get("originalExternalTransactionId", ""), - "subscriptionProductReplacementParams": replacement_params, - "developerBillingOption": developer_billing_option, } - if purchase_type == "in-app" and not str(offer_token).is_empty(): - params["offerToken"] = str(offer_token) + if has_subscription_offers: + params["subscriptionOffers"] = subscription_offers + if has_replacement_params: + params["subscriptionProductReplacementParams"] = replacement_params + if has_developer_billing_option: + params["developerBillingOption"] = developer_billing_option + for field in ["purchaseToken", "originalExternalTransactionId"]: + if google_props.has(field) and google_props[field] != null: + params[field] = google_props[field] + if purchase_type == "in-app" and not offer_token.is_empty(): + params["offerToken"] = offer_token var params_json = JSON.stringify(params) - print("[GodotIap] Calling Android requestPurchase: type=", purchase_type, ", skus=", params["skus"].size(), ", subscriptionOffers=", params["subscriptionOffers"].size(), ", hasPurchaseToken=", not str(params["purchaseToken"]).is_empty()) + print("[GodotIap] Calling Android requestPurchase: type=", purchase_type, ", skus=", params["skus"].size(), ", subscriptionOffers=", subscription_offers.size(), ", hasPurchaseToken=", params.has("purchaseToken")) result_raw = _native_plugin.call("requestPurchase", params_json) elif _platform == "iOS": var apple_props = request.get("apple", {}) @@ -1221,7 +1320,6 @@ func verify_purchase(props) -> Variant: if _platform == "Android": return Types.VerifyPurchaseResultAndroid.from_dict(result) return null - ## Internal: Verify purchase with raw Dictionary func _verify_purchase_raw(props: Dictionary) -> Dictionary: if _native_plugin and _platform == "Android": diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 097d3c06d..482a3baa6 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -466,12 +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"]) - else: - obj.period = 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: @@ -561,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"] @@ -714,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"] @@ -722,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: @@ -757,12 +769,18 @@ 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"] 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: @@ -797,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 @@ -871,7 +891,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"] @@ -884,9 +904,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 @@ -919,7 +946,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"] @@ -927,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: @@ -1028,7 +1061,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"] @@ -1042,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: @@ -1225,12 +1264,18 @@ 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"] 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: @@ -1258,12 +1303,18 @@ class IapkitProductClientPayload: var version: float = 0.0 var updated_at: float = 0.0 - static func from_dict(data: Dictionary) -> IapkitProductClientPayload: + 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: 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: @@ -1274,6 +1325,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,12 +1346,18 @@ 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"] 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: @@ -1450,6 +1510,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 +1549,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"] @@ -1498,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: @@ -1514,33 +1587,51 @@ 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) - else: + 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 if data.has("discountOffers") and data["discountOffers"] != null: if data["discountOffers"] is Array: 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 @@ -1613,7 +1704,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"] @@ -1625,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: @@ -1641,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: @@ -1653,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: @@ -1660,18 +1769,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 @@ -1742,7 +1865,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"] @@ -1754,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: @@ -1770,24 +1903,35 @@ 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) - else: + 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 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 @@ -1855,13 +1999,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"] @@ -1873,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: @@ -1889,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: @@ -1901,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: @@ -1908,18 +2074,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: @@ -1929,6 +2109,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"] @@ -1940,24 +2123,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: @@ -2053,9 +2248,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 @@ -2096,18 +2291,22 @@ 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) - else: + 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 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: + 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 if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2192,7 +2391,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 @@ -2206,10 +2405,12 @@ 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) - else: + 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 if data.has("message") and data["message"] != null: obj.message = data["message"] if data.has("productId") and data["productId"] != null: @@ -2222,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: @@ -2270,9 +2473,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 = "" @@ -2338,18 +2541,22 @@ 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) - else: + 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 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: + 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 if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2410,10 +2617,12 @@ 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) - else: + 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 if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null: if data["commitmentInfoIOS"] is Dictionary: obj.commitment_info_ios = TransactionCommitmentInfoIOS.from_dict(data["commitmentInfoIOS"]) @@ -2576,7 +2785,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 @@ -2588,10 +2797,12 @@ 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) - else: + 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 if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null: obj.commitment_renewal_date = data["commitmentRenewalDate"] if data.has("commitmentRenewalPrice") and data["commitmentRenewalPrice"] != null: @@ -2670,10 +2881,12 @@ 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) - else: + 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 if data.has("commitmentInfo") and data["commitmentInfo"] != null: if data["commitmentInfo"] is Dictionary: obj.commitment_info = RenewalCommitmentInfoIOS.from_dict(data["commitmentInfo"]) @@ -2754,13 +2967,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. @@ -2770,27 +2983,29 @@ 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) - else: + 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 if data.has("environment") and data["environment"] != null: obj.environment = data["environment"] if data.has("isValid") and data["isValid"] != null: 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) - else: + 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 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"] + obj.client_payload = IapkitProductClientPayload.from_dict_or_null(data["clientPayload"]) return obj func to_dict() -> Dictionary: @@ -2819,15 +3034,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 @@ -2883,7 +3109,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"] @@ -2897,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: @@ -2908,10 +3140,12 @@ 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) - else: + 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 if data.has("keyIdentifierIOS") and data["keyIdentifierIOS"] != null: obj.key_identifier_ios = data["keyIdentifierIOS"] if data.has("nonceIOS") and data["nonceIOS"] != null: @@ -2999,7 +3233,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 @@ -3007,10 +3241,12 @@ 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) - else: + 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 if data.has("value") and data["value"] != null: obj.value = data["value"] return obj @@ -3025,21 +3261,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: + 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): @@ -3052,41 +3299,72 @@ 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: - obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + 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 if data.has("billingPrice") and data["billingPrice"] != null: 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 @@ -3179,7 +3457,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"] @@ -3197,9 +3475,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 @@ -3416,12 +3701,18 @@ 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"] 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: @@ -3437,6 +3728,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 @@ -3487,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"] @@ -3509,14 +3809,24 @@ 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 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") + return null var obj = BillingProgramInformationDialogParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: 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 @@ -3539,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"] @@ -3565,22 +3881,38 @@ 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: + 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") + return null var obj = DeveloperBillingOptionParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: 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 @@ -3616,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"] @@ -3652,20 +3999,37 @@ 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("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") + 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"] 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 @@ -3699,9 +4063,15 @@ class InAppMessageParamsAndroid: 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]) - elif item is int: + elif item is int and IN_APP_MESSAGE_CATEGORY_ANDROID_VALUES.has(item): arr.append(item) + 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: @@ -3729,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: @@ -3766,26 +4142,53 @@ 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("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") + 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"] 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: @@ -3820,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: @@ -3827,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: @@ -3855,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"] @@ -3880,7 +4301,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: @@ -3902,18 +4323,22 @@ 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) - else: + 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 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: + 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 if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] return obj @@ -3955,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"] @@ -3979,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"] @@ -4005,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: @@ -4012,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: @@ -4023,9 +4481,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"]) - else: - obj.developer_billing_option = 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: @@ -4062,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"] @@ -4073,9 +4551,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 @@ -4128,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: @@ -4171,14 +4664,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: @@ -4216,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: @@ -4223,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: @@ -4239,20 +4766,39 @@ 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: - 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"]) - else: - obj.developer_billing_option = 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: @@ -4308,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"] @@ -4319,25 +4883,43 @@ 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: - obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + 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: + 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: @@ -4391,14 +4973,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: @@ -4426,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"] @@ -4454,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"] @@ -4470,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"] @@ -4497,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"] @@ -4506,19 +5125,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: @@ -4551,9 +5185,15 @@ 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: + 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"] @@ -4561,8 +5201,11 @@ class SubscriptionProductReplacementParamsAndroid: 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: + elif enum_str is int and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_VALUES.has(enum_str): obj.replacement_mode = enum_str + else: + push_error("Invalid SubscriptionReplacementModeAndroid input value") + return null return obj func to_dict() -> Dictionary: @@ -4582,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"] @@ -4607,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"] @@ -4644,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"] @@ -4676,19 +5346,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: @@ -4714,19 +5399,31 @@ 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"] 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: - 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: @@ -4749,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"] @@ -5335,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: @@ -5361,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: @@ -5422,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: @@ -5870,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: @@ -5937,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: @@ -5965,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/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/GodotIap.kt b/libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt index ea37000b0..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,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 = validateGodotInAppMessageParams(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 321cbb219..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 @@ -36,7 +36,10 @@ internal object GodotIapHelper { * This avoids Kotlin type inference issues with optString(key, null). */ private fun JSONObject.optStringOrNull(key: String): String? { - return if (has(key)) optString(key).takeIf { it.isNotEmpty() } else null + if (!has(key) || isNull(key)) return null + val value = opt(key) as? String + ?: throw IllegalArgumentException("$key must be a string") + return value.takeIf { it.isNotEmpty() } } /** @@ -98,60 +101,112 @@ internal object GodotIapHelper { val type = json.optStringOrNull("type") // Parse canonical SKUs. - val skus = mutableListOf() val skusArray = json.optJSONArray("skus") - if (skusArray != null) { - for (i in 0 until skusArray.length()) { - skus.add(skusArray.getString(i)) + ?: throw IllegalArgumentException("skus must be an array") + val skus = mutableListOf() + for (i in 0 until skusArray.length()) { + val sku = skusArray.opt(i) as? String + if (sku.isNullOrBlank()) { + throw IllegalArgumentException("skus[$i] must be a non-empty string") } + 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") // Parse other options - val isOfferPersonalized = json.optBoolean("isOfferPersonalized", false) + val isOfferPersonalized = when (val value = json.opt("isOfferPersonalized")) { + null, JSONObject.NULL -> false + is Boolean -> value + else -> throw IllegalArgumentException("isOfferPersonalized must be a boolean") + } val purchaseToken = json.optStringOrNull("purchaseToken") val originalExternalTransactionId = json.optStringOrNull("originalExternalTransactionId") // Parse subscriptionProductReplacementParams (8.1.0+) - val subscriptionProductReplacementParams = if (json.has("subscriptionProductReplacementParams")) { + val subscriptionProductReplacementParams = if ( + json.has("subscriptionProductReplacementParams") && !json.isNull("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.optStringOrNull("oldProductId")?.takeIf { it.isNotBlank() } + ?: 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") && !json.isNull("developerBillingOption")) { + val option = json.optJSONObject("developerBillingOption") + ?: throw IllegalArgumentException("developerBillingOption must be an object") + DeveloperBillingOptionParamsAndroid.fromJson(jsonObjectToMap(option)) + } else null val offerToken = json.optStringOrNull("offerToken") // Parse explicit subscription offers + val hasExplicitSubscriptionOffers = json.has("subscriptionOffers") && !json.isNull("subscriptionOffers") val explicitSubscriptionOffers = mutableListOf() - val offersArray = json.optJSONArray("subscriptionOffers") - if (offersArray != null) { + if (hasExplicitSubscriptionOffers) { + val offersArray = json.optJSONArray("subscriptionOffers") + ?: throw IllegalArgumentException("subscriptionOffers must be an array") for (i in 0 until offersArray.length()) { - val offer = offersArray.getJSONObject(i) - val sku = offer.optString("sku", "") - val offerToken = offer.optString("offerToken", "") - if (sku.isNotEmpty() && offerToken.isNotEmpty()) { - explicitSubscriptionOffers.add( - AndroidSubscriptionOfferInput(offerToken = offerToken, sku = sku) - ) + val offer = offersArray.optJSONObject(i) + ?: throw IllegalArgumentException("subscriptionOffers[$i] must be an object") + val sku = offer.opt("sku") as? String + if (sku.isNullOrBlank()) { + throw IllegalArgumentException("subscriptionOffers[$i].sku must be a non-empty string") } + val offerToken = offer.opt("offerToken") as? String + if (offerToken.isNullOrBlank()) { + throw IllegalArgumentException("subscriptionOffers[$i].offerToken must be a non-empty string") + } + explicitSubscriptionOffers.add( + AndroidSubscriptionOfferInput(offerToken = offerToken, sku = sku) + ) } } + val productType = parseProductQueryType( + rawType = type, + defaultType = ProductQueryType.InApp, + allowAll = false, + ) + if (subscriptionProductReplacementParams != null && productType != ProductQueryType.Subs) { + throw IllegalArgumentException("subscriptionProductReplacementParams requires type `subs`") + } + if (json.has("offerToken") && !json.isNull("offerToken") && productType != ProductQueryType.InApp) { + throw IllegalArgumentException("offerToken requires type `in-app`") + } + if (hasExplicitSubscriptionOffers) { + if (productType != ProductQueryType.Subs) { + throw IllegalArgumentException("subscriptionOffers requires type `subs`") + } + val requested = skus.toSet() + val offered = explicitSubscriptionOffers.mapTo(mutableSetOf()) { it.sku } + if (explicitSubscriptionOffers.any { it.sku !in requested } || !offered.containsAll(requested)) { + throw IllegalArgumentException("subscriptionOffers must match the requested skus") + } + } + if (json.has("purchaseToken") && !json.isNull("purchaseToken") && productType != ProductQueryType.Subs) { + throw IllegalArgumentException("purchaseToken requires type `subs`") + } + if ( + json.has("originalExternalTransactionId") && + !json.isNull("originalExternalTransactionId") && + productType != ProductQueryType.Subs + ) { + throw IllegalArgumentException("originalExternalTransactionId requires type `subs`") + } + return RequestPurchaseParams( type = type, skus = skus, @@ -173,12 +228,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/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 e8b09c013..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 @@ -1,11 +1,46 @@ 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 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 = validateGodotInAppMessageParams( + """{"categories":["transactional","unknown-in-app-message-category-id"]}""", + ) + assertEquals( + listOf( + InAppMessageCategoryAndroid.Transactional, + InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId, + ), + params.categories, + ) + + listOf( + """{"categories":["future-category"]}""", + """{"categories":[999]}""", + """{"categories":"transactional"}""", + ).forEach { json -> + assertThrows(IllegalArgumentException::class.java) { + validateGodotInAppMessageParams(json) + } + } + } + @Test fun `canonical product query types preserve their exact meaning`() { assertEquals( @@ -40,4 +75,109 @@ class GodotIapHelperTest { } } + @Test + 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"}}""", + ) + + 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","skus":["new"],"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( + """{"skus":["coins"],"developerBillingOption":"billing-choice"}""", + """{"skus":["coins"],"developerBillingOption":{}}""", + """{"skus":["coins"],"developerBillingOption":{"billingProgram":"future-program"}}""", + ).forEach { json -> + assertThrows(IllegalArgumentException::class.java) { + GodotIapHelper.parseRequestPurchaseParams(json) + } + } + + val absent = GodotIapHelper.parseRequestPurchaseParams("""{"skus":["coins"]}""") + assertEquals(null, absent.developerBillingOption) + + val nullable = GodotIapHelper.parseRequestPurchaseParams( + """{"type":"subs","skus":["monthly"],"developerBillingOption":null,"subscriptionOffers":null,"subscriptionProductReplacementParams":null}""", + ) + assertEquals(null, nullable.developerBillingOption) + assertEquals(0, nullable.subscriptionOffers.size) + assertEquals(null, nullable.subscriptionProductReplacementParams) + + val valid = GodotIapHelper.parseRequestPurchaseParams( + """{"skus":["coins"],"developerBillingOption":{"billingProgram":"billing-choice"}}""", + ) + assertEquals(BillingProgramAndroid.BillingChoice, valid.developerBillingOption?.billingProgram) + } + + @Test + 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}""", + """{"type":"subs","skus":["monthly"],"isOfferPersonalized":"true"}""", + """{"type":"subs","skus":["monthly"],"offerToken":"one-time-token"}""", + """{"type":"subs","skus":["monthly"],"subscriptionOffers":"offer"}""", + """{"type":"subs","skus":["monthly"],"subscriptionOffers":[]}""", + """{"type":"subs","skus":["monthly"],"subscriptionOffers":[7]}""", + """{"type":"subs","skus":["monthly"],"subscriptionOffers":[{"offerToken":"token"}]}""", + """{"type":"subs","skus":["monthly"],"subscriptionOffers":[{"sku":"monthly"}]}""", + """{"type":"subs","skus":["monthly"],"subscriptionOffers":[{"sku":"yearly","offerToken":"token"}]}""", + """{"type":"in-app","skus":["coins"],"subscriptionOffers":[]}""", + """{"type":"in-app","skus":["coins"],"purchaseToken":"subscription-token"}""", + """{"type":"in-app","skus":["coins"],"originalExternalTransactionId":"external-id"}""", + ).forEach { json -> + assertThrows(IllegalArgumentException::class.java) { + GodotIapHelper.parseRequestPurchaseParams(json) + } + } + + val multipleOffers = GodotIapHelper.parseRequestPurchaseParams( + """{"type":"subs","skus":["monthly"],"subscriptionOffers":[{"sku":"monthly","offerToken":"one"},{"sku":"monthly","offerToken":"two"}]}""", + ) + assertEquals(2, multipleOffers.subscriptionOffers.size) + } + } diff --git a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapHelper.swift b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapHelper.swift index 2029d39ef..09d13fb8b 100644 --- a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapHelper.swift +++ b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIapHelper.swift @@ -96,12 +96,21 @@ enum GodotIapHelper { /// Parse an OpenIAP product query type without silently changing an /// unknown value into another query class. static func parseProductQueryType( - _ rawValue: String?, + _ rawValue: Any?, defaultType: ProductQueryType = .all, allowAll: Bool = true ) throws -> ProductQueryType { - guard let raw = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty - else { + if rawValue == nil || rawValue is NSNull { + return defaultType + } + guard let stringValue = rawValue as? String else { + throw PurchaseError.make( + code: .developerError, + message: "Product query type must be a string." + ) + } + let raw = stringValue.trimmingCharacters(in: .whitespacesAndNewlines) + if raw.isEmpty { return defaultType } @@ -116,7 +125,7 @@ enum GodotIapHelper { default: throw PurchaseError.make( code: .developerError, - message: "Unknown product query type '\(rawValue ?? "")'. Expected in-app, subs, or all." + message: "Unknown product query type '\(stringValue)'. Expected in-app, subs, or all." ) } @@ -133,7 +142,7 @@ enum GodotIapHelper { static func decodeProductRequest(from payload: [String: Any]) throws -> ProductRequest { if let skus = payload["skus"] as? [String], !skus.isEmpty { let type = try parseProductQueryType( - payload["type"] as? String, + payload["type"], defaultType: .all ) return try OpenIapSerialization.productRequest(skus: skus, type: type) @@ -143,7 +152,7 @@ enum GodotIapHelper { var normalized = payload if payload["type"] != nil { normalized["type"] = try parseProductQueryType( - payload["type"] as? String, + payload["type"], defaultType: .all ).rawValue } @@ -167,7 +176,7 @@ enum GodotIapHelper { var normalized = payload let hasSubscription = payload["requestSubscription"] != nil normalized["type"] = try parseProductQueryType( - payload["type"] as? String, + payload["type"], defaultType: hasSubscription ? .subs : .inApp, allowAll: false ).rawValue diff --git a/libraries/godot-iap/ios-gdextension/Tests/GodotIapTests/GodotIapHelperTests.swift b/libraries/godot-iap/ios-gdextension/Tests/GodotIapTests/GodotIapHelperTests.swift index 6a3f7e7f6..d69026dad 100644 --- a/libraries/godot-iap/ios-gdextension/Tests/GodotIapTests/GodotIapHelperTests.swift +++ b/libraries/godot-iap/ios-gdextension/Tests/GodotIapTests/GodotIapHelperTests.swift @@ -47,6 +47,15 @@ final class GodotIapHelperTests: XCTestCase { ) } + func testProductRequestRejectsNonStringType() { + XCTAssertThrowsError( + try GodotIapHelper.decodeProductRequest(from: [ + "skus": ["coins.100"], + "type": 7, + ]) + ) + } + func testCanonicalPurchaseRequestDecodes() throws { let request = try GodotIapHelper.decodeRequestPurchaseProps(from: [ "requestPurchase": [ @@ -107,4 +116,13 @@ final class GodotIapHelperTests: XCTestCase { ]) ) } + + func testPurchaseRequestRejectsNonStringType() { + XCTAssertThrowsError( + try GodotIapHelper.decodeRequestPurchaseProps(from: [ + "requestPurchase": ["apple": ["sku": "coins.100"]], + "type": 7, + ]) + ) + } } 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/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt index 919e0436b..21b527dec 100644 --- a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt +++ b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt @@ -166,6 +166,32 @@ internal fun throwPurchaseRequestFailure( throw PurchaseException(purchaseError) } +internal fun hasInvalidKmpSubscriptionOffers( + 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 isValidKmpSubscriptionOfferToken( + token: String?, + availableTokens: Collection, +): Boolean = !token.isNullOrEmpty() && token in availableTokens + +internal fun areValidKmpSubscriptionOfferTokens( + tokens: Collection, + availableTokens: Collection, +): Boolean = tokens.isNotEmpty() && tokens.all { it in availableTokens } + +internal fun isValidKmpOneTimeOfferToken( + token: String, + availableTokens: Collection, +): Boolean = token.isNotEmpty() && token in availableTokens + internal fun billingProgramConfigurationException( program: BillingProgramAndroid, error: Exception, @@ -1002,6 +1028,18 @@ internal class InAppPurchaseAndroid( ) } + if ( + resolvedType == ProductQueryType.Subs && + hasInvalidKmpSubscriptionOffers(targetSkus, subscriptionOffers) + ) { + failWith( + PurchaseError( + code = ErrorCode.SkuOfferMismatch, + message = "Subscription offers must match the requested SKUs", + ) + ) + } + if (!isSubscriptionReplacementTargetCountValid( targetSkuCount = targetSkus.size, hasProductLevelReplacementParams = @@ -1146,10 +1184,7 @@ internal class InAppPurchaseAndroid( } val paramsList = mutableListOf() - val offersBySku = subscriptionOffers - .groupBy(AndroidSubscriptionOfferInput::sku) - .mapValues { entry -> entry.value.toMutableList() } - .toMutableMap() + val offersBySku = subscriptionOffers.groupBy(AndroidSubscriptionOfferInput::sku) var mismatch = false for (sku in targetSkus) { @@ -1159,10 +1194,27 @@ internal class InAppPurchaseAndroid( if (desiredProductType == BillingClient.ProductType.SUBS) { val availableTokens = detail.subscriptionOfferDetails?.map { it.offerToken }.orEmpty() - val queuedToken = offersBySku[detail.productId]?.takeIf { it.isNotEmpty() }?.removeAt(0)?.offerToken - val resolvedToken = queuedToken ?: detail.subscriptionOfferDetails?.firstOrNull()?.offerToken + val requestedTokens = offersBySku[detail.productId].orEmpty().map { it.offerToken } + if (subscriptionOffers.isNotEmpty() && + !areValidKmpSubscriptionOfferTokens(requestedTokens, availableTokens)) { + failPendingPurchaseFlow( + requestLifecycle, + callback, + PurchaseError( + code = ErrorCode.SkuOfferMismatch, + message = "Offer token mismatch for ${detail.productId}", + ), + ) + mismatch = true + break + } + val resolvedToken = if (subscriptionOffers.isEmpty()) { + detail.subscriptionOfferDetails?.firstOrNull()?.offerToken + } else { + requestedTokens.firstOrNull() + } - if (resolvedToken.isNullOrEmpty() || (availableTokens.isNotEmpty() && !availableTokens.contains(resolvedToken))) { + if (!isValidKmpSubscriptionOfferToken(resolvedToken, availableTokens)) { failPendingPurchaseFlow( requestLifecycle, callback, @@ -1174,6 +1226,7 @@ internal class InAppPurchaseAndroid( mismatch = true break } + val selectedOfferToken = checkNotNull(resolvedToken) // Apply item-level subscription replacement params (8.1.0+) subscriptionProductReplacementParams?.let { params -> @@ -1186,11 +1239,26 @@ internal class InAppPurchaseAndroid( } if (subscriptionProductReplacementParams?.replacementMode != SubscriptionReplacementModeAndroid.KeepExisting) { - builder.setOfferToken(resolvedToken) + builder.setOfferToken(selectedOfferToken) } } else { // Handle offerToken for one-time purchase discounts (Android 8.0+) oneTimePurchaseOfferToken?.let { token -> + val availableTokens = detail.oneTimePurchaseOfferDetailsList + ?.mapNotNull { it.offerToken } + .orEmpty() + if (!isValidKmpOneTimeOfferToken(token, availableTokens)) { + failPendingPurchaseFlow( + requestLifecycle, + callback, + PurchaseError( + code = ErrorCode.SkuOfferMismatch, + message = "Offer token mismatch for ${detail.productId}", + ), + ) + mismatch = true + break + } builder.setOfferToken(token) } } 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..6793624d4 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,12 @@ 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 io.github.hyochan.kmpiap.openiap.AndroidSubscriptionOfferInput import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull class SubscriptionReplacementResolutionTest { @@ -28,4 +33,64 @@ 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 -> + assertFailsWith { + 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, hasInvalidKmpSubscriptionOffers(listOf("monthly"), emptyList())) + assertEquals(false, hasInvalidKmpSubscriptionOffers(listOf("monthly", "yearly"), listOf(monthly, yearly))) + assertEquals(true, hasInvalidKmpSubscriptionOffers(listOf("monthly"), listOf(yearly))) + assertEquals(false, hasInvalidKmpSubscriptionOffers(listOf("monthly"), listOf(monthly, monthly))) + assertEquals(true, hasInvalidKmpSubscriptionOffers(listOf("monthly", "yearly"), listOf(monthly))) + } + + @Test + fun `explicit subscription offer tokens must exist in store metadata`() { + assertEquals(false, isValidKmpSubscriptionOfferToken("token", emptyList())) + assertEquals(false, isValidKmpSubscriptionOfferToken("future", listOf("known"))) + assertEquals(true, isValidKmpSubscriptionOfferToken("known", listOf("known"))) + assertEquals(false, areValidKmpSubscriptionOfferTokens(listOf("known", "future"), listOf("known"))) + assertEquals(false, areValidKmpSubscriptionOfferTokens(listOf("future", "known"), listOf("known"))) + assertEquals(true, areValidKmpSubscriptionOfferTokens(listOf("known", "known"), listOf("known"))) + } + + @Test + fun `explicit one-time offer tokens must exist in store metadata`() { + assertEquals(false, isValidKmpOneTimeOfferToken("", listOf("known"))) + assertEquals(false, isValidKmpOneTimeOfferToken("token", emptyList())) + assertEquals(false, isValidKmpOneTimeOfferToken("future", listOf("known"))) + assertEquals(true, isValidKmpOneTimeOfferToken("known", listOf("known"))) + } } 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..0e52a5fe4 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 @@ -29,15 +29,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 @@ -62,15 +67,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 @@ -121,7 +131,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 @@ -134,8 +144,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 @@ -163,15 +178,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 @@ -196,15 +216,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 @@ -229,7 +254,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 @@ -239,8 +264,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 @@ -285,7 +315,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 @@ -394,8 +424,13 @@ 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 -> 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 @@ -422,15 +457,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 @@ -456,15 +496,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 @@ -483,12 +528,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 @@ -512,15 +562,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 @@ -540,15 +595,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 @@ -576,7 +636,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 @@ -595,8 +655,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 @@ -611,7 +676,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 @@ -621,8 +686,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 @@ -670,7 +740,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 @@ -689,8 +759,13 @@ public enum class IapkitPurchaseState(val rawValue: String) { "UNKNOWN" -> IapkitPurchaseState.Unknown "inauthentic" -> IapkitPurchaseState.Inauthentic "INAUTHENTIC" -> IapkitPurchaseState.Inauthentic - else -> throw IllegalArgumentException("Unknown IapkitPurchaseState value: $value") + 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 @@ -701,14 +776,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 @@ -722,7 +802,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 @@ -738,8 +818,13 @@ public enum class IapStore(val rawValue: String) { "amazon" -> IapStore.Amazon "AMAZON" -> IapStore.Amazon "Amazon" -> IapStore.Amazon - else -> throw IllegalArgumentException("Unknown IapStore value: $value") + 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 @@ -761,13 +846,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 -> throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid value: $value") + 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 @@ -789,13 +879,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 @@ -824,7 +919,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 @@ -837,8 +932,13 @@ public enum class PaymentMode(val rawValue: String) { "unknown" -> PaymentMode.Unknown "UNKNOWN" -> PaymentMode.Unknown "Unknown" -> PaymentMode.Unknown - else -> throw IllegalArgumentException("Unknown PaymentMode value: $value") + 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 @@ -851,7 +951,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 @@ -864,8 +964,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 @@ -877,7 +982,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 @@ -887,8 +992,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 @@ -919,7 +1029,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 @@ -928,8 +1038,13 @@ 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 -> 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 @@ -940,15 +1055,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 @@ -969,7 +1089,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 @@ -988,8 +1108,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 @@ -1001,7 +1126,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 @@ -1011,8 +1136,13 @@ public enum class PurchaseState(val rawValue: String) { "unknown" -> PurchaseState.Unknown "UNKNOWN" -> PurchaseState.Unknown "Unknown" -> PurchaseState.Unknown - else -> throw IllegalArgumentException("Unknown PurchaseState value: $value") + 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 @@ -1022,12 +1152,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 @@ -1052,15 +1187,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 @@ -1081,7 +1221,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 @@ -1091,8 +1231,13 @@ 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 -> 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 @@ -1108,7 +1253,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 @@ -1118,8 +1263,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 @@ -1133,7 +1283,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 @@ -1149,8 +1299,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 @@ -1167,7 +1322,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 @@ -1183,8 +1338,13 @@ public enum class SubscriptionPeriodUnit(val rawValue: String) { "unknown" -> SubscriptionPeriodUnit.Unknown "UNKNOWN" -> SubscriptionPeriodUnit.Unknown "Unknown" -> SubscriptionPeriodUnit.Unknown - else -> throw IllegalArgumentException("Unknown SubscriptionPeriodUnit value: $value") + 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 @@ -1226,7 +1386,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 @@ -1241,8 +1401,13 @@ 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 -> 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 @@ -1428,7 +1593,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, @@ -1668,7 +1833,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 +1871,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 +2038,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 +2223,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 +2391,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"), ) } } @@ -2262,7 +2427,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, ) @@ -2298,7 +2463,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,9 +2708,9 @@ 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 { 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, @@ -2619,7 +2784,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 +2848,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 { 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, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, ) } } @@ -2781,8 +2946,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"), ) } } @@ -2888,11 +3053,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 +3134,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 +3256,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 +3278,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 +3288,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 +3404,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 +3509,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 +3659,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,14 +3817,14 @@ 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, 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"), ) } } @@ -3705,7 +3870,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 +3918,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") }, @@ -4109,7 +4274,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"), ) } } @@ -4170,7 +4335,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( @@ -4199,8 +4364,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") }, ) } } @@ -4241,10 +4406,10 @@ public data class DeveloperBillingOptionParamsAndroid( ) { companion object { fun fromJson(json: Map): DeveloperBillingOptionParamsAndroid? { - val billingProgram = (json["billingProgram"] as? String)?.let { BillingProgramAndroid.fromJson(it) } ?: BillingProgramAndroid.Unspecified - 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, @@ -4332,9 +4497,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") }, ) } } @@ -4360,7 +4525,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 = 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"), ) } } @@ -4399,8 +4564,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") }, ) } } @@ -4443,10 +4608,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 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 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( @@ -4474,9 +4639,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, @@ -4548,9 +4712,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") }, ) } } @@ -4573,7 +4737,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") }, ) } } @@ -4616,13 +4780,12 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - 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 developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } + 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, @@ -4676,12 +4839,12 @@ 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"] 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 +4950,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,16 +5011,15 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - 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 subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } - if (skus == null) return null + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } + 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") } return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, isOfferPersonalized = isOfferPersonalized, @@ -4930,16 +5092,16 @@ 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 { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown } - val compactJWS = json["compactJWS"] as? String - val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } - 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 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"]?.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"] 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 +5154,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") }, ) } } @@ -5042,9 +5204,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, @@ -5167,12 +5329,12 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( - amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, - apiKey = json["apiKey"] as? String, - apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, - baseUrl = json["baseUrl"] as? String, - google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, - includeClientPayload = json["includeClientPayload"] as? Boolean, + amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, + 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"]?.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"]?.let { raw -> (raw as? Boolean) ?: throw IllegalArgumentException("Invalid Boolean input value") }, ) } } @@ -5205,7 +5367,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.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for SubscriptionReplacementModeAndroid") if (oldProductId == null) return null return SubscriptionProductReplacementParamsAndroid( oldProductId = oldProductId, @@ -5278,7 +5440,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 @@ -5369,9 +5531,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 +5551,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.fromJsonStrict(it) } ?: throw IllegalArgumentException("Missing or invalid enum input value for PurchaseVerificationProvider") return VerifyPurchaseWithProviderProps( iapkit = iapkit, provider = provider, diff --git a/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt b/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt index a2d572690..da614ae2b 100644 --- a/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt +++ b/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt @@ -7,6 +7,7 @@ import io.github.hyochan.kmpiap.openiap.* import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -20,6 +21,63 @@ class InAppPurchaseTest { assertEquals("user-cancelled", errorCode.toJson()) } + @Test + fun testInAppMessageInputRejectsUnknownCategories() { + 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 -> + assertFailsWith { + InAppMessageParamsAndroid.fromJson(input) + } + } + } + + @Test + fun testSubscriptionPurchaseInputRejectsMalformedExplicitLists() { + 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 -> + assertFailsWith { + RequestSubscriptionAndroidProps.fromJson(input) + } + } + } + + @Test + fun testPurchaseInputRejectsNonIntegralOrOutOfRangeQuantity() { + listOf(1.5, Long.MAX_VALUE, Double.POSITIVE_INFINITY).forEach { quantity -> + assertFailsWith { + RequestPurchaseIosProps.fromJson( + mapOf("sku" to "coins", "quantity" to quantity) + ) + } + } + + assertEquals( + 2, + RequestPurchaseIosProps.fromJson(mapOf("sku" to "coins", "quantity" to 2))?.quantity, + ) + } + @Test fun testPlatformTypes() { val platform = IapPlatform.Android diff --git a/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs b/libraries/maui-iap/src/OpenIap.Maui/KitApi.cs index ea18bc480..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; @@ -24,6 +25,78 @@ 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); + 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) + => 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(); + var index = 0; + foreach (var element in document.RootElement.EnumerateArray()) + { + var value = TolerantJson.Deserialize(element, options); + 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; + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) + => JsonSerializer.Serialize(writer, value, options); +} + public sealed record KitSubscription { [JsonPropertyName("id")] @@ -63,6 +136,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 +145,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 +211,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 +230,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..726a73d14 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -13,6 +13,118 @@ 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) + { + 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}"); + } + + 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) + { + 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}"); + } + + 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 +144,7 @@ public enum BillingChoiceImageLayoutAndroid RectangularTwoByTwo } -public sealed class BillingChoiceImageLayoutAndroidJsonConverter : JsonConverter +public sealed class BillingChoiceImageLayoutAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -66,6 +178,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 +206,7 @@ public enum BillingChoiceScreenTypeAndroid GoogleRendered } -public sealed class BillingChoiceScreenTypeAndroidJsonConverter : JsonConverter +public sealed class BillingChoiceScreenTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -123,6 +240,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 +298,7 @@ public enum BillingProgramAndroid BillingChoice } -public sealed class BillingProgramAndroidJsonConverter : JsonConverter +public sealed class BillingProgramAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -219,6 +341,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 +376,7 @@ public enum DeveloperBillingLaunchModeAndroid CallerWillLaunchLink } -public sealed class DeveloperBillingLaunchModeAndroidJsonConverter : JsonConverter +public sealed class DeveloperBillingLaunchModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -283,6 +410,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 +438,7 @@ public enum DeveloperBillingTypeAndroid ExternalLink } -public sealed class DeveloperBillingTypeAndroidJsonConverter : JsonConverter +public sealed class DeveloperBillingTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -340,6 +472,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 +500,7 @@ public enum DiscountOfferType OneTime } -public sealed class DiscountOfferTypeJsonConverter : JsonConverter +public sealed class DiscountOfferTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -400,6 +537,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 +591,7 @@ public enum ErrorCode DuplicatePurchase } -public sealed class ErrorCodeJsonConverter : JsonConverter +public sealed class ErrorCodeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -607,7 +749,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 +759,12 @@ 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 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 +790,7 @@ public enum ExternalLinkLaunchModeAndroid CallerWillLaunchLink } -public sealed class ExternalLinkLaunchModeAndroidJsonConverter : JsonConverter +public sealed class ExternalLinkLaunchModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -677,6 +824,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 +853,7 @@ public enum ExternalLinkTypeAndroid LinkToAppDownload } -public sealed class ExternalLinkTypeAndroidJsonConverter : JsonConverter +public sealed class ExternalLinkTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -735,6 +887,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 +915,7 @@ public enum ExternalPurchaseCustomLinkNoticeTypeIOS Browser } -public sealed class ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter : JsonConverter +public sealed class ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -787,6 +944,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 +977,7 @@ public enum ExternalPurchaseCustomLinkTokenTypeIOS Services } -public sealed class ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter : JsonConverter +public sealed class ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -848,6 +1010,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 +1033,7 @@ public enum ExternalPurchaseNoticeAction Dismissed } -public sealed class ExternalPurchaseNoticeActionJsonConverter : JsonConverter +public sealed class ExternalPurchaseNoticeActionJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -899,6 +1066,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 +1103,7 @@ public enum IapEvent SubscriptionBillingIssue } -public sealed class IapEventJsonConverter : JsonConverter +public sealed class IapEventJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -980,6 +1152,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 +1174,7 @@ public enum IapkitClientPayloadFormat Text } -public sealed class IapkitClientPayloadFormatJsonConverter : JsonConverter +public sealed class IapkitClientPayloadFormatJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1034,6 +1211,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 +1248,7 @@ public enum IapkitPurchaseState Inauthentic } -public sealed class IapkitPurchaseStateJsonConverter : JsonConverter +public sealed class IapkitPurchaseStateJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1107,7 +1289,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 +1299,12 @@ 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 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 +1320,7 @@ public enum IapPlatform Android } -public sealed class IapPlatformJsonConverter : JsonConverter +public sealed class IapPlatformJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1165,6 +1352,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 +1375,7 @@ public enum IapStore Amazon } -public sealed class IapStoreJsonConverter : JsonConverter +public sealed class IapStoreJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1217,7 +1409,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 +1419,12 @@ 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 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 +1447,7 @@ public enum InAppMessageCategoryAndroid Transactional } -public sealed class InAppMessageCategoryAndroidJsonConverter : JsonConverter +public sealed class InAppMessageCategoryAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1270,7 +1467,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 +1477,12 @@ 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 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 +1505,7 @@ public enum InAppMessageResponseCodeAndroid SubscriptionStatusUpdated } -public sealed class InAppMessageResponseCodeAndroidJsonConverter : JsonConverter +public sealed class InAppMessageResponseCodeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1334,6 +1536,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 +1566,7 @@ public enum PaymentMode Unknown } -public sealed class PaymentModeJsonConverter : JsonConverter +public sealed class PaymentModeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1389,7 +1596,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 +1606,12 @@ 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 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 +1629,7 @@ public enum PaymentModeIOS PayUpFront } -public sealed class PaymentModeIOSJsonConverter : JsonConverter +public sealed class PaymentModeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1458,6 +1670,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 +1691,7 @@ public enum ProductQueryType All } -public sealed class ProductQueryTypeJsonConverter : JsonConverter +public sealed class ProductQueryTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1511,6 +1728,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 +1760,7 @@ public enum ProductStatusAndroid Unknown } -public sealed class ProductStatusAndroidJsonConverter : JsonConverter +public sealed class ProductStatusAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1564,7 +1786,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 +1796,12 @@ 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 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 +1817,7 @@ public enum ProductType Subs } -public sealed class ProductTypeJsonConverter : JsonConverter +public sealed class ProductTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1623,6 +1850,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 +1876,7 @@ public enum ProductTypeIOS SubscriptionSuite } -public sealed class ProductTypeIOSJsonConverter : JsonConverter +public sealed class ProductTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1693,6 +1925,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 +1946,7 @@ public enum PurchaseState Unknown } -public sealed class PurchaseStateJsonConverter : JsonConverter +public sealed class PurchaseStateJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1735,7 +1972,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 +1982,12 @@ 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 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 +2002,7 @@ public enum PurchaseVerificationProvider Iapkit } -public sealed class PurchaseVerificationProviderJsonConverter : JsonConverter +public sealed class PurchaseVerificationProviderJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1789,6 +2031,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 +2059,7 @@ public enum SubResponseCodeAndroid UserIneligible } -public sealed class SubResponseCodeAndroidJsonConverter : JsonConverter +public sealed class SubResponseCodeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1846,6 +2093,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 +2117,7 @@ public enum SubscriptionBillingPlanTypeIOS UpFront } -public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter +public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1891,7 +2143,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 +2153,12 @@ 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 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 +2179,7 @@ public enum SubscriptionOfferTypeIOS WinBack } -public sealed class SubscriptionOfferTypeIOSJsonConverter : JsonConverter +public sealed class SubscriptionOfferTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1959,6 +2216,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 +2239,7 @@ public enum SubscriptionPeriodIOS Empty } -public sealed class SubscriptionPeriodIOSJsonConverter : JsonConverter +public sealed class SubscriptionPeriodIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2022,6 +2284,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 +2308,7 @@ public enum SubscriptionPeriodUnit Unknown } -public sealed class SubscriptionPeriodUnitJsonConverter : JsonConverter +public sealed class SubscriptionPeriodUnitJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2075,7 +2342,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 +2352,12 @@ 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 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 +2390,7 @@ public enum SubscriptionReplacementModeAndroid KeepExisting } -public sealed class SubscriptionReplacementModeAndroidJsonConverter : JsonConverter +public sealed class SubscriptionReplacementModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2153,7 +2425,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 +2435,12 @@ 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 bool TryReadRaw(string value, out SubscriptionReplacementModeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubscriptionReplacementModeAndroid value) => _toString[value]; } public static class SubscriptionReplacementModeAndroidExtensions @@ -2374,6 +2651,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 +3117,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 +3816,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 +3981,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")] @@ -3888,6 +4213,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")] @@ -3915,6 +4241,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. @@ -3927,6 +4254,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 @@ -3963,9 +4291,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")] @@ -3981,6 +4311,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 }; } @@ -3998,6 +4329,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 @@ -4009,6 +4341,7 @@ public sealed record InitConnectionConfig /// Defaults to GOOGLE_RENDERED. /// [JsonPropertyName("billingChoiceScreenTypeAndroid")] + [JsonConverter(typeof(StrictNullableEnumJsonConverter))] public BillingChoiceScreenTypeAndroid? BillingChoiceScreenTypeAndroid { get; init; } = global::OpenIap.BillingChoiceScreenTypeAndroid.GoogleRendered; } @@ -4022,12 +4355,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")] @@ -4046,6 +4382,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; } @@ -4177,6 +4514,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() @@ -4297,6 +4635,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 @@ -4419,6 +4758,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; } } @@ -4510,6 +4850,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/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..6f9aea65c 100644 --- a/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs +++ b/libraries/maui-iap/tests/OpenIap.Maui.Tests/EnumJsonTests.cs @@ -110,10 +110,34 @@ 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\"")); + } + + [Fact] + public void InAppMessageInput_RejectsUnknownAndMalformedCategories() + { + var known = JsonSerializer.Deserialize( + """{"categories":["unknown-in-app-message-category-id"]}"""); + Assert.Equal(InAppMessageCategoryAndroid.UnknownInAppMessageCategoryId, Assert.Single(known!.Categories!)); + + Assert.Throws(() => JsonSerializer.Deserialize( + """{"categories":["future-category"]}""")); + Assert.Throws(() => JsonSerializer.Deserialize( + """{"categories":[999]}""")); + Assert.Throws(() => JsonSerializer.Deserialize( + """{"categories":"transactional"}""")); + } + + [Fact] + public void RequestEnumInputs_RejectNonStringTokensAsJsonErrors() + { + Assert.Throws(() => JsonSerializer.Deserialize( + """{"billingProgram":7}""")); + Assert.Throws(() => JsonSerializer.Deserialize( + """{"enableBillingProgramAndroid":true}""")); } [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..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; @@ -134,6 +136,61 @@ 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); + using var diagnostics = new StringWriter(); + using var listener = new TextWriterTraceListener(diagnostics); + Trace.Listeners.Add(listener); + + 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] public async Task ClientPayload_UsesEscapedUriAndDeserializes() { @@ -241,6 +298,48 @@ 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); + using var diagnostics = new StringWriter(); + using var listener = new TextWriterTraceListener(diagnostics); + Trace.Listeners.Add(listener); + + 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] public async Task Entitlements_BuildsUriAndDeserializes() { @@ -264,6 +363,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/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt index 71a16052d..fbb330311 100644 --- a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -4,7 +4,7 @@ import com.facebook.react.bridge.ReactApplicationContext import com.margelo.nitro.NitroModules import com.margelo.nitro.core.NullType import com.margelo.nitro.core.Promise -import dev.hyo.openiap.AndroidSubscriptionOfferInput +import dev.hyo.openiap.AndroidSubscriptionOfferInput as OpenIapAndroidSubscriptionOfferInput import dev.hyo.openiap.DeepLinkOptions as OpenIapDeepLinkOptions import dev.hyo.openiap.FetchProductsResult import dev.hyo.openiap.FetchProductsResultAll @@ -101,6 +101,44 @@ internal fun removeSingletonBridgeListener( } } +internal fun requireValidSubscriptionOffers( + offers: Array?, +): List { + require(offers.orEmpty().all { it.sku.isNotBlank() && it.offerToken.isNotBlank() }) { + "Every subscription offer must include non-empty sku and offerToken strings" + } + return offers.orEmpty().map { offer -> + OpenIapAndroidSubscriptionOfferInput( + sku = offer.sku, + offerToken = offer.offerToken, + ) + } +} + +internal fun requireMatchingPurchaseOptions( + type: ProductQueryType, + hasSubscriptionOffers: Boolean, + hasSubscriptionReplacementParams: Boolean, + purchaseToken: String?, + originalExternalTransactionId: String?, + offerToken: String?, +) { + when (type) { + ProductQueryType.InApp -> require( + !hasSubscriptionOffers && + !hasSubscriptionReplacementParams && + purchaseToken == null && + originalExternalTransactionId == null, + ) { "Subscription options require product type subs" } + ProductQueryType.Subs -> require(offerToken == null) { + "offerToken requires product type in-app" + } + ProductQueryType.All -> throw IllegalArgumentException( + "Product type all is only supported for product queries" + ) + } +} + /** * Bound for the pending purchase-event queues, matching expo-iap's * ExpoIapHelper.MAX_BUFFERED_EVENTS. Oldest entries are dropped on overflow. @@ -557,17 +595,37 @@ class HybridRnIap : HybridRnIapSpec() { val typeHint = androidRequest.skus.firstOrNull()?.let { productTypeBySku[it] } ?: "in-app" val queryType = parseProductQueryType(typeHint) - val subscriptionOffers = (androidRequest.subscriptionOffers as? Variant_NullType_Array_AndroidSubscriptionOfferInput_.Second)?.value - ?.mapNotNull { offer -> - val sku = offer.sku - val token = offer.offerToken - if (sku.isBlank() || token.isBlank()) { - null - } else { - AndroidSubscriptionOfferInput(sku = sku, offerToken = token) - } - } - ?: emptyList() + try { + requireMatchingPurchaseOptions( + type = queryType, + hasSubscriptionOffers = androidRequest.subscriptionOffers is + Variant_NullType_Array_AndroidSubscriptionOfferInput_.Second, + hasSubscriptionReplacementParams = + androidRequest.subscriptionProductReplacementParams is + Variant_NullType_SubscriptionProductReplacementParamsAndroid.Second, + purchaseToken = androidRequest.purchaseToken.unwrapString(), + originalExternalTransactionId = + androidRequest.originalExternalTransactionId.unwrapString(), + offerToken = androidRequest.offerToken.unwrapString(), + ) + } catch (error: IllegalArgumentException) { + sendPurchaseError( + toErrorResult(OpenIapError.DeveloperError(error.message)) + ) + return@async defaultResult + } + + val subscriptionOffers = try { + requireValidSubscriptionOffers( + (androidRequest.subscriptionOffers as? + Variant_NullType_Array_AndroidSubscriptionOfferInput_.Second)?.value, + ) + } catch (error: IllegalArgumentException) { + sendPurchaseError( + toErrorResult(OpenIapError.DeveloperError(error.message)) + ) + return@async defaultResult + } val normalizedOffers = subscriptionOffers.takeIf { it.isNotEmpty() } val developerBillingOption = (androidRequest.developerBillingOption as? diff --git a/libraries/react-native-iap/android/src/test/java/com/margelo/nitro/iap/SubscriptionOfferValidationTest.kt b/libraries/react-native-iap/android/src/test/java/com/margelo/nitro/iap/SubscriptionOfferValidationTest.kt new file mode 100644 index 000000000..e3e4dcd30 --- /dev/null +++ b/libraries/react-native-iap/android/src/test/java/com/margelo/nitro/iap/SubscriptionOfferValidationTest.kt @@ -0,0 +1,54 @@ +package com.margelo.nitro.iap + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test +import dev.hyo.openiap.ProductQueryType + +class SubscriptionOfferValidationTest { + @Test + fun `valid offers preserve every entry`() { + val offers = arrayOf( + AndroidSubscriptionOfferInput(offerToken = "base", sku = "monthly"), + AndroidSubscriptionOfferInput(offerToken = "promo", sku = "monthly"), + ) + + assertEquals(listOf("base", "promo"), requireValidSubscriptionOffers(offers).map { it.offerToken }) + } + + @Test + fun `blank offer fields reject the whole list`() { + listOf( + AndroidSubscriptionOfferInput(offerToken = "", sku = "monthly"), + AndroidSubscriptionOfferInput(offerToken = "base", sku = ""), + ).forEach { invalidOffer -> + assertThrows(IllegalArgumentException::class.java) { + requireValidSubscriptionOffers(arrayOf(invalidOffer)) + } + } + } + + @Test + fun `branch specific options cannot cross purchase types`() { + assertThrows(IllegalArgumentException::class.java) { + requireMatchingPurchaseOptions( + type = ProductQueryType.InApp, + hasSubscriptionOffers = true, + hasSubscriptionReplacementParams = false, + purchaseToken = null, + originalExternalTransactionId = null, + offerToken = null, + ) + } + assertThrows(IllegalArgumentException::class.java) { + requireMatchingPurchaseOptions( + type = ProductQueryType.Subs, + hasSubscriptionOffers = false, + hasSubscriptionReplacementParams = false, + purchaseToken = null, + originalExternalTransactionId = null, + offerToken = "one-time-token", + ) + } + } +} 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 9106ed12e..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'; @@ -66,6 +67,19 @@ describe('Amazon Vega public API', () => { }); }); + it('rejects all without dispatching a purchase', async () => { + await expect( + IAP.requestPurchase({ + request: {google: {skus: ['coins']}}, + type: 'all' as any, + }), + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: expect.stringMatching(/only supported for product queries/), + }); + expect(requestPurchaseNative).not.toHaveBeenCalled(); + }); + it('rejects a malformed purchase result instead of returning partial success', async () => { requestPurchaseNative.mockResolvedValueOnce([ { diff --git a/libraries/react-native-iap/src/__tests__/index.test.ts b/libraries/react-native-iap/src/__tests__/index.test.ts index c62ff8c65..79d0e0e62 100644 --- a/libraries/react-native-iap/src/__tests__/index.test.ts +++ b/libraries/react-native-iap/src/__tests__/index.test.ts @@ -748,6 +748,20 @@ describe('Public API (src/index.ts)', () => { }); describe('requestPurchase', () => { + it('rejects all without dispatching a purchase', async () => { + (Platform as any).OS = 'android'; + await expect( + IAP.requestPurchase({ + request: {google: {skus: ['p1']}}, + type: 'all' as any, + }), + ).rejects.toMatchObject({ + code: ErrorCode.DeveloperError, + message: expect.stringMatching(/only supported for product queries/), + }); + expect(mockIap.requestPurchase).not.toHaveBeenCalled(); + }); + it('requires apple.sku on iOS', async () => { (Platform as any).OS = 'ios'; await expect( @@ -859,6 +873,54 @@ describe('Public API (src/index.ts)', () => { ]); }); + it.each([ + [[[null]]], + [[{sku: 'sub1'}]], + [[{sku: '', offerToken: 'offer-token'}]], + [[{sku: 'sub1', offerToken: ''}]], + ])( + 'Android subs rejects malformed explicit offers without dispatching: %j', + async (subscriptionOffers) => { + (Platform as any).OS = 'android'; + await expect( + IAP.requestPurchase({ + request: { + google: { + skus: ['sub1'], + subscriptionOffers, + } as any, + }, + type: 'subs', + }), + ).rejects.toThrow(/Every subscription offer/); + expect(mockIap.requestPurchase).not.toHaveBeenCalled(); + }, + ); + + it.each([ + ['in-app', {skus: ['coins'], subscriptionOffers: []}], + [ + 'in-app', + { + skus: ['coins'], + subscriptionProductReplacementParams: { + oldProductId: 'old', + replacementMode: 'without-proration', + }, + }, + ], + ['subs', {skus: ['premium'], offerToken: 'one-time-token'}], + ])( + 'rejects branch-mismatched Android options for %s without dispatching', + async (type, google) => { + (Platform as any).OS = 'android'; + await expect( + IAP.requestPurchase({request: {google} as any, type: type as any}), + ).rejects.toThrow(/must match the selected product type/); + expect(mockIap.requestPurchase).not.toHaveBeenCalled(); + }, + ); + it('Android subs forwards subscriptionProductReplacementParams when provided', async () => { (Platform as any).OS = 'android'; await IAP.requestPurchase({ diff --git a/libraries/react-native-iap/src/index.kepler.ts b/libraries/react-native-iap/src/index.kepler.ts index 92e0490f5..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'; @@ -129,13 +131,19 @@ export const fetchProducts = async ( const normalizedType = normalizeProductQueryType(type); const nitroProducts = await getModule().fetchProducts(skus, normalizedType); return mapProducts(nitroProducts, normalizedType) as - | Product[] - | ProductSubscription[]; + Product[] | ProductSubscription[]; }; export const requestPurchase: MutationField<'requestPurchase'> = async ( request, ) => { + const type = normalizeProductQueryType(request.type); + if (type === 'all') { + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); + } const perPlatformRequest = request.request as | RequestPurchasePropsByPlatforms | RequestSubscriptionPropsByPlatforms diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index 64e69443f..3262b61fa 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -1678,6 +1678,12 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( try { const {request: platformRequest, type} = request; const normalizedType = normalizeProductQueryType(type ?? 'in-app'); + if (normalizedType === 'all') { + throw createPurchaseError({ + code: ErrorCode.DeveloperError, + message: 'Product type all is only supported for product queries.', + }); + } const isSubs = isSubscriptionQuery(normalizedType); const perPlatformRequest = platformRequest as | RequestPurchasePropsByPlatforms @@ -1702,10 +1708,15 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( }); } } else if (isAndroidStoreRuntime()) { - if (!androidRequestSource?.skus?.length) { + const skus = androidRequestSource?.skus as unknown; + if ( + !Array.isArray(skus) || + skus.length === 0 || + skus.some((sku) => typeof sku !== 'string' || sku.trim() === '') + ) { throw createPurchaseError({ message: - 'Invalid request for Android. The `skus` property is required and must be a non-empty array.', + 'Invalid request for Android. The `skus` property must contain only non-empty strings.', code: ErrorCode.EmptySkuList, }); } @@ -1764,6 +1775,29 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( } if (isAndroidStoreRuntime() && androidRequestSource) { + const rawAndroidRequest = androidRequestSource as unknown as Record< + string, + unknown + >; + const subscriptionOnlyFields = [ + 'subscriptionOffers', + 'subscriptionProductReplacementParams', + 'purchaseToken', + 'originalExternalTransactionId', + ]; + if ( + (!isSubs && + subscriptionOnlyFields.some( + (field) => rawAndroidRequest[field] != null, + )) || + (isSubs && rawAndroidRequest.offerToken != null) + ) { + throw createPurchaseError({ + message: + 'Invalid request for Android. Purchase options must match the selected product type.', + code: ErrorCode.DeveloperError, + }); + } const androidRequest = isSubs ? (androidRequestSource as RequestSubscriptionAndroidProps) : (androidRequestSource as RequestPurchaseAndroidProps); @@ -1796,6 +1830,29 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( if (isSubs) { const subsRequest = androidRequest as RequestSubscriptionAndroidProps; + const subscriptionOffers = subsRequest.subscriptionOffers as unknown; + if ( + subscriptionOffers != null && + (!Array.isArray(subscriptionOffers) || + subscriptionOffers.some((offer) => { + if (offer == null || typeof offer !== 'object') { + return true; + } + const candidate = offer as Record; + return ( + typeof candidate.sku !== 'string' || + candidate.sku.trim() === '' || + typeof candidate.offerToken !== 'string' || + candidate.offerToken.trim() === '' + ); + })) + ) { + throw createPurchaseError({ + message: + 'Invalid request for Android. Every subscription offer must include non-empty `sku` and `offerToken` strings.', + code: ErrorCode.DeveloperError, + }); + } if (subsRequest.purchaseToken) { androidPayload.purchaseToken = subsRequest.purchaseToken; } @@ -1808,15 +1865,11 @@ export const requestPurchase: MutationField<'requestPurchase'> = async ( subsRequest.subscriptionProductReplacementParams; } androidPayload.subscriptionOffers = ( - subsRequest.subscriptionOffers ?? [] - ) - .filter( - (offer): offer is AndroidSubscriptionOfferInput => offer != null, - ) - .map((offer) => ({ - sku: offer.sku, - offerToken: offer.offerToken, - })); + (subscriptionOffers ?? []) as AndroidSubscriptionOfferInput[] + ).map((offer) => ({ + sku: offer.sku, + offerToken: offer.offerToken, + })); } unifiedRequest.google = androidPayload; diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 81ff94c19..73777d530 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -438,8 +438,8 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// Performs the StoreKit request while helper-level error emissions are /// suppressed. `requestPurchase` emits the final canonical error exactly once. private func performRequestPurchase(_ params: RequestPurchaseProps) async throws -> RequestPurchaseResult? { - try await ensureConnection() let iosProps = try resolveIOSPurchaseProps(from: params) + try await ensureConnection() let sku = iosProps.sku let product = try await storeProduct(for: sku) #if os(iOS) @@ -2022,6 +2022,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// Resolves iOS purchase props from request params. /// Returns either RequestPurchaseIosProps or RequestSubscriptionIosProps based on request type. func resolveIOSPurchaseProps(from params: RequestPurchaseProps) throws -> any IosPropsProtocol { + try Self.validateIOSPurchaseProps(params) switch params.request { case let .purchase(platforms): if let ios = platforms.apple { @@ -2035,6 +2036,25 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { throw makePurchaseError(code: .purchaseError, message: "Missing iOS purchase parameters") } + static func validateIOSPurchaseProps(_ params: RequestPurchaseProps) throws { + switch params.request { + case .purchase where params.type == .inApp: + return + case .subscription where params.type == .subs: + return + case .purchase: + throw PurchaseError.make( + code: .developerError, + message: "type must be in-app when requestPurchase is provided" + ) + case .subscription: + throw PurchaseError.make( + code: .developerError, + message: "type must be subs when requestSubscription is provided" + ) + } + } + private func purchaseProductId(from params: RequestPurchaseProps) -> String? { switch params.request { case let .purchase(platforms): diff --git a/packages/apple/Sources/OpenIapStore.swift b/packages/apple/Sources/OpenIapStore.swift index 6d9dd610c..50b4b81e1 100644 --- a/packages/apple/Sources/OpenIapStore.swift +++ b/packages/apple/Sources/OpenIapStore.swift @@ -322,7 +322,7 @@ public final class OpenIapStore: ObservableObject { type: .subs ) return try await requestPurchase(request) - default: + case .inApp: let iosProps = RequestPurchaseIosProps( advancedCommerceData: advancedCommerceData, andDangerouslyFinishTransactionAutomatically: autoFinish, @@ -336,10 +336,16 @@ public final class OpenIapStore: ObservableObject { type: .inApp ) return try await requestPurchase(request) + case .all: + throw PurchaseError.make( + code: .developerError, + message: "Product type all is only supported for product queries." + ) } } public func requestPurchase(_ params: RequestPurchaseProps) async throws -> OpenIAP.Purchase? { + try OpenIapModule.validateIOSPurchaseProps(params) clearCurrentPurchase() clearCurrentPurchaseError() diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift index 626cefe2a..5080bd6fc 100644 --- a/packages/apple/Tests/OpenIapTests.swift +++ b/packages/apple/Tests/OpenIapTests.swift @@ -891,6 +891,22 @@ final class OpenIapTests: XCTestCase { XCTAssertEqual(resolved.sku, "dev.hyo.sub.apple") } + @available(iOS 15.0, macOS 14.0, tvOS 16.0, watchOS 8.0, *) + func testResolvePurchasePropsRejectsMutatedQueryOnlyType() throws { + var props = RequestPurchaseProps( + request: .purchase(RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: "dev.hyo.apple") + )) + ) + props.type = .all + + XCTAssertThrowsError( + try OpenIapModule.shared.resolveIOSPurchaseProps(from: props) + ) { error in + XCTAssertEqual((error as? PurchaseError)?.code, .developerError) + } + } + func testAdvancedCommerceDataJSONSerialization() throws { let props = RequestPurchaseIosProps( advancedCommerceData: "promo_code_abc", diff --git a/packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift b/packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift index db7501064..456fae824 100644 --- a/packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift +++ b/packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift @@ -155,6 +155,48 @@ final class VerifyPurchaseTests: XCTestCase { XCTAssertEqual(platforms.apple?.sku, "dev.hyo.premium") } + @MainActor + func testConveniencePurchaseRejectsQueryOnlyAllType() async { + let module = FakeOpenIapModule( + validateResult: .verifyPurchaseResultIos(makeVerifyPurchaseResult()) + ) + let store = OpenIapStore(module: module) + + do { + _ = try await store.requestPurchase(sku: "dev.hyo.coins", type: .all) + XCTFail("Expected query-only type to be rejected") + } catch let error as PurchaseError { + XCTAssertEqual(error.code, .developerError) + } catch { + XCTFail("Unexpected error: \(error)") + } + XCTAssertNil(module.lastRequestPurchaseProps) + } + + @MainActor + func testPurchaseRejectsMutatedQueryOnlyTypeBeforeModuleDispatch() async { + let module = FakeOpenIapModule( + validateResult: .verifyPurchaseResultIos(makeVerifyPurchaseResult()) + ) + let store = OpenIapStore(module: module) + var props = RequestPurchaseProps( + request: .purchase(RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: "dev.hyo.coins") + )) + ) + props.type = .all + + do { + _ = try await store.requestPurchase(props) + XCTFail("Expected mutated query-only type to be rejected") + } catch let error as PurchaseError { + XCTAssertEqual(error.code, .developerError) + } catch { + XCTFail("Unexpected error: \(error)") + } + XCTAssertNil(module.lastRequestPurchaseProps) + } + func testStoreSkuResolverUsesCanonicalAppleAcrossUnionBranches() { let purchase = RequestPurchaseProps( request: .purchase(RequestPurchasePropsByPlatforms( diff --git a/packages/docs/public/showcase/record-scanner.webp b/packages/docs/public/showcase/record-scanner.webp new file mode 100644 index 000000000..26d1734b2 Binary files /dev/null and b/packages/docs/public/showcase/record-scanner.webp differ 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 } ] diff --git a/packages/docs/src/pages/docs/kit-compatibility.tsx b/packages/docs/src/pages/docs/kit-compatibility.tsx index 2bbaaee98..f38ca74c1 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 @@ -179,8 +181,8 @@ X-OpenIAP-Spec: 3.2.0`} for a legitimate reason.
  • - 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 2dbdca7fe..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 -> throw IllegalArgumentException("Unknown ErrorCode value: $value") + 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 -> throw IllegalArgumentException("Unknown IapkitPurchaseState value: $value") + 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 -> throw IllegalArgumentException("Unknown IapStore value: $value") + 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 -> throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid value: $value") + 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 -> throw IllegalArgumentException("Unknown PaymentMode value: $value") + 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 -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value") + 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 -> throw IllegalArgumentException("Unknown PurchaseState value: $value") + 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 -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value") + 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 -> throw IllegalArgumentException("Unknown SubscriptionPeriodUnit value: $value") + 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 -> throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid value: $value") + 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 @@ -1480,7 +1645,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, @@ -1720,7 +1885,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 +1923,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 +2090,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 +2275,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 +2443,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"), ) } } @@ -2314,7 +2479,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, ) @@ -2350,7 +2515,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,9 +2760,9 @@ 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 { 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, @@ -2671,7 +2836,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 +2900,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 { 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, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, ) } } @@ -2833,8 +2998,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"), ) } } @@ -2940,11 +3105,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 +3186,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 +3308,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 +3330,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 +3340,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 +3456,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 +3561,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 +3711,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,14 +3869,14 @@ 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, 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"), ) } } @@ -3757,7 +3922,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 +3970,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") }, @@ -4161,7 +4326,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"), ) } } @@ -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) } ?: BillingProgramAndroid.Unspecified - 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 { runCatching { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } }.getOrNull() ?: 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) } ?: BillingProgramAndroid.Unspecified - 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 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") }, ) } } @@ -4668,13 +4832,12 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - 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 developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } + 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,12 +4891,12 @@ 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"] 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 +5002,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,16 +5063,15 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - 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 subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } - if (skus == null) return null + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } + 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") } return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, isOfferPersonalized = isOfferPersonalized, @@ -4982,16 +5144,16 @@ 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 { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown } - val compactJWS = json["compactJWS"] as? String - val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } - 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 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"]?.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"] 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 +5206,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") }, ) } } @@ -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, @@ -5219,12 +5381,12 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( - amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, - apiKey = json["apiKey"] as? String, - apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, - baseUrl = json["baseUrl"] as? String, - google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, - includeClientPayload = json["includeClientPayload"] as? Boolean, + amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, + 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"]?.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"]?.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 = runCatching { (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } }.getOrNull() ?: 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 @@ -5421,9 +5583,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 +5603,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.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/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/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 861f95df4..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 @@ -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,54 @@ 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) { + assertThrows(IllegalArgumentException::class.java) { + 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/base-plugin.ts b/packages/gql/codegen/plugins/base-plugin.ts index 3fecfbc93..b21570645 100644 --- a/packages/gql/codegen/plugins/base-plugin.ts +++ b/packages/gql/codegen/plugins/base-plugin.ts @@ -239,6 +239,53 @@ 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, 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 === '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; + if (!schema.objects.some((container) => container.name === typeName)) return false; + return schema.objects.some((container) => + container.fields.some( + (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), + ), + ); + } + /** * 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..a4142c179 100644 --- a/packages/gql/codegen/plugins/csharp.ts +++ b/packages/gql/codegen/plugins/csharp.ts @@ -247,6 +247,118 @@ 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(' 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}");'); + 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(' 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}");'); + 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 +406,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(' {'); @@ -319,7 +431,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 +446,15 @@ 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(` 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(''); @@ -467,6 +590,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[] { @@ -502,7 +626,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, ' '); @@ -512,6 +636,31 @@ export class CSharpPlugin extends CodegenPlugin { const jsonName = field.name; const overrideModifier = inheritedFields.has(field.name) ? 'override ' : ''; this.emit(` [JsonPropertyName("${jsonName}")]`); + 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) + ) { + 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# // `required` modifier so callers must initialize them; nullable @@ -615,7 +764,86 @@ 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); + } + + 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(''); + + 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(''); } @@ -657,6 +885,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 3f8227a4c..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,7 +184,24 @@ export class DartPlugin extends CodegenPlugin { } this.emit(' }'); - this.emit(` throw ArgumentError('Unknown ${irEnum.name} value: \$value');`); + 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))};`); + } else { + this.emit(` throw ArgumentError('Unknown ${irEnum.name} value: \$value');`); + } + 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;'); @@ -295,6 +312,7 @@ export class DartPlugin extends CodegenPlugin { } this.emit(' );'); this.emit(' }'); + this.emitTolerantFromJsonHelper(irObject.name); // toJson method this.emit(''); @@ -393,11 +411,17 @@ 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(' );'); this.emit(' }'); + this.emitTolerantFromJsonHelper(irInput.name); // toJson method this.emit(''); @@ -772,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 ? '!' : ''}`)}`; @@ -815,15 +844,19 @@ 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)) { + if (type.nullable && this.typeNeedsTolerantNullableDecoder(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 +865,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..7beb20402 100644 --- a/packages/gql/codegen/plugins/gdscript.ts +++ b/packages/gql/codegen/plugins/gdscript.ts @@ -41,7 +41,10 @@ export class GDScriptPlugin extends CodegenPlugin { mapType(type: IRType): string { if (type.kind === 'list') { - const elementType = this.mapType(type.elementType!); + const element = type.elementType!; + 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') { @@ -86,9 +89,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 @@ -316,27 +319,67 @@ 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; } - private emitEnumFromDictAssignment(indent: string, target: string, typeName: string, sourceExpression: string): void { + 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, + 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}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:`); + 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) { + 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:`); + 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 { @@ -402,7 +445,12 @@ export class GDScriptPlugin extends CodegenPlugin { // from_dict method this.emit(''); - this.emit(`\tstatic func from_dict(data: Dictionary) -> ${irObject.name}:`); + 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) { const fieldName = this.getGdscriptFieldName(field.name, irObject.name); @@ -410,6 +458,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:'); @@ -433,11 +487,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}"])`); - 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 { @@ -445,61 +495,203 @@ 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 && (elementType.kind === 'scalar' || 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 (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) { + 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:`); - this.emit(`${itemIndent}\tarr.append(${enumReverseLookup}.get(item, ${fallback}))`); + 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 (isRequestContext) { + this.emitListDecodeFailure(itemIndent, elementTypeName, graphqlName, reportErrorsExpression); + } else { + 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 { + 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( + 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 { @@ -597,6 +789,7 @@ export class GDScriptPlugin extends CodegenPlugin { } } + const responseDerivedInput = irInput.customTypeKind === 'PurchaseInput'; this.generateDocComment(irInput.description); this.emit(`class ${irInput.name}:`); @@ -624,11 +817,23 @@ export class GDScriptPlugin extends CodegenPlugin { // from_dict method this.emit(''); - this.emit(`\tstatic func from_dict(data: Dictionary) -> ${irInput.name}:`); + 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'); @@ -646,6 +851,96 @@ 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; + 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) { + 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 ${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 @@ -687,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:'); @@ -734,15 +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); + this.generateListFromDictAssignment(type, graphqlName, fieldName, '\t\t\t', null, 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}"])`); - 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}"]`); + this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`, true); } else { this.emit(`\t\t\tobj.${fieldName} = data["${graphqlName}"]`); } @@ -828,9 +1125,9 @@ 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}"]`); + 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 1d1d94454..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,8 +227,17 @@ export class KotlinPlugin extends CodegenPlugin { } } - this.emit(` else -> throw IllegalArgumentException("Unknown ${irEnum.name} value: $value")`); + 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(` 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'); @@ -326,9 +335,18 @@ export class KotlinPlugin extends CodegenPlugin { this.emit(` fun fromJson(json: Map): ${irObject.name} {`); this.emit(` return ${irObject.name}(`); + const rejectMissingStrictEnums = this.typeHasRequiredEnumWithoutUnknown(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, + this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, + ); this.emit(` ${propertyName} = ${expression},`); } @@ -423,8 +441,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.typeHasRequiredEnumWithoutUnknown(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, + this.buildDefaultValueExpression(value), + rejectMissingStrictEnums, + ); this.emit(` ${value.name} = ${expression},`); } this.emit(' )'); @@ -485,6 +511,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.typeHasRequiredEnumWithoutUnknown(irInput.name, this.schema); this.generateDocComment(irInput.description); this.generateDeprecationAnnotation(irInput.description); @@ -518,12 +545,16 @@ export class KotlinPlugin extends CodegenPlugin { false, 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`); @@ -548,6 +579,8 @@ export class KotlinPlugin extends CodegenPlugin { false, false, this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, + true, ); this.emit(` ${propertyName} = ${expression},`); } @@ -647,6 +680,8 @@ export class KotlinPlugin extends CodegenPlugin { false, true, this.buildDefaultValueExpression(value), + false, + true, ); this.emit(` val ${value.name} = ${expression}`); } @@ -665,6 +700,8 @@ export class KotlinPlugin extends CodegenPlugin { false, false, this.buildDefaultValueExpression(value), + false, + true, ); this.emit(` ${value.name} = ${expression},`); } @@ -703,6 +740,7 @@ export class KotlinPlugin extends CodegenPlugin { } private generateStandardInput(irInput: IRInput): void { + const rejectMissingStrictEnums = this.typeHasRequiredEnumWithoutUnknown(irInput.name, this.schema); this.generateDocComment(irInput.description); this.generateDeprecationAnnotation(irInput.description); this.emit(`public data class ${irInput.name}(`); @@ -735,6 +773,8 @@ export class KotlinPlugin extends CodegenPlugin { false, true, this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, + true, ); this.emit(` val ${propertyName} = ${expression}`); } @@ -765,6 +805,8 @@ export class KotlinPlugin extends CodegenPlugin { false, false, this.buildDefaultValueExpression(field), + rejectMissingStrictEnums, + true, ); this.emit(` ${propertyName} = ${expression},`); } @@ -1039,33 +1081,77 @@ export class KotlinPlugin extends CodegenPlugin { isListElement: boolean = false, forNullableFromJson: boolean = false, defaultExpression?: string | null, + rejectMissingStrictEnums: boolean = false, + isInputContext: boolean = false, ): string { if (type.kind === 'list') { - const element = this.buildFromJsonExpression(type.elementType!, 'it', true, forNullableFromJson); - const mapFn = type.elementType!.nullable ? 'map' : 'mapNotNull'; + const element = this.buildFromJsonExpression( + type.elementType!, + 'it', + true, + forNullableFromJson, + undefined, + rejectMissingStrictEnums, + isInputContext, + ); + 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}`; } @@ -1073,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}`; } @@ -1082,15 +1171,26 @@ 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}`; } 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 +1202,26 @@ 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 (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)) { + 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 +1291,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/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 new file mode 100644 index 000000000..7b46c93fc --- /dev/null +++ b/packages/gql/src/enum-decoder-compatibility.test.ts @@ -0,0 +1,417 @@ +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: [] }, + ], +}; + +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, platform], + 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 }), + 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('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', + 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, + }, + }), + field('statuses', { + kind: 'list', + nullable: true, + elementType: { + kind: 'enum', + name: 'TolerantStatus', + nullable: false, + }, + }), + ], + hasRequiredFields: false, + isCustomType: false, + }, + ], + unions: [], + 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 }, + }, + ], + }, + ], + }; +} + +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('fun fromJson(value: String): TolerantStatus = fromKnownJson(value) ?: TolerantStatus.Unknown'); + expect(output).not.toContain('Unknown TolerantStatus 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")', + ); + expect(output).toContain('payload = (json["payload"] as? Map)?.let { runCatching { Payload.fromJson(it) }.getOrNull() }'); + expect(output).toContain( + '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('(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'); + }); + + 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('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( + "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', () => { + 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('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('public sealed class StrictEnumJsonConverter'); + expect(output.match(/reader\.TokenType != JsonTokenType\.String/g)).toHaveLength(2); + 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)'); + }); + + it('degrades neutral GDScript enums and drops an unreadable optional payload', () => { + const output = new GDScriptPlugin({ outputPath: 'types.gd' }).generate(decoderSchema()); + + 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")'); + expect(output).toContain('\t\t\treturn null'); + expect(output).toContain( + '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( + '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")'); + 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:')); + 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..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}:`); @@ -49,7 +53,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] = []'); }); @@ -62,71 +69,31 @@ 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, - }, - ], - 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):'); 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] = []'); + 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 0c6d8a4c1..726a73d14 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -13,6 +13,118 @@ 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) + { + 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}"); + } + + 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) + { + 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}"); + } + + 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 +144,7 @@ public enum BillingChoiceImageLayoutAndroid RectangularTwoByTwo } -public sealed class BillingChoiceImageLayoutAndroidJsonConverter : JsonConverter +public sealed class BillingChoiceImageLayoutAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -66,6 +178,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 +206,7 @@ public enum BillingChoiceScreenTypeAndroid GoogleRendered } -public sealed class BillingChoiceScreenTypeAndroidJsonConverter : JsonConverter +public sealed class BillingChoiceScreenTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -123,6 +240,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 +298,7 @@ public enum BillingProgramAndroid BillingChoice } -public sealed class BillingProgramAndroidJsonConverter : JsonConverter +public sealed class BillingProgramAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -219,6 +341,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 +376,7 @@ public enum DeveloperBillingLaunchModeAndroid CallerWillLaunchLink } -public sealed class DeveloperBillingLaunchModeAndroidJsonConverter : JsonConverter +public sealed class DeveloperBillingLaunchModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -283,6 +410,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 +438,7 @@ public enum DeveloperBillingTypeAndroid ExternalLink } -public sealed class DeveloperBillingTypeAndroidJsonConverter : JsonConverter +public sealed class DeveloperBillingTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -340,6 +472,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 +500,7 @@ public enum DiscountOfferType OneTime } -public sealed class DiscountOfferTypeJsonConverter : JsonConverter +public sealed class DiscountOfferTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -400,6 +537,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 +591,7 @@ public enum ErrorCode DuplicatePurchase } -public sealed class ErrorCodeJsonConverter : JsonConverter +public sealed class ErrorCodeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -607,7 +749,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 +759,12 @@ 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 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 +790,7 @@ public enum ExternalLinkLaunchModeAndroid CallerWillLaunchLink } -public sealed class ExternalLinkLaunchModeAndroidJsonConverter : JsonConverter +public sealed class ExternalLinkLaunchModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -677,6 +824,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 +853,7 @@ public enum ExternalLinkTypeAndroid LinkToAppDownload } -public sealed class ExternalLinkTypeAndroidJsonConverter : JsonConverter +public sealed class ExternalLinkTypeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -735,6 +887,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 +915,7 @@ public enum ExternalPurchaseCustomLinkNoticeTypeIOS Browser } -public sealed class ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter : JsonConverter +public sealed class ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -787,6 +944,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 +977,7 @@ public enum ExternalPurchaseCustomLinkTokenTypeIOS Services } -public sealed class ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter : JsonConverter +public sealed class ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -848,6 +1010,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 +1033,7 @@ public enum ExternalPurchaseNoticeAction Dismissed } -public sealed class ExternalPurchaseNoticeActionJsonConverter : JsonConverter +public sealed class ExternalPurchaseNoticeActionJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -899,6 +1066,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 +1103,7 @@ public enum IapEvent SubscriptionBillingIssue } -public sealed class IapEventJsonConverter : JsonConverter +public sealed class IapEventJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -980,6 +1152,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 +1174,7 @@ public enum IapkitClientPayloadFormat Text } -public sealed class IapkitClientPayloadFormatJsonConverter : JsonConverter +public sealed class IapkitClientPayloadFormatJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1034,6 +1211,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 +1248,7 @@ public enum IapkitPurchaseState Inauthentic } -public sealed class IapkitPurchaseStateJsonConverter : JsonConverter +public sealed class IapkitPurchaseStateJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1107,7 +1289,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 +1299,12 @@ 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 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 +1320,7 @@ public enum IapPlatform Android } -public sealed class IapPlatformJsonConverter : JsonConverter +public sealed class IapPlatformJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1165,6 +1352,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 +1375,7 @@ public enum IapStore Amazon } -public sealed class IapStoreJsonConverter : JsonConverter +public sealed class IapStoreJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1217,7 +1409,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 +1419,12 @@ 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 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 +1447,7 @@ public enum InAppMessageCategoryAndroid Transactional } -public sealed class InAppMessageCategoryAndroidJsonConverter : JsonConverter +public sealed class InAppMessageCategoryAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1270,7 +1467,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 +1477,12 @@ 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 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 +1505,7 @@ public enum InAppMessageResponseCodeAndroid SubscriptionStatusUpdated } -public sealed class InAppMessageResponseCodeAndroidJsonConverter : JsonConverter +public sealed class InAppMessageResponseCodeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1334,6 +1536,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 +1566,7 @@ public enum PaymentMode Unknown } -public sealed class PaymentModeJsonConverter : JsonConverter +public sealed class PaymentModeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1389,7 +1596,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 +1606,12 @@ 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 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 +1629,7 @@ public enum PaymentModeIOS PayUpFront } -public sealed class PaymentModeIOSJsonConverter : JsonConverter +public sealed class PaymentModeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1458,6 +1670,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 +1691,7 @@ public enum ProductQueryType All } -public sealed class ProductQueryTypeJsonConverter : JsonConverter +public sealed class ProductQueryTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1511,6 +1728,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 +1760,7 @@ public enum ProductStatusAndroid Unknown } -public sealed class ProductStatusAndroidJsonConverter : JsonConverter +public sealed class ProductStatusAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1564,7 +1786,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 +1796,12 @@ 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 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 +1817,7 @@ public enum ProductType Subs } -public sealed class ProductTypeJsonConverter : JsonConverter +public sealed class ProductTypeJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1623,6 +1850,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 +1876,7 @@ public enum ProductTypeIOS SubscriptionSuite } -public sealed class ProductTypeIOSJsonConverter : JsonConverter +public sealed class ProductTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1693,6 +1925,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 +1946,7 @@ public enum PurchaseState Unknown } -public sealed class PurchaseStateJsonConverter : JsonConverter +public sealed class PurchaseStateJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1735,7 +1972,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 +1982,12 @@ 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 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 +2002,7 @@ public enum PurchaseVerificationProvider Iapkit } -public sealed class PurchaseVerificationProviderJsonConverter : JsonConverter +public sealed class PurchaseVerificationProviderJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1789,6 +2031,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 +2059,7 @@ public enum SubResponseCodeAndroid UserIneligible } -public sealed class SubResponseCodeAndroidJsonConverter : JsonConverter +public sealed class SubResponseCodeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1846,6 +2093,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 +2117,7 @@ public enum SubscriptionBillingPlanTypeIOS UpFront } -public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter +public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1891,7 +2143,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 +2153,12 @@ 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 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 +2179,7 @@ public enum SubscriptionOfferTypeIOS WinBack } -public sealed class SubscriptionOfferTypeIOSJsonConverter : JsonConverter +public sealed class SubscriptionOfferTypeIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -1959,6 +2216,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 +2239,7 @@ public enum SubscriptionPeriodIOS Empty } -public sealed class SubscriptionPeriodIOSJsonConverter : JsonConverter +public sealed class SubscriptionPeriodIOSJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2022,6 +2284,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 +2308,7 @@ public enum SubscriptionPeriodUnit Unknown } -public sealed class SubscriptionPeriodUnitJsonConverter : JsonConverter +public sealed class SubscriptionPeriodUnitJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2075,7 +2342,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 +2352,12 @@ 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 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 +2390,7 @@ public enum SubscriptionReplacementModeAndroid KeepExisting } -public sealed class SubscriptionReplacementModeAndroidJsonConverter : JsonConverter +public sealed class SubscriptionReplacementModeAndroidJsonConverter : JsonConverter, IOpenIapEnumJsonConverter { private static readonly Dictionary _fromString = new() { @@ -2153,7 +2425,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 +2435,12 @@ 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 bool TryReadRaw(string value, out SubscriptionReplacementModeAndroid result) => + _fromString.TryGetValue(value, out result); + + public string WriteRaw(SubscriptionReplacementModeAndroid value) => _toString[value]; } public static class SubscriptionReplacementModeAndroidExtensions @@ -2374,6 +2651,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 +3117,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 +3816,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 +3981,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")] @@ -3888,6 +4213,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")] @@ -3915,6 +4241,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. @@ -3927,6 +4254,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 @@ -3963,9 +4291,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")] @@ -3981,6 +4311,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 }; } @@ -3998,6 +4329,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 @@ -4009,6 +4341,7 @@ public sealed record InitConnectionConfig /// Defaults to GOOGLE_RENDERED. /// [JsonPropertyName("billingChoiceScreenTypeAndroid")] + [JsonConverter(typeof(StrictNullableEnumJsonConverter))] public BillingChoiceScreenTypeAndroid? BillingChoiceScreenTypeAndroid { get; init; } = global::OpenIap.BillingChoiceScreenTypeAndroid.GoogleRendered; } @@ -4022,12 +4355,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")] @@ -4046,6 +4382,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; } @@ -4177,6 +4514,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() @@ -4297,6 +4635,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 @@ -4419,6 +4758,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; } } @@ -4510,6 +4850,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 5ed78d22d..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 -> throw IllegalArgumentException("Unknown ErrorCode value: $value") + 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 -> throw IllegalArgumentException("Unknown IapkitPurchaseState value: $value") + 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 -> throw IllegalArgumentException("Unknown IapStore value: $value") + 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 -> throw IllegalArgumentException("Unknown InAppMessageCategoryAndroid value: $value") + 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 -> throw IllegalArgumentException("Unknown PaymentMode value: $value") + 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 -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value") + 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 -> throw IllegalArgumentException("Unknown PurchaseState value: $value") + 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 -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value") + 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 -> throw IllegalArgumentException("Unknown SubscriptionPeriodUnit value: $value") + 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 -> throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid value: $value") + 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 @@ -1426,7 +1591,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, @@ -1666,7 +1831,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 +1869,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 +2036,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 +2221,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 +2389,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"), ) } } @@ -2260,7 +2425,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, ) @@ -2296,7 +2461,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,9 +2706,9 @@ 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 { 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, @@ -2617,7 +2782,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 +2846,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 { 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, + type = (json["type"] as? String)?.let { ProductType.fromJson(it) } ?: ProductType.Subs, ) } } @@ -2779,8 +2944,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"), ) } } @@ -2886,11 +3051,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 +3132,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 +3254,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 +3276,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 +3286,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 +3402,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 +3507,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 +3657,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,14 +3815,14 @@ 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, 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"), ) } } @@ -3703,7 +3868,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 +3916,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") }, @@ -4107,7 +4272,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"), ) } } @@ -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) } ?: BillingProgramAndroid.Unspecified - 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 { runCatching { (it as? String)?.let { InAppMessageCategoryAndroid.fromJson(it) } }.getOrNull() ?: 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) } ?: BillingProgramAndroid.Unspecified - 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 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") }, ) } } @@ -4614,13 +4778,12 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - 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 developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } + 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,12 +4837,12 @@ 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"] 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 +4948,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,16 +5009,15 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - 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 subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } - if (skus == null) return null + val developerBillingOption = json["developerBillingOption"]?.let { value -> (value as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for DeveloperBillingOptionParamsAndroid") } + 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") } return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, isOfferPersonalized = isOfferPersonalized, @@ -4928,16 +5090,16 @@ 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 { runCatching { SubscriptionBillingPlanTypeIOS.fromJson(it) }.getOrNull() ?: SubscriptionBillingPlanTypeIOS.Unknown } - val compactJWS = json["compactJWS"] as? String - val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) } - 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 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"]?.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"] 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 +5152,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") }, ) } } @@ -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, @@ -5165,12 +5327,12 @@ public data class RequestVerifyPurchaseWithIapkitProps( companion object { fun fromJson(json: Map): RequestVerifyPurchaseWithIapkitProps { return RequestVerifyPurchaseWithIapkitProps( - amazon = (json["amazon"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) }, - apiKey = json["apiKey"] as? String, - apple = (json["apple"] as? Map)?.let { RequestVerifyPurchaseWithIapkitAppleProps.fromJson(it) }, - baseUrl = json["baseUrl"] as? String, - google = (json["google"] as? Map)?.let { RequestVerifyPurchaseWithIapkitGoogleProps.fromJson(it) }, - includeClientPayload = json["includeClientPayload"] as? Boolean, + amazon = json["amazon"]?.let { value -> (value as? Map)?.let { RequestVerifyPurchaseWithIapkitAmazonProps.fromJson(it) } ?: throw IllegalArgumentException("Invalid input object for RequestVerifyPurchaseWithIapkitAmazonProps") }, + 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"]?.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"]?.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 = runCatching { (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } }.getOrNull() ?: 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 @@ -5367,9 +5529,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 +5549,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.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 13e69dde8..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,7 +401,19 @@ enum ErrorCode { case 'duplicate-purchase': return ErrorCode.DuplicatePurchase; } - throw ArgumentError('Unknown ErrorCode value: $value'); + 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,7 +752,19 @@ enum IapkitPurchaseState { case 'inauthentic': return IapkitPurchaseState.Inauthentic; } - throw ArgumentError('Unknown IapkitPurchaseState value: $value'); + 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,7 +827,19 @@ enum IapStore { case 'amazon': return IapStore.Amazon; } - throw ArgumentError('Unknown IapStore value: $value'); + 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,7 +865,19 @@ enum InAppMessageCategoryAndroid { case 'transactional': return InAppMessageCategoryAndroid.Transactional; } - throw ArgumentError('Unknown InAppMessageCategoryAndroid value: $value'); + 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,7 +948,19 @@ enum PaymentMode { case 'unknown': return PaymentMode.Unknown; } - throw ArgumentError('Unknown PaymentMode value: $value'); + 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,7 +1070,19 @@ enum ProductStatusAndroid { case 'unknown': return ProductStatusAndroid.Unknown; } - throw ArgumentError('Unknown ProductStatusAndroid value: $value'); + 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,7 +1186,19 @@ enum PurchaseState { case 'unknown': return PurchaseState.Unknown; } - throw ArgumentError('Unknown PurchaseState value: $value'); + 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,7 +1296,19 @@ enum SubscriptionBillingPlanTypeIOS { case 'up-front': return SubscriptionBillingPlanTypeIOS.UpFront; } - throw ArgumentError('Unknown SubscriptionBillingPlanTypeIOS value: $value'); + 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,7 +1419,19 @@ enum SubscriptionPeriodUnit { case 'unknown': return SubscriptionPeriodUnit.Unknown; } - throw ArgumentError('Unknown SubscriptionPeriodUnit value: $value'); + 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,7 +1477,19 @@ enum SubscriptionReplacementModeAndroid { case 'keep-existing': return SubscriptionReplacementModeAndroid.KeepExisting; } - throw ArgumentError('Unknown SubscriptionReplacementModeAndroid value: $value'); + 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; @@ -1264,7 +1660,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 +2488,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 +3780,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 +4006,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', @@ -4053,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, ); } @@ -4119,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?, ); } @@ -4195,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?, ); } @@ -4223,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(), ); } @@ -4261,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, ); } @@ -4302,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, ); } @@ -4333,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, ); } @@ -4745,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?, @@ -4957,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), ); } @@ -5124,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 097d3c06d..482a3baa6 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -466,12 +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"]) - else: - obj.period = 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: @@ -561,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"] @@ -714,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"] @@ -722,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: @@ -757,12 +769,18 @@ 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"] 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: @@ -797,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 @@ -871,7 +891,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"] @@ -884,9 +904,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 @@ -919,7 +946,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"] @@ -927,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: @@ -1028,7 +1061,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"] @@ -1042,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: @@ -1225,12 +1264,18 @@ 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"] 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: @@ -1258,12 +1303,18 @@ class IapkitProductClientPayload: var version: float = 0.0 var updated_at: float = 0.0 - static func from_dict(data: Dictionary) -> IapkitProductClientPayload: + 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: 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: @@ -1274,6 +1325,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,12 +1346,18 @@ 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"] 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: @@ -1450,6 +1510,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 +1549,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"] @@ -1498,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: @@ -1514,33 +1587,51 @@ 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) - else: + 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 if data.has("discountOffers") and data["discountOffers"] != null: if data["discountOffers"] is Array: 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 @@ -1613,7 +1704,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"] @@ -1625,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: @@ -1641,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: @@ -1653,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: @@ -1660,18 +1769,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 @@ -1742,7 +1865,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"] @@ -1754,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: @@ -1770,24 +1903,35 @@ 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) - else: + 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 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 @@ -1855,13 +1999,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"] @@ -1873,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: @@ -1889,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: @@ -1901,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: @@ -1908,18 +2074,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: @@ -1929,6 +2109,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"] @@ -1940,24 +2123,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: @@ -2053,9 +2248,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 @@ -2096,18 +2291,22 @@ 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) - else: + 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 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: + 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 if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2192,7 +2391,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 @@ -2206,10 +2405,12 @@ 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) - else: + 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 if data.has("message") and data["message"] != null: obj.message = data["message"] if data.has("productId") and data["productId"] != null: @@ -2222,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: @@ -2270,9 +2473,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 = "" @@ -2338,18 +2541,22 @@ 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) - else: + 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 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: + 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 if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] if data.has("currentPlanId") and data["currentPlanId"] != null: @@ -2410,10 +2617,12 @@ 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) - else: + 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 if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null: if data["commitmentInfoIOS"] is Dictionary: obj.commitment_info_ios = TransactionCommitmentInfoIOS.from_dict(data["commitmentInfoIOS"]) @@ -2576,7 +2785,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 @@ -2588,10 +2797,12 @@ 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) - else: + 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 if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null: obj.commitment_renewal_date = data["commitmentRenewalDate"] if data.has("commitmentRenewalPrice") and data["commitmentRenewalPrice"] != null: @@ -2670,10 +2881,12 @@ 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) - else: + 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 if data.has("commitmentInfo") and data["commitmentInfo"] != null: if data["commitmentInfo"] is Dictionary: obj.commitment_info = RenewalCommitmentInfoIOS.from_dict(data["commitmentInfo"]) @@ -2754,13 +2967,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. @@ -2770,27 +2983,29 @@ 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) - else: + 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 if data.has("environment") and data["environment"] != null: obj.environment = data["environment"] if data.has("isValid") and data["isValid"] != null: 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) - else: + 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 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"] + obj.client_payload = IapkitProductClientPayload.from_dict_or_null(data["clientPayload"]) return obj func to_dict() -> Dictionary: @@ -2819,15 +3034,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 @@ -2883,7 +3109,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"] @@ -2897,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: @@ -2908,10 +3140,12 @@ 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) - else: + 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 if data.has("keyIdentifierIOS") and data["keyIdentifierIOS"] != null: obj.key_identifier_ios = data["keyIdentifierIOS"] if data.has("nonceIOS") and data["nonceIOS"] != null: @@ -2999,7 +3233,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 @@ -3007,10 +3241,12 @@ 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) - else: + 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 if data.has("value") and data["value"] != null: obj.value = data["value"] return obj @@ -3025,21 +3261,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: + 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): @@ -3052,41 +3299,72 @@ 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: - obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + 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 if data.has("billingPrice") and data["billingPrice"] != null: 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 @@ -3179,7 +3457,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"] @@ -3197,9 +3475,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 @@ -3416,12 +3701,18 @@ 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"] 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: @@ -3437,6 +3728,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 @@ -3487,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"] @@ -3509,14 +3809,24 @@ 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 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") + return null var obj = BillingProgramInformationDialogParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: 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 @@ -3539,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"] @@ -3565,22 +3881,38 @@ 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: + 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") + return null var obj = DeveloperBillingOptionParamsAndroid.new() if data.has("billingProgram") and data["billingProgram"] != null: 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 @@ -3616,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"] @@ -3652,20 +3999,37 @@ 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("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") + 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"] 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 @@ -3699,9 +4063,15 @@ class InAppMessageParamsAndroid: 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]) - elif item is int: + elif item is int and IN_APP_MESSAGE_CATEGORY_ANDROID_VALUES.has(item): arr.append(item) + 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: @@ -3729,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: @@ -3766,26 +4142,53 @@ 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("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") + 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"] 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: @@ -3820,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: @@ -3827,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: @@ -3855,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"] @@ -3880,7 +4301,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: @@ -3902,18 +4323,22 @@ 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) - else: + 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 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: + 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 if data.has("isAutoRenewing") and data["isAutoRenewing"] != null: obj.is_auto_renewing = data["isAutoRenewing"] return obj @@ -3955,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"] @@ -3979,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"] @@ -4005,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: @@ -4012,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: @@ -4023,9 +4481,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"]) - else: - obj.developer_billing_option = 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: @@ -4062,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"] @@ -4073,9 +4551,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 @@ -4128,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: @@ -4171,14 +4664,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: @@ -4216,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: @@ -4223,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: @@ -4239,20 +4766,39 @@ 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: - 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"]) - else: - obj.developer_billing_option = 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: @@ -4308,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"] @@ -4319,25 +4883,43 @@ 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: - obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN) - else: + 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: + 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: @@ -4391,14 +4973,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: @@ -4426,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"] @@ -4454,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"] @@ -4470,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"] @@ -4497,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"] @@ -4506,19 +5125,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: @@ -4551,9 +5185,15 @@ 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: + 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"] @@ -4561,8 +5201,11 @@ class SubscriptionProductReplacementParamsAndroid: 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: + elif enum_str is int and SUBSCRIPTION_REPLACEMENT_MODE_ANDROID_VALUES.has(enum_str): obj.replacement_mode = enum_str + else: + push_error("Invalid SubscriptionReplacementModeAndroid input value") + return null return obj func to_dict() -> Dictionary: @@ -4582,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"] @@ -4607,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"] @@ -4644,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"] @@ -4676,19 +5346,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: @@ -4714,19 +5399,31 @@ 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"] 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: - 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: @@ -4749,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"] @@ -5335,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: @@ -5361,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: @@ -5422,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: @@ -5870,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: @@ -5937,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: @@ -5965,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 80814867d..d815ce406 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 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 e506b5acf..e252aa3f0 100644 --- a/packages/kit/src/index.css +++ b/packages/kit/src/index.css @@ -585,6 +585,32 @@ 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; + } + + .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 && (