diff --git a/openiap-versions.json b/openiap-versions.json index 9ee47441..6913a182 100644 --- a/openiap-versions.json +++ b/openiap-versions.json @@ -1,5 +1,5 @@ { - "gql": "1.2.5", + "gql": "1.2.4", "docs": "1.2.5", "google": "1.3.5", "apple": "1.2.32" diff --git a/packages/apple/Sources/Models/OpenIapSerialization.swift b/packages/apple/Sources/Models/OpenIapSerialization.swift index 7ea6b640..534cebc1 100644 --- a/packages/apple/Sources/Models/OpenIapSerialization.swift +++ b/packages/apple/Sources/Models/OpenIapSerialization.swift @@ -213,8 +213,7 @@ public enum OpenIapSerialization { return value } let iosSubscriptions = allItems.compactMap { item -> ProductSubscriptionIOS? in - guard case .subscription(let subscription) = item, - case .productSubscriptionIos(let value) = subscription + guard case .productSubscription(.productSubscriptionIos(let value)) = item else { return nil } return value } diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 153275ea..a3446a45 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -245,43 +245,10 @@ public struct ExternalPurchaseNoticeResultIOS: Codable { public var result: ExternalPurchaseNoticeAction } - -// Union type for FetchProductsResult.all -public enum ProductOrSubscription: Codable { - case product(Product) - case subscription(ProductSubscription) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let product = try? container.decode(Product.self) { - self = .product(product) - return - } - if let subscription = try? container.decode(ProductSubscription.self) { - self = .subscription(subscription) - return - } - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Cannot decode ProductOrSubscription" - ) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .product(let product): - try container.encode(product) - case .subscription(let subscription): - try container.encode(subscription) - } - } -} - public enum FetchProductsResult { + case all([ProductOrSubscription]?) case products([Product]?) case subscriptions([ProductSubscription]?) - case all([ProductOrSubscription]?) } public struct PricingPhaseAndroid: Codable { @@ -1032,6 +999,11 @@ public enum Product: Codable, ProductCommon { } } +public enum ProductOrSubscription: Codable { + case product(Product) + case productSubscription(ProductSubscription) +} + public enum ProductSubscription: Codable, ProductCommon { case productSubscriptionAndroid(ProductSubscriptionAndroid) case productSubscriptionIos(ProductSubscriptionIOS) diff --git a/packages/apple/Sources/OpenIapModule+ObjC.swift b/packages/apple/Sources/OpenIapModule+ObjC.swift index 4288aa42..2aa5a11b 100644 --- a/packages/apple/Sources/OpenIapModule+ObjC.swift +++ b/packages/apple/Sources/OpenIapModule+ObjC.swift @@ -74,8 +74,7 @@ import StoreKit return value } let subscriptionIOS = allItems.compactMap { item -> ProductSubscriptionIOS? in - guard case .subscription(let subscription) = item, - case .productSubscriptionIos(let value) = subscription + guard case .productSubscription(.productSubscriptionIos(let value)) = item else { return nil } return value } diff --git a/packages/apple/Sources/OpenIapStore.swift b/packages/apple/Sources/OpenIapStore.swift index f710cd5e..7a9e4952 100644 --- a/packages/apple/Sources/OpenIapStore.swift +++ b/packages/apple/Sources/OpenIapStore.swift @@ -193,8 +193,8 @@ public final class OpenIapStore: ObservableObject { return nil } subscriptions = allItems.compactMap { item in - if case .subscription(let subscription) = item { - return subscription + if case .productSubscription(.productSubscriptionIos(let subscription)) = item { + return .productSubscriptionIos(subscription) } return nil } 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 66d93787..109c28a1 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 @@ -712,21 +712,14 @@ public data class ExternalPurchaseNoticeResultIOS( ) } - -// Union type for FetchProductsResult.all -public sealed interface ProductOrSubscription { - data class ProductItem(val value: Product) : ProductOrSubscription - data class SubscriptionItem(val value: ProductSubscription) : ProductOrSubscription -} - public sealed interface FetchProductsResult +public data class FetchProductsResultAll(val value: List?) : FetchProductsResult + public data class FetchProductsResultProducts(val value: List?) : FetchProductsResult public data class FetchProductsResultSubscriptions(val value: List?) : FetchProductsResult -public data class FetchProductsResultAll(val value: List?) : FetchProductsResult - public data class PricingPhaseAndroid( val billingCycleCount: Int, val billingPeriod: String, @@ -2170,6 +2163,30 @@ public sealed interface Product : ProductCommon { } } +public sealed interface ProductOrSubscription { + fun toJson(): Map + + companion object { + fun fromJson(json: Map): ProductOrSubscription { + return when (json["__typename"] as String?) { + "ProductAndroid" -> ProductItem(Product.fromJson(json)) + "ProductIOS" -> ProductItem(Product.fromJson(json)) + "ProductSubscriptionAndroid" -> ProductSubscriptionItem(ProductSubscription.fromJson(json)) + "ProductSubscriptionIOS" -> ProductSubscriptionItem(ProductSubscription.fromJson(json)) + else -> throw IllegalArgumentException("Unknown __typename for ProductOrSubscription: ${json["__typename"]}") + } + } + } + + data class ProductItem(val value: Product) : ProductOrSubscription { + override fun toJson() = value.toJson() + } + + data class ProductSubscriptionItem(val value: ProductSubscription) : ProductOrSubscription { + override fun toJson() = value.toJson() + } +} + public sealed interface ProductSubscription : ProductCommon { fun toJson(): Map diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt index 89311551..7632fe10 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt @@ -280,15 +280,19 @@ class OpenIapStore(private val module: OpenIapProtocol) { } is FetchProductsResultAll -> { // Handle the all case - merge both products and subscriptions - // The result.value is List? containing union of Product and ProductSubscription + // The result.value is List? containing union wrappers val items = result.value ?: emptyList() - // Extract products and subscriptions from ProductOrSubscription union + // Extract Android-specific products and subscriptions from wrapper classes val allProducts = items.mapNotNull { - (it as? ProductOrSubscription.ProductItem)?.value + (it as? ProductOrSubscription.ProductItem)?.value?.let { product -> + if (product is ProductAndroid) product else null + } } val allSubs = items.mapNotNull { - (it as? ProductOrSubscription.SubscriptionItem)?.value + (it as? ProductOrSubscription.ProductSubscriptionItem)?.value?.let { subscription -> + if (subscription is ProductSubscriptionAndroid) subscription else null + } } // Merge products diff --git a/packages/gql/scripts/fix-generated-types.mjs b/packages/gql/scripts/fix-generated-types.mjs index 6569824f..3d079bda 100644 --- a/packages/gql/scripts/fix-generated-types.mjs +++ b/packages/gql/scripts/fix-generated-types.mjs @@ -310,6 +310,18 @@ for (const file of schemaDefinitionFiles) { } } +// Extend FetchProductsResult to support mixed arrays for 'all' type +// MUST be done BEFORE interface parsing to ensure optionalUnionInterfaces map has the correct union +// The generated union `Product[] | ProductSubscription[] | null` doesn't support mixed arrays +// Add `(Product | ProductSubscription)[]` to the union to enable type narrowing +const fetchProductsResultPattern = /export type FetchProductsResult = Product\[\] \| ProductSubscription\[\] \| null;/; +if (fetchProductsResultPattern.test(content)) { + content = content.replace( + fetchProductsResultPattern, + 'export type FetchProductsResult = Product[] | ProductSubscription[] | (Product | ProductSubscription)[] | null;' + ); +} + const singleFieldInterfaceTypes = new Map(); const optionalUnionInterfaces = new Map(); const interfacePattern = /export interface (\w+) \{\n([\s\S]*?)\n\}\n/g; @@ -489,17 +501,6 @@ for (const [name, unionType] of optionalUnionInterfaces) { content = content.replace(pattern, `export type ${name} = ${unionType};\n\n`); } -// Extend FetchProductsResult to support mixed arrays for 'all' type -// The generated union `Product[] | ProductSubscription[] | null` doesn't support mixed arrays -// Add `(Product | ProductSubscription)[]` to the union to enable type narrowing -const fetchProductsResultPattern = /export type FetchProductsResult = Product\[\] \| ProductSubscription\[\] \| null;/; -if (fetchProductsResultPattern.test(content)) { - content = content.replace( - fetchProductsResultPattern, - 'export type FetchProductsResult = Product[] | ProductSubscription[] | (Product | ProductSubscription)[] | null;' - ); -} - const futureFields = new Set(); for (const file of schemaFiles) { let previousWasMarker = false; @@ -548,6 +549,14 @@ for (const [name, unionType] of optionalUnionInterfaces) { } } +// Fix Query interface to use FetchProductsResult type alias instead of inline union +// This ensures the Query['fetchProducts'] return type matches our implementation +// Must be done AFTER singleFieldInterfaceTypes replacement expands the type +content = content.replace( + /fetchProducts: Promise<\(Product\[\] \| ProductSubscription\[\] \| \(Product \| ProductSubscription\)\[\] \| null\)>/g, + 'fetchProducts: Promise' +); + content = content.replace(/^\s*_placeholder\??: [^;]+;\n/gm, ''); const ROOT_DEFINITIONS = ['Query', 'Mutation', 'Subscription']; diff --git a/packages/gql/scripts/generate-dart-types.mjs b/packages/gql/scripts/generate-dart-types.mjs index 70bfa0e3..dc7a1015 100644 --- a/packages/gql/scripts/generate-dart-types.mjs +++ b/packages/gql/scripts/generate-dart-types.mjs @@ -668,16 +668,28 @@ const printUnion = (unionType) => { let sharedInterfaceNames = []; if (memberTypes.length > 0) { const [firstMember, ...otherMembers] = memberTypes; - const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name)); - for (const member of otherMembers) { - const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name)); - for (const ifaceName of Array.from(firstInterfaces)) { - if (!memberInterfaces.has(ifaceName)) { - firstInterfaces.delete(ifaceName); + // Check if member is a union (unions don't have getInterfaces) + if (typeof firstMember.getInterfaces === 'function') { + const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name)); + let allMembersHaveInterfaces = true; + for (const member of otherMembers) { + if (typeof member.getInterfaces === 'function') { + const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name)); + for (const ifaceName of Array.from(firstInterfaces)) { + if (!memberInterfaces.has(ifaceName)) { + firstInterfaces.delete(ifaceName); + } + } + } else { + // Member is a union, so no shared interfaces + allMembersHaveInterfaces = false; + break; } } + if (allMembersHaveInterfaces) { + sharedInterfaceNames = Array.from(firstInterfaces).sort(); + } } - sharedInterfaceNames = Array.from(firstInterfaces).sort(); } const implementsClause = sharedInterfaceNames.length ? ` implements ${sharedInterfaceNames.join(', ')}` : ''; @@ -687,9 +699,54 @@ const printUnion = (unionType) => { lines.push(` factory ${unionType.name}.fromJson(Map json) {`); lines.push(` final typeName = json['__typename'] as String?;`); lines.push(' switch (typeName) {'); - members.forEach((member) => { - lines.push(` case '${member}':`, ` return ${member}.fromJson(json);`); + + // Flatten nested unions: if a member is itself a union, include its concrete members + const concreteMembers = new Set(); + for (const memberType of memberTypes) { + if (isUnionType(memberType)) { + // Member is a union, get its concrete members + const nestedMembers = memberType.getTypes(); + for (const nestedMember of nestedMembers) { + concreteMembers.add(nestedMember.name); + } + } else { + // Member is a concrete type + concreteMembers.add(memberType.name); + } + } + + // Track nested unions that need wrapper classes + const nestedUnions = new Set(); + + // Generate case for each concrete member, wrapping nested unions + const sortedConcreteMembers = Array.from(concreteMembers).sort(); + sortedConcreteMembers.forEach((concreteMember) => { + // Find which direct member this concrete type belongs to + let delegateTo = concreteMember; + let isNestedUnion = false; + + for (const memberType of memberTypes) { + if (isUnionType(memberType)) { + const nestedMembers = memberType.getTypes().map(t => t.name); + if (nestedMembers.includes(concreteMember)) { + delegateTo = memberType.name; + isNestedUnion = true; + nestedUnions.add(memberType.name); + break; + } + } + } + + if (isNestedUnion) { + // Wrap nested union in a typed wrapper class + const wrapperName = `${unionType.name}${delegateTo}`; + lines.push(` case '${concreteMember}':`, ` return ${wrapperName}(${delegateTo}.fromJson(json));`); + } else { + // Direct member, no wrapping needed + lines.push(` case '${concreteMember}':`, ` return ${delegateTo}.fromJson(json);`); + } }); + lines.push(' }'); lines.push(` throw ArgumentError('Unknown __typename for ${unionType.name}: $typeName');`); lines.push(' }', ''); @@ -722,6 +779,18 @@ const printUnion = (unionType) => { lines.push(' Map toJson();'); lines.push('}', ''); + + // Generate wrapper classes for nested unions + for (const nestedUnionName of Array.from(nestedUnions).sort()) { + const wrapperName = `${unionType.name}${nestedUnionName}`; + lines.push(`class ${wrapperName} extends ${unionType.name} {`); + lines.push(` const ${wrapperName}(this.value);`); + lines.push(` final ${nestedUnionName} value;`); + lines.push(''); + lines.push(' @override'); + lines.push(' Map toJson() => value.toJson();'); + lines.push('}', ''); + } }; const expandInputToParams = (inputTypeName) => { @@ -989,45 +1058,9 @@ for (const [typeName, literals] of Object.entries(productTypeMapping)) { } } -// Post-process: Add ProductOrSubscription union class -// This allows FetchProductsResult.all to contain heterogeneous lists -const productOrSubscriptionUnion = ` -// Union type for FetchProductsResult.all -abstract class ProductOrSubscription { - const ProductOrSubscription(); -} - -class ProductOrSubscriptionProduct extends ProductOrSubscription { - const ProductOrSubscriptionProduct(this.value); - final Product value; -} - -class ProductOrSubscriptionSubscription extends ProductOrSubscription { - const ProductOrSubscriptionSubscription(this.value); - final ProductSubscription value; -} -`; - +// All unions including nested ones are auto-generated with proper wrapper classes let output = lines.join('\n'); -// Insert ProductOrSubscription before FetchProductsResult -const fetchProductsResultAbstractPattern = /abstract class FetchProductsResult \{/; -if (fetchProductsResultAbstractPattern.test(output)) { - output = output.replace( - fetchProductsResultAbstractPattern, - productOrSubscriptionUnion + '\nabstract class FetchProductsResult {' - ); -} - -// Add the 'all' case to FetchProductsResult -const fetchProductsResultPattern = /(class FetchProductsResultSubscriptions extends FetchProductsResult \{\n const FetchProductsResultSubscriptions\(this\.value\);\n final List\? value;\n\})/; -if (fetchProductsResultPattern.test(output)) { - output = output.replace( - fetchProductsResultPattern, - '$1\n\nclass FetchProductsResultAll extends FetchProductsResult {\n const FetchProductsResultAll(this.value);\n final List? value;\n}' - ); -} - // Fix enum default values - Dart uses PascalCase for enum values output = output.replace(/IapPlatform\.ios/g, 'IapPlatform.IOS'); output = output.replace(/IapPlatform\.android/g, 'IapPlatform.Android'); diff --git a/packages/gql/scripts/generate-kotlin-types.mjs b/packages/gql/scripts/generate-kotlin-types.mjs index 7cd1be2a..3b5df2d3 100644 --- a/packages/gql/scripts/generate-kotlin-types.mjs +++ b/packages/gql/scripts/generate-kotlin-types.mjs @@ -633,16 +633,28 @@ const printUnion = (unionType) => { let sharedInterfaceNames = []; if (memberTypes.length > 0) { const [firstMember, ...otherMembers] = memberTypes; - const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name)); - for (const member of otherMembers) { - const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name)); - for (const ifaceName of Array.from(firstInterfaces)) { - if (!memberInterfaces.has(ifaceName)) { - firstInterfaces.delete(ifaceName); + // Check if member is a union (unions don't have getInterfaces) + if (typeof firstMember.getInterfaces === 'function') { + const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name)); + let allMembersHaveInterfaces = true; + for (const member of otherMembers) { + if (typeof member.getInterfaces === 'function') { + const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name)); + for (const ifaceName of Array.from(firstInterfaces)) { + if (!memberInterfaces.has(ifaceName)) { + firstInterfaces.delete(ifaceName); + } + } + } else { + // Member is a union, so no shared interfaces + allMembersHaveInterfaces = false; + break; } } + if (allMembersHaveInterfaces) { + sharedInterfaceNames = Array.from(firstInterfaces).sort(); + } } - sharedInterfaceNames = Array.from(firstInterfaces).sort(); } const implementations = sharedInterfaceNames.length ? ` : ${sharedInterfaceNames.join(', ')}` : ''; @@ -651,13 +663,68 @@ const printUnion = (unionType) => { lines.push(' companion object {'); lines.push(` fun fromJson(json: Map): ${unionType.name} {`); lines.push(' return when (json["__typename"] as String?) {'); - members.forEach((member) => { - lines.push(` "${member}" -> ${member}.fromJson(json)`); + + // Flatten nested unions: if a member is itself a union, include its concrete members + const concreteMembers = new Set(); + for (const memberType of memberTypes) { + if (isUnionType(memberType)) { + // Member is a union, get its concrete members + const nestedMembers = memberType.getTypes(); + for (const nestedMember of nestedMembers) { + concreteMembers.add(nestedMember.name); + } + } else { + // Member is a concrete type + concreteMembers.add(memberType.name); + } + } + + // Track nested unions that need wrapper classes + const nestedUnions = new Set(); + + // Generate case for each concrete member, wrapping nested unions + const sortedConcreteMembers = Array.from(concreteMembers).sort(); + sortedConcreteMembers.forEach((concreteMember) => { + // Find which direct member this concrete type belongs to + let delegateTo = concreteMember; + let isNestedUnion = false; + + for (const memberType of memberTypes) { + if (isUnionType(memberType)) { + const nestedMembers = memberType.getTypes().map(t => t.name); + if (nestedMembers.includes(concreteMember)) { + delegateTo = memberType.name; + isNestedUnion = true; + nestedUnions.add(memberType.name); + break; + } + } + } + + if (isNestedUnion) { + // Wrap nested union in a typed wrapper class + const wrapperName = `${delegateTo}Item`; + lines.push(` "${concreteMember}" -> ${wrapperName}(${delegateTo}.fromJson(json))`); + } else { + // Direct member, no wrapping needed + lines.push(` "${concreteMember}" -> ${delegateTo}.fromJson(json)`); + } }); + lines.push(` else -> throw IllegalArgumentException("Unknown __typename for ${unionType.name}: ${'$'}{json["__typename"]}")`); lines.push(' }'); lines.push(' }'); lines.push(' }'); + + // Generate wrapper data classes for nested unions (inside the sealed interface) + for (const nestedUnionName of Array.from(nestedUnions).sort()) { + const wrapperName = `${nestedUnionName}Item`; + lines.push(''); + lines.push(` data class ${wrapperName}(val value: ${nestedUnionName}) : ${unionType.name} {`); + lines.push(' override fun toJson() = value.toJson()'); + lines.push(' }'); + } + lines.push('}', ''); }; @@ -793,36 +860,10 @@ for (const [typeName, literals] of Object.entries(productTypeMapping)) { } } -// Post-process: Add ProductOrSubscription sealed interface for union type -// This allows FetchProductsResult.all to contain heterogeneous lists -const productOrSubscriptionUnion = ` -// Union type for FetchProductsResult.all -public sealed interface ProductOrSubscription { - data class ProductItem(val value: Product) : ProductOrSubscription - data class SubscriptionItem(val value: ProductSubscription) : ProductOrSubscription -} -`; - +// ProductOrSubscription union is now auto-generated from GraphQL schema +// FetchProductsResultAll is also auto-generated let output = lines.join('\n'); -// Insert ProductOrSubscription before FetchProductsResult -const fetchProductsResultInterfacePattern = /public sealed interface FetchProductsResult/; -if (fetchProductsResultInterfacePattern.test(output)) { - output = output.replace( - fetchProductsResultInterfacePattern, - productOrSubscriptionUnion + '\npublic sealed interface FetchProductsResult' - ); -} - -// Add the 'all' case to FetchProductsResult -const fetchProductsResultPattern = /(public data class FetchProductsResultSubscriptions\(val value: List\?\) : FetchProductsResult)/; -if (fetchProductsResultPattern.test(output)) { - output = output.replace( - fetchProductsResultPattern, - '$1\n\npublic data class FetchProductsResultAll(val value: List?) : FetchProductsResult' - ); -} - const outputPath = resolve(__dirname, '../src/generated/Types.kt'); mkdirSync(dirname(outputPath), { recursive: true }); writeFileSync(outputPath, output); diff --git a/packages/gql/scripts/generate-swift-types.mjs b/packages/gql/scripts/generate-swift-types.mjs index 12698595..5a87facc 100644 --- a/packages/gql/scripts/generate-swift-types.mjs +++ b/packages/gql/scripts/generate-swift-types.mjs @@ -541,16 +541,21 @@ const printUnion = (unionType) => { let sharedInterfaceNames = []; if (memberTypes.length > 0) { const [firstMember, ...otherMembers] = memberTypes; - const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name)); - for (const member of otherMembers) { - const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name)); - for (const ifaceName of Array.from(firstInterfaces)) { - if (!memberInterfaces.has(ifaceName)) { - firstInterfaces.delete(ifaceName); + // Check if member is a union (unions don't have getInterfaces) + if (typeof firstMember.getInterfaces === 'function') { + const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name)); + for (const member of otherMembers) { + if (typeof member.getInterfaces === 'function') { + const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name)); + for (const ifaceName of Array.from(firstInterfaces)) { + if (!memberInterfaces.has(ifaceName)) { + firstInterfaces.delete(ifaceName); + } + } } } + sharedInterfaceNames = Array.from(firstInterfaces).sort(); } - sharedInterfaceNames = Array.from(firstInterfaces).sort(); } const conformances = ['Codable', ...sharedInterfaceNames]; @@ -773,60 +778,9 @@ for (const [typeName, literals] of Object.entries(productTypeMapping)) { } } -// Post-process: Add ProductOrSubscription union enum -// This allows FetchProductsResult.all to contain heterogeneous arrays -const productOrSubscriptionEnum = ` -// Union type for FetchProductsResult.all -public enum ProductOrSubscription: Codable { - case product(Product) - case subscription(ProductSubscription) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let product = try? container.decode(Product.self) { - self = .product(product) - return - } - if let subscription = try? container.decode(ProductSubscription.self) { - self = .subscription(subscription) - return - } - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Cannot decode ProductOrSubscription" - ) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .product(let product): - try container.encode(product) - case .subscription(let subscription): - try container.encode(subscription) - } - } -} -`; - -// Add ProductOrSubscription before FetchProductsResult +// ProductOrSubscription union is now auto-generated from GraphQL schema +// FetchProductsResultAll case is also auto-generated let output = lines.join('\n'); -const fetchProductsResultPattern = /public enum FetchProductsResult \{/; -if (fetchProductsResultPattern.test(output)) { - output = output.replace( - fetchProductsResultPattern, - productOrSubscriptionEnum + '\npublic enum FetchProductsResult {' - ); -} - -// Add the 'all' case to FetchProductsResult -const fetchProductsResultEnumPattern = /public enum FetchProductsResult \{([\s\S]*?)\n\}/; -if (fetchProductsResultEnumPattern.test(output)) { - output = output.replace( - fetchProductsResultEnumPattern, - 'public enum FetchProductsResult {$1\n case all([ProductOrSubscription]?)\n}' - ); -} const outputPath = resolve(__dirname, '../src/generated/Types.swift'); mkdirSync(dirname(outputPath), { recursive: true }); diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index c68b4df0..d11cf9f6 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -774,21 +774,14 @@ public data class ExternalPurchaseNoticeResultIOS( ) } - -// Union type for FetchProductsResult.all -public sealed interface ProductOrSubscription { - data class ProductItem(val value: Product) : ProductOrSubscription - data class SubscriptionItem(val value: ProductSubscription) : ProductOrSubscription -} - public sealed interface FetchProductsResult +public data class FetchProductsResultAll(val value: List?) : FetchProductsResult + public data class FetchProductsResultProducts(val value: List?) : FetchProductsResult public data class FetchProductsResultSubscriptions(val value: List?) : FetchProductsResult -public data class FetchProductsResultAll(val value: List?) : FetchProductsResult - public data class PricingPhaseAndroid( val billingCycleCount: Int, val billingPeriod: String, @@ -2232,6 +2225,30 @@ public sealed interface Product : ProductCommon { } } +public sealed interface ProductOrSubscription { + fun toJson(): Map + + companion object { + fun fromJson(json: Map): ProductOrSubscription { + return when (json["__typename"] as String?) { + "ProductAndroid" -> ProductItem(Product.fromJson(json)) + "ProductIOS" -> ProductItem(Product.fromJson(json)) + "ProductSubscriptionAndroid" -> ProductSubscriptionItem(ProductSubscription.fromJson(json)) + "ProductSubscriptionIOS" -> ProductSubscriptionItem(ProductSubscription.fromJson(json)) + else -> throw IllegalArgumentException("Unknown __typename for ProductOrSubscription: ${json["__typename"]}") + } + } + } + + data class ProductItem(val value: Product) : ProductOrSubscription { + override fun toJson() = value.toJson() + } + + data class ProductSubscriptionItem(val value: ProductSubscription) : ProductOrSubscription { + override fun toJson() = value.toJson() + } +} + public sealed interface ProductSubscription : ProductCommon { fun toJson(): Map diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 153275ea..a3446a45 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -245,43 +245,10 @@ public struct ExternalPurchaseNoticeResultIOS: Codable { public var result: ExternalPurchaseNoticeAction } - -// Union type for FetchProductsResult.all -public enum ProductOrSubscription: Codable { - case product(Product) - case subscription(ProductSubscription) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let product = try? container.decode(Product.self) { - self = .product(product) - return - } - if let subscription = try? container.decode(ProductSubscription.self) { - self = .subscription(subscription) - return - } - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Cannot decode ProductOrSubscription" - ) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .product(let product): - try container.encode(product) - case .subscription(let subscription): - try container.encode(subscription) - } - } -} - public enum FetchProductsResult { + case all([ProductOrSubscription]?) case products([Product]?) case subscriptions([ProductSubscription]?) - case all([ProductOrSubscription]?) } public struct PricingPhaseAndroid: Codable { @@ -1032,6 +999,11 @@ public enum Product: Codable, ProductCommon { } } +public enum ProductOrSubscription: Codable { + case product(Product) + case productSubscription(ProductSubscription) +} + public enum ProductSubscription: Codable, ProductCommon { case productSubscriptionAndroid(ProductSubscriptionAndroid) case productSubscriptionIos(ProductSubscriptionIOS) diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 3531075c..bca38f9b 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -914,26 +914,15 @@ class ExternalPurchaseNoticeResultIOS { } } - -// Union type for FetchProductsResult.all -abstract class ProductOrSubscription { - const ProductOrSubscription(); -} - -class ProductOrSubscriptionProduct extends ProductOrSubscription { - const ProductOrSubscriptionProduct(this.value); - final Product value; -} - -class ProductOrSubscriptionSubscription extends ProductOrSubscription { - const ProductOrSubscriptionSubscription(this.value); - final ProductSubscription value; -} - abstract class FetchProductsResult { const FetchProductsResult(); } +class FetchProductsResultAll extends FetchProductsResult { + const FetchProductsResultAll(this.value); + final List? value; +} + class FetchProductsResultProducts extends FetchProductsResult { const FetchProductsResultProducts(this.value); final List? value; @@ -944,11 +933,6 @@ class FetchProductsResultSubscriptions extends FetchProductsResult { final List? value; } -class FetchProductsResultAll extends FetchProductsResult { - const FetchProductsResultAll(this.value); - final List? value; -} - class PricingPhaseAndroid { const PricingPhaseAndroid({ required this.billingCycleCount, @@ -2719,6 +2703,43 @@ sealed class Product implements ProductCommon { Map toJson(); } +sealed class ProductOrSubscription { + const ProductOrSubscription(); + + factory ProductOrSubscription.fromJson(Map json) { + final typeName = json['__typename'] as String?; + switch (typeName) { + case 'ProductAndroid': + return ProductOrSubscriptionProduct(Product.fromJson(json)); + case 'ProductIOS': + return ProductOrSubscriptionProduct(Product.fromJson(json)); + case 'ProductSubscriptionAndroid': + return ProductOrSubscriptionProductSubscription(ProductSubscription.fromJson(json)); + case 'ProductSubscriptionIOS': + return ProductOrSubscriptionProductSubscription(ProductSubscription.fromJson(json)); + } + throw ArgumentError('Unknown __typename for ProductOrSubscription: $typeName'); + } + + Map toJson(); +} + +class ProductOrSubscriptionProduct extends ProductOrSubscription { + const ProductOrSubscriptionProduct(this.value); + final Product value; + + @override + Map toJson() => value.toJson(); +} + +class ProductOrSubscriptionProductSubscription extends ProductOrSubscription { + const ProductOrSubscriptionProductSubscription(this.value); + final ProductSubscription value; + + @override + Map toJson() => value.toJson(); +} + sealed class ProductSubscription implements ProductCommon { const ProductSubscription(); diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index a3c624ab..5d0698e9 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -166,7 +166,7 @@ export interface ExternalPurchaseNoticeResultIOS { result: ExternalPurchaseNoticeAction; } -export type FetchProductsResult = Product[] | ProductSubscription[] | (Product | ProductSubscription)[] | null; +export type FetchProductsResult = ProductOrSubscription[] | Product[] | ProductSubscription[] | null; export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product-ios' | 'user-choice-billing-android'; @@ -354,6 +354,8 @@ export interface ProductIOS extends ProductCommon { typeIOS: ProductTypeIOS; } +export type ProductOrSubscription = Product | ProductSubscription; + export type ProductQueryType = 'in-app' | 'subs' | 'all'; export interface ProductRequest { @@ -526,7 +528,7 @@ export interface Query { /** Get current StoreKit 2 entitlements (iOS 15+) */ currentEntitlementIOS?: Promise<(PurchaseIOS | null)>; /** Retrieve products or subscriptions from the store */ - fetchProducts: Promise<(Product[] | ProductSubscription[] | null)>; + fetchProducts: Promise<(ProductOrSubscription[] | Product[] | ProductSubscription[] | null)>; /** Get active subscriptions (filters by subscriptionIds when provided) */ getActiveSubscriptions: Promise; /** Fetch the current app transaction (iOS 16+) */ diff --git a/packages/gql/src/type.graphql b/packages/gql/src/type.graphql index d2883e59..6bf83c60 100644 --- a/packages/gql/src/type.graphql +++ b/packages/gql/src/type.graphql @@ -77,11 +77,15 @@ type VoidResult { success: Boolean! } +# Product or Subscription union for 'all' type +union ProductOrSubscription = Product | ProductSubscription + # => Union # Product fetch responses can return products, subscriptions, or both type FetchProductsResult { products: [Product!] subscriptions: [ProductSubscription!] + all: [ProductOrSubscription!] } # => Union