diff --git a/scripts/fix-generated-types.mjs b/scripts/fix-generated-types.mjs index 6ba023e..cbfa92a 100644 --- a/scripts/fix-generated-types.mjs +++ b/scripts/fix-generated-types.mjs @@ -168,6 +168,23 @@ content = content.replace(/export enum [^{]+\{[\s\S]*?\}/g, (block) => block.replace(/= '([^']+)'/g, (_, value) => `= '${toConstantCase(value)}'`) ); +content = content.replace( + /export interface RequestPurchaseProps \{[\s\S]*?\}\n\n/, + [ + 'export type RequestPurchaseProps =', + ' | {', + ' /** Per-platform purchase request props */', + ' request: RequestPurchasePropsByPlatforms;', + " type: 'in-app';", + ' }', + ' | {', + ' /** Per-platform subscription request props */', + ' request: RequestSubscriptionPropsByPlatforms;', + " type: 'subs';", + ' };\n\n', + ].join('\n'), +); + const futureFields = new Set(); for (const file of schemaFiles) { let previousWasMarker = false; diff --git a/scripts/generate-dart-types.mjs b/scripts/generate-dart-types.mjs index c6c6c08..76cef5e 100644 --- a/scripts/generate-dart-types.mjs +++ b/scripts/generate-dart-types.mjs @@ -390,6 +390,91 @@ const printObject = (objectType) => { }; const printInput = (inputType) => { + if (inputType.name === 'RequestPurchaseProps') { + addDocComment(lines, inputType.description); + lines.push('class RequestPurchaseProps {'); + lines.push(' RequestPurchaseProps({'); + lines.push(' required this.request,'); + lines.push(' ProductQueryType? type,'); + lines.push(' }) : type = type ?? (request is RequestPurchasePropsRequestPurchase'); + lines.push(' ? ProductQueryType.InApp'); + lines.push(' : ProductQueryType.Subs) {'); + lines.push(' if (request is RequestPurchasePropsRequestPurchase && this.type != ProductQueryType.InApp) {'); + lines.push(" throw ArgumentError('type must be IN_APP when requestPurchase is provided');"); + lines.push(' }'); + lines.push(' if (request is RequestPurchasePropsRequestSubscription && this.type != ProductQueryType.Subs) {'); + lines.push(" throw ArgumentError('type must be SUBS when requestSubscription is provided');"); + lines.push(' }'); + lines.push(' }'); + lines.push(''); + lines.push(' final RequestPurchasePropsRequest request;'); + lines.push(' final ProductQueryType type;'); + lines.push(''); + lines.push(' factory RequestPurchaseProps.fromJson(Map json) {'); + lines.push(" final typeValue = json['type'] as String?;"); + lines.push(' final parsedType = typeValue != null ? ProductQueryType.fromJson(typeValue) : null;'); + lines.push(" final purchaseJson = json['requestPurchase'] as Map?;"); + lines.push(' if (purchaseJson != null) {'); + lines.push(' final request = RequestPurchasePropsRequestPurchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson));'); + lines.push(' final finalType = parsedType ?? ProductQueryType.InApp;'); + lines.push(' if (finalType != ProductQueryType.InApp) {'); + lines.push(" throw ArgumentError('type must be IN_APP when requestPurchase is provided');"); + lines.push(' }'); + lines.push(' return RequestPurchaseProps(request: request, type: finalType);'); + lines.push(' }'); + lines.push(" final subscriptionJson = json['requestSubscription'] as Map?;"); + lines.push(' if (subscriptionJson != null) {'); + lines.push(' final request = RequestPurchasePropsRequestSubscription(RequestSubscriptionPropsByPlatforms.fromJson(subscriptionJson));'); + lines.push(' final finalType = parsedType ?? ProductQueryType.Subs;'); + lines.push(' if (finalType != ProductQueryType.Subs) {'); + lines.push(" throw ArgumentError('type must be SUBS when requestSubscription is provided');"); + lines.push(' }'); + lines.push(' return RequestPurchaseProps(request: request, type: finalType);'); + lines.push(' }'); + lines.push(" throw ArgumentError('RequestPurchaseProps requires requestPurchase or requestSubscription');"); + lines.push(' }'); + lines.push(''); + lines.push(' Map toJson() {'); + lines.push(' if (request is RequestPurchasePropsRequestPurchase) {'); + lines.push(' return {'); + lines.push(" 'requestPurchase': (request as RequestPurchasePropsRequestPurchase).value.toJson(),"); + lines.push(" 'type': type.toJson(),"); + lines.push(' };'); + lines.push(' }'); + lines.push(' if (request is RequestPurchasePropsRequestSubscription) {'); + lines.push(' return {'); + lines.push(" 'requestSubscription': (request as RequestPurchasePropsRequestSubscription).value.toJson(),"); + lines.push(" 'type': type.toJson(),"); + lines.push(' };'); + lines.push(' }'); + lines.push(" throw StateError('Unsupported RequestPurchaseProps request variant');"); + lines.push(' }'); + lines.push(''); + lines.push(' static RequestPurchaseProps inApp({required RequestPurchasePropsByPlatforms request}) {'); + lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestPurchase(request), type: ProductQueryType.InApp);'); + lines.push(' }'); + lines.push(''); + lines.push(' static RequestPurchaseProps subs({required RequestSubscriptionPropsByPlatforms request}) {'); + lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestSubscription(request), type: ProductQueryType.Subs);'); + lines.push(' }'); + lines.push('}'); + lines.push(''); + lines.push('sealed class RequestPurchasePropsRequest {'); + lines.push(' const RequestPurchasePropsRequest();'); + lines.push('}'); + lines.push(''); + lines.push('class RequestPurchasePropsRequestPurchase extends RequestPurchasePropsRequest {'); + lines.push(' const RequestPurchasePropsRequestPurchase(this.value);'); + lines.push(' final RequestPurchasePropsByPlatforms value;'); + lines.push('}'); + lines.push(''); + lines.push('class RequestPurchasePropsRequestSubscription extends RequestPurchasePropsRequest {'); + lines.push(' const RequestPurchasePropsRequestSubscription(this.value);'); + lines.push(' final RequestSubscriptionPropsByPlatforms value;'); + lines.push('}'); + lines.push(''); + return; + } addDocComment(lines, inputType.description); lines.push(`class ${inputType.name} {`); lines.push(` const ${inputType.name}({`); @@ -432,8 +517,27 @@ const printInput = (inputType) => { const printUnion = (unionType) => { addDocComment(lines, unionType.description); - const members = unionType.getTypes().map((member) => member.name).sort(); - lines.push(`sealed class ${unionType.name} {`); + const memberTypes = unionType.getTypes(); + const members = memberTypes.map((member) => member.name).sort(); + + 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); + } + } + } + sharedInterfaceNames = Array.from(firstInterfaces).sort(); + } + + const implementsClause = sharedInterfaceNames.length ? ` implements ${sharedInterfaceNames.join(', ')}` : ''; + + lines.push(`sealed class ${unionType.name}${implementsClause} {`); lines.push(` const ${unionType.name}();`, ''); lines.push(` factory ${unionType.name}.fromJson(Map json) {`); lines.push(` final typeName = json['__typename'] as String?;`); @@ -444,6 +548,33 @@ const printUnion = (unionType) => { lines.push(' }'); lines.push(` throw ArgumentError('Unknown __typename for ${unionType.name}: $typeName');`); lines.push(' }', ''); + + if (sharedInterfaceNames.length) { + const interfaceFieldMap = new Map(); + for (const interfaceName of sharedInterfaceNames) { + const interfaceType = schema.getType(interfaceName); + if (!interfaceType || !isInterfaceType(interfaceType)) continue; + const fields = Object.values(interfaceType.getFields()).sort((a, b) => a.name.localeCompare(b.name)); + for (const field of fields) { + if (interfaceFieldMap.has(field.name)) continue; + const { type, nullable } = getDartType(field.type); + interfaceFieldMap.set(field.name, { field, type, nullable }); + } + } + + const interfaceFields = Array.from(interfaceFieldMap.values()).sort((a, b) => a.field.name.localeCompare(b.field.name)); + interfaceFields.forEach(({ field, type, nullable }) => { + addDocComment(lines, field.description, ' '); + const fieldType = `${type}${nullable ? '?' : ''}`; + const fieldName = escapeDartName(field.name); + lines.push(' @override'); + lines.push(` ${fieldType} get ${fieldName};`); + }); + if (interfaceFields.length) { + lines.push(''); + } + } + lines.push(' Map toJson();'); lines.push('}', ''); }; diff --git a/scripts/generate-kotlin-types.mjs b/scripts/generate-kotlin-types.mjs index 2c3306f..4869e49 100644 --- a/scripts/generate-kotlin-types.mjs +++ b/scripts/generate-kotlin-types.mjs @@ -390,7 +390,61 @@ const printDataClass = (objectType) => { }; const printInput = (inputType) => { + if (inputType.name === 'RequestPurchaseProps') { + addDocComment(lines, inputType.description); + lines.push('public data class RequestPurchaseProps('); + lines.push(' val request: Request,'); + lines.push(' val type: ProductQueryType'); + lines.push(') {'); + lines.push(' init {'); + lines.push(' when (request) {'); + lines.push(' is Request.Purchase -> require(type == ProductQueryType.InApp) { "type must be IN_APP when request is purchase" }'); + lines.push(' is Request.Subscription -> require(type == ProductQueryType.Subs) { "type must be SUBS when request is subscription" }'); + lines.push(' }'); + lines.push(' }'); + lines.push(''); + lines.push(' companion object {'); + lines.push(' fun fromJson(json: Map): RequestPurchaseProps {'); + lines.push(' val rawType = (json["type"] as String?)?.let { ProductQueryType.fromJson(it) }'); + lines.push(' val purchaseJson = json["requestPurchase"] as Map?'); + lines.push(' if (purchaseJson != null) {'); + lines.push(' val request = Request.Purchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson))'); + lines.push(' val finalType = rawType ?: ProductQueryType.InApp'); + lines.push(' require(finalType == ProductQueryType.InApp) { "type must be IN_APP when requestPurchase is provided" }'); + lines.push(' return RequestPurchaseProps(request = request, type = finalType)'); + lines.push(' }'); + lines.push(' val subscriptionJson = json["requestSubscription"] as Map?'); + lines.push(' if (subscriptionJson != null) {'); + lines.push(' val request = Request.Subscription(RequestSubscriptionPropsByPlatforms.fromJson(subscriptionJson))'); + lines.push(' val finalType = rawType ?: ProductQueryType.Subs'); + lines.push(' require(finalType == ProductQueryType.Subs) { "type must be SUBS when requestSubscription is provided" }'); + lines.push(' return RequestPurchaseProps(request = request, type = finalType)'); + lines.push(' }'); + lines.push(' throw IllegalArgumentException("RequestPurchaseProps requires requestPurchase or requestSubscription")'); + lines.push(' }'); + lines.push(' }'); + lines.push(''); + lines.push(' fun toJson(): Map = when (request) {'); + lines.push(' is Request.Purchase -> mapOf('); + lines.push(' "requestPurchase" to request.value.toJson(),'); + lines.push(' "type" to type.toJson(),'); + lines.push(' )'); + lines.push(' is Request.Subscription -> mapOf('); + lines.push(' "requestSubscription" to request.value.toJson(),'); + lines.push(' "type" to type.toJson(),'); + lines.push(' )'); + lines.push(' }'); + lines.push(''); + lines.push(' sealed class Request {'); + lines.push(' data class Purchase(val value: RequestPurchasePropsByPlatforms) : Request()'); + lines.push(' data class Subscription(val value: RequestSubscriptionPropsByPlatforms) : Request()'); + lines.push(' }'); + lines.push('}'); + lines.push(''); + return; + } addDocComment(lines, inputType.description); + lines.push(`public data class ${inputType.name}(`); const fields = Object.values(inputType.getFields()).sort((a, b) => a.name.localeCompare(b.name)); const fieldInfos = fields.map((field) => { const { type, nullable, metadata } = getKotlinType(field.type); @@ -399,7 +453,6 @@ const printInput = (inputType) => { const defaultValue = nullable ? ' = null' : ''; return { field, propertyName, propertyType, defaultValue, metadata }; }); - lines.push(`public data class ${inputType.name}(`); fieldInfos.forEach(({ field, propertyName, propertyType, defaultValue }, index) => { addDocComment(lines, field.description, ' '); const suffix = index === fieldInfos.length - 1 ? '' : ','; @@ -427,8 +480,26 @@ const printInput = (inputType) => { const printUnion = (unionType) => { addDocComment(lines, unionType.description); - const members = unionType.getTypes().map((member) => member.name).sort(); - lines.push(`public sealed interface ${unionType.name} {`); + const memberTypes = unionType.getTypes(); + const members = memberTypes.map((member) => member.name).sort(); + + 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); + } + } + } + sharedInterfaceNames = Array.from(firstInterfaces).sort(); + } + + const implementations = sharedInterfaceNames.length ? ` : ${sharedInterfaceNames.join(', ')}` : ''; + lines.push(`public sealed interface ${unionType.name}${implementations} {`); lines.push(' fun toJson(): Map', ''); lines.push(' companion object {'); lines.push(` fun fromJson(json: Map): ${unionType.name} {`); diff --git a/scripts/generate-swift-types.mjs b/scripts/generate-swift-types.mjs index a0751e3..3cfaee7 100644 --- a/scripts/generate-swift-types.mjs +++ b/scripts/generate-swift-types.mjs @@ -248,6 +248,75 @@ const printObject = (objectType) => { }; const printInput = (inputType) => { + if (inputType.name === 'RequestPurchaseProps') { + addDocComment(lines, inputType.description); + lines.push('public struct RequestPurchaseProps: Codable {'); + lines.push(' public var request: Request'); + lines.push(' public var type: ProductQueryType'); + lines.push(''); + lines.push(' public init(request: Request, type: ProductQueryType? = nil) {'); + lines.push(' switch request {'); + lines.push(' case .purchase:'); + lines.push(' let resolved = type ?? .inApp'); + lines.push(' precondition(resolved == .inApp, "RequestPurchaseProps.type must be .inApp when request is purchase")'); + lines.push(' self.type = resolved'); + lines.push(' case .subscription:'); + lines.push(' let resolved = type ?? .subs'); + lines.push(' precondition(resolved == .subs, "RequestPurchaseProps.type must be .subs when request is subscription")'); + lines.push(' self.type = resolved'); + lines.push(' }'); + lines.push(' self.request = request'); + lines.push(' }'); + lines.push(''); + lines.push(' private enum CodingKeys: String, CodingKey {'); + lines.push(' case requestPurchase'); + lines.push(' case requestSubscription'); + lines.push(' case type'); + lines.push(' }'); + lines.push(''); + lines.push(' public init(from decoder: Decoder) throws {'); + lines.push(' let container = try decoder.container(keyedBy: CodingKeys.self)'); + lines.push(' let decodedType = try container.decodeIfPresent(ProductQueryType.self, forKey: .type)'); + lines.push(' if let purchase = try container.decodeIfPresent(RequestPurchasePropsByPlatforms.self, forKey: .requestPurchase) {'); + lines.push(' let finalType = decodedType ?? .inApp'); + lines.push(' guard finalType == .inApp else {'); + lines.push(' throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "type must be IN_APP when requestPurchase is provided")'); + lines.push(' }'); + lines.push(' self.request = .purchase(purchase)'); + lines.push(' self.type = finalType'); + lines.push(' return'); + lines.push(' }'); + lines.push(' if let subscription = try container.decodeIfPresent(RequestSubscriptionPropsByPlatforms.self, forKey: .requestSubscription) {'); + lines.push(' let finalType = decodedType ?? .subs'); + lines.push(' guard finalType == .subs else {'); + lines.push(' throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "type must be SUBS when requestSubscription is provided")'); + lines.push(' }'); + lines.push(' self.request = .subscription(subscription)'); + lines.push(' self.type = finalType'); + lines.push(' return'); + lines.push(' }'); + lines.push(' throw DecodingError.dataCorruptedError(forKey: .requestPurchase, in: container, debugDescription: "RequestPurchaseProps requires requestPurchase or requestSubscription.")'); + lines.push(' }'); + lines.push(''); + lines.push(' public func encode(to encoder: Encoder) throws {'); + lines.push(' var container = encoder.container(keyedBy: CodingKeys.self)'); + lines.push(' switch request {'); + lines.push(' case let .purchase(value):'); + lines.push(' try container.encode(value, forKey: .requestPurchase)'); + lines.push(' case let .subscription(value):'); + lines.push(' try container.encode(value, forKey: .requestSubscription)'); + lines.push(' }'); + lines.push(' try container.encode(type, forKey: .type)'); + lines.push(' }'); + lines.push(''); + lines.push(' public enum Request {'); + lines.push(' case purchase(RequestPurchasePropsByPlatforms)'); + lines.push(' case subscription(RequestSubscriptionPropsByPlatforms)'); + lines.push(' }'); + lines.push('}'); + lines.push(''); + return; + } addDocComment(lines, inputType.description); lines.push(`public struct ${inputType.name}: Codable {`); const fields = Object.values(inputType.getFields()).sort((a, b) => a.name.localeCompare(b.name)); @@ -267,12 +336,70 @@ const printInput = (inputType) => { const printUnion = (unionType) => { addDocComment(lines, unionType.description); - lines.push(`public enum ${unionType.name} {`); - const types = unionType.getTypes(); - for (const member of types) { - const caseName = escapeSwiftName(lowerCamelCase(member.name)); - lines.push(` case ${caseName}(${member.name})`); + const memberTypes = unionType.getTypes(); + const caseInfos = memberTypes.map((member) => ({ + typeName: member.name, + caseName: escapeSwiftName(lowerCamelCase(member.name)), + })); + + 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); + } + } + } + sharedInterfaceNames = Array.from(firstInterfaces).sort(); + } + + const conformances = ['Codable', ...sharedInterfaceNames]; + const conformanceClause = conformances.length ? `: ${conformances.join(', ')}` : ''; + + lines.push(`public enum ${unionType.name}${conformanceClause} {`); + caseInfos.forEach(({ typeName, caseName }) => { + lines.push(` case ${caseName}(${typeName})`); + }); + + if (sharedInterfaceNames.length) { + const interfaceFieldMap = new Map(); + for (const interfaceName of sharedInterfaceNames) { + const interfaceType = schema.getType(interfaceName); + if (!interfaceType || !isInterfaceType(interfaceType)) continue; + const fields = Object.values(interfaceType.getFields()).sort((a, b) => a.name.localeCompare(b.name)); + for (const field of fields) { + if (interfaceFieldMap.has(field.name)) continue; + const { type, optional } = swiftTypeFor(field.type); + interfaceFieldMap.set(field.name, { field, type, optional }); + } + } + + const interfaceFields = Array.from(interfaceFieldMap.values()).sort((a, b) => a.field.name.localeCompare(b.field.name)); + if (interfaceFields.length) { + lines.push(''); + } + interfaceFields.forEach(({ field, type, optional }, index) => { + addDocComment(lines, field.description, ' '); + const propertyType = type + (optional ? '?' : ''); + const propertyName = escapeSwiftName(field.name); + lines.push(` public var ${propertyName}: ${propertyType} {`); + lines.push(' switch self {'); + caseInfos.forEach(({ caseName }) => { + lines.push(` case let .${caseName}(value):`); + lines.push(` return value.${propertyName}`); + }); + lines.push(' }'); + lines.push(' }'); + if (index < interfaceFields.length - 1) { + lines.push(''); + } + }); } + lines.push('}', ''); }; diff --git a/src/api.graphql b/src/api.graphql index ff823b7..90bd320 100644 --- a/src/api.graphql +++ b/src/api.graphql @@ -40,7 +40,7 @@ extend type Mutation { Initiate a purchase flow; rely on events for final state """ # Future - requestPurchase(params: PurchaseParams!): RequestPurchaseResult + requestPurchase(params: RequestPurchaseProps!): RequestPurchaseResult """ Finish a transaction after validating receipts """ diff --git a/src/generated/Types.kt b/src/generated/Types.kt index ff02f37..e525176 100644 --- a/src/generated/Types.kt +++ b/src/generated/Types.kt @@ -1559,37 +1559,6 @@ public data class PurchaseOptions( ) } -public data class PurchaseParams( - /** - * Per-platform purchase request props - */ - val requestPurchase: RequestPurchasePropsByPlatforms? = null, - /** - * Per-platform subscription request props - */ - val requestSubscription: RequestSubscriptionPropsByPlatforms? = null, - /** - * Explicit purchase type hint (defaults to in-app) - */ - val type: ProductQueryType? = null -) { - companion object { - fun fromJson(json: Map): PurchaseParams { - return PurchaseParams( - requestPurchase = (json["requestPurchase"] as Map?)?.let { RequestPurchasePropsByPlatforms.fromJson(it) }, - requestSubscription = (json["requestSubscription"] as Map?)?.let { RequestSubscriptionPropsByPlatforms.fromJson(it) }, - type = (json["type"] as String?)?.let { ProductQueryType.fromJson(it) }, - ) - } - } - - fun toJson(): Map = mapOf( - "requestPurchase" to requestPurchase?.toJson(), - "requestSubscription" to requestSubscription?.toJson(), - "type" to type?.toJson(), - ) -} - public data class ReceiptValidationAndroidOptions( val accessToken: String, val isSub: Boolean? = null, @@ -1721,28 +1690,52 @@ public data class RequestPurchaseIosProps( } public data class RequestPurchaseProps( - /** - * Android-specific purchase parameters - */ - val android: RequestPurchaseAndroidProps? = null, - /** - * iOS-specific purchase parameters - */ - val ios: RequestPurchaseIosProps? = null + val request: Request, + val type: ProductQueryType ) { + init { + when (request) { + is Request.Purchase -> require(type == ProductQueryType.InApp) { "type must be IN_APP when request is purchase" } + is Request.Subscription -> require(type == ProductQueryType.Subs) { "type must be SUBS when request is subscription" } + } + } + companion object { fun fromJson(json: Map): RequestPurchaseProps { - return RequestPurchaseProps( - android = (json["android"] as Map?)?.let { RequestPurchaseAndroidProps.fromJson(it) }, - ios = (json["ios"] as Map?)?.let { RequestPurchaseIosProps.fromJson(it) }, - ) + val rawType = (json["type"] as String?)?.let { ProductQueryType.fromJson(it) } + val purchaseJson = json["requestPurchase"] as Map? + if (purchaseJson != null) { + val request = Request.Purchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson)) + val finalType = rawType ?: ProductQueryType.InApp + require(finalType == ProductQueryType.InApp) { "type must be IN_APP when requestPurchase is provided" } + return RequestPurchaseProps(request = request, type = finalType) + } + val subscriptionJson = json["requestSubscription"] as Map? + if (subscriptionJson != null) { + val request = Request.Subscription(RequestSubscriptionPropsByPlatforms.fromJson(subscriptionJson)) + val finalType = rawType ?: ProductQueryType.Subs + require(finalType == ProductQueryType.Subs) { "type must be SUBS when requestSubscription is provided" } + return RequestPurchaseProps(request = request, type = finalType) + } + throw IllegalArgumentException("RequestPurchaseProps requires requestPurchase or requestSubscription") } } - fun toJson(): Map = mapOf( - "android" to android?.toJson(), - "ios" to ios?.toJson(), - ) + fun toJson(): Map = when (request) { + is Request.Purchase -> mapOf( + "requestPurchase" to request.value.toJson(), + "type" to type.toJson(), + ) + is Request.Subscription -> mapOf( + "requestSubscription" to request.value.toJson(), + "type" to type.toJson(), + ) + } + + sealed class Request { + data class Purchase(val value: RequestPurchasePropsByPlatforms) : Request() + data class Subscription(val value: RequestSubscriptionPropsByPlatforms) : Request() + } } public data class RequestPurchasePropsByPlatforms( @@ -1880,7 +1873,7 @@ public data class RequestSubscriptionPropsByPlatforms( // MARK: - Unions -public sealed interface Product { +public sealed interface Product : ProductCommon { fun toJson(): Map companion object { @@ -1894,7 +1887,7 @@ public sealed interface Product { } } -public sealed interface ProductSubscription { +public sealed interface ProductSubscription : ProductCommon { fun toJson(): Map companion object { @@ -1908,7 +1901,7 @@ public sealed interface ProductSubscription { } } -public sealed interface Purchase { +public sealed interface Purchase : PurchaseCommon { fun toJson(): Map companion object { @@ -1981,7 +1974,7 @@ public interface MutationResolver { /** * Initiate a purchase flow; rely on events for final state */ - suspend fun requestPurchase(params: PurchaseParams): RequestPurchaseResult? + suspend fun requestPurchase(params: RequestPurchaseProps): RequestPurchaseResult? /** * Purchase the promoted product surfaced by the App Store */ diff --git a/src/generated/Types.swift b/src/generated/Types.swift index 079a6c8..0b8cbd4 100644 --- a/src/generated/Types.swift +++ b/src/generated/Types.swift @@ -496,15 +496,6 @@ public struct PurchaseOptions: Codable { public var onlyIncludeActiveItemsIOS: Bool? } -public struct PurchaseParams: Codable { - /// Per-platform purchase request props - public var requestPurchase: RequestPurchasePropsByPlatforms? - /// Per-platform subscription request props - public var requestSubscription: RequestSubscriptionPropsByPlatforms? - /// Explicit purchase type hint (defaults to in-app) - public var type: ProductQueryType? -} - public struct ReceiptValidationAndroidOptions: Codable { public var accessToken: String public var isSub: Bool? @@ -544,10 +535,68 @@ public struct RequestPurchaseIosProps: Codable { } public struct RequestPurchaseProps: Codable { - /// Android-specific purchase parameters - public var android: RequestPurchaseAndroidProps? - /// iOS-specific purchase parameters - public var ios: RequestPurchaseIosProps? + public var request: Request + public var type: ProductQueryType + + public init(request: Request, type: ProductQueryType? = nil) { + switch request { + case .purchase: + let resolved = type ?? .inApp + precondition(resolved == .inApp, "RequestPurchaseProps.type must be .inApp when request is purchase") + self.type = resolved + case .subscription: + let resolved = type ?? .subs + precondition(resolved == .subs, "RequestPurchaseProps.type must be .subs when request is subscription") + self.type = resolved + } + self.request = request + } + + private enum CodingKeys: String, CodingKey { + case requestPurchase + case requestSubscription + case type + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let decodedType = try container.decodeIfPresent(ProductQueryType.self, forKey: .type) + if let purchase = try container.decodeIfPresent(RequestPurchasePropsByPlatforms.self, forKey: .requestPurchase) { + let finalType = decodedType ?? .inApp + guard finalType == .inApp else { + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "type must be IN_APP when requestPurchase is provided") + } + self.request = .purchase(purchase) + self.type = finalType + return + } + if let subscription = try container.decodeIfPresent(RequestSubscriptionPropsByPlatforms.self, forKey: .requestSubscription) { + let finalType = decodedType ?? .subs + guard finalType == .subs else { + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "type must be SUBS when requestSubscription is provided") + } + self.request = .subscription(subscription) + self.type = finalType + return + } + throw DecodingError.dataCorruptedError(forKey: .requestPurchase, in: container, debugDescription: "RequestPurchaseProps requires requestPurchase or requestSubscription.") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch request { + case let .purchase(value): + try container.encode(value, forKey: .requestPurchase) + case let .subscription(value): + try container.encode(value, forKey: .requestSubscription) + } + try container.encode(type, forKey: .type) + } + + public enum Request { + case purchase(RequestPurchasePropsByPlatforms) + case subscription(RequestSubscriptionPropsByPlatforms) + } } public struct RequestPurchasePropsByPlatforms: Codable { @@ -591,22 +640,284 @@ public struct RequestSubscriptionPropsByPlatforms: Codable { // MARK: - Unions -public enum Product { +public enum Product: Codable, ProductCommon { case productAndroid(ProductAndroid) case productIos(ProductIOS) -} -public enum ProductSubscription { + public var currency: String { + switch self { + case let .productAndroid(value): + return value.currency + case let .productIos(value): + return value.currency + } + } + + public var debugDescription: String? { + switch self { + case let .productAndroid(value): + return value.debugDescription + case let .productIos(value): + return value.debugDescription + } + } + + public var description: String { + switch self { + case let .productAndroid(value): + return value.description + case let .productIos(value): + return value.description + } + } + + public var displayName: String? { + switch self { + case let .productAndroid(value): + return value.displayName + case let .productIos(value): + return value.displayName + } + } + + public var displayPrice: String { + switch self { + case let .productAndroid(value): + return value.displayPrice + case let .productIos(value): + return value.displayPrice + } + } + + public var id: String { + switch self { + case let .productAndroid(value): + return value.id + case let .productIos(value): + return value.id + } + } + + public var platform: IapPlatform { + switch self { + case let .productAndroid(value): + return value.platform + case let .productIos(value): + return value.platform + } + } + + public var price: Double? { + switch self { + case let .productAndroid(value): + return value.price + case let .productIos(value): + return value.price + } + } + + public var title: String { + switch self { + case let .productAndroid(value): + return value.title + case let .productIos(value): + return value.title + } + } + + public var type: ProductType { + switch self { + case let .productAndroid(value): + return value.type + case let .productIos(value): + return value.type + } + } +} + +public enum ProductSubscription: Codable, ProductCommon { case productSubscriptionAndroid(ProductSubscriptionAndroid) case productSubscriptionIos(ProductSubscriptionIOS) -} -public enum Purchase { + public var currency: String { + switch self { + case let .productSubscriptionAndroid(value): + return value.currency + case let .productSubscriptionIos(value): + return value.currency + } + } + + public var debugDescription: String? { + switch self { + case let .productSubscriptionAndroid(value): + return value.debugDescription + case let .productSubscriptionIos(value): + return value.debugDescription + } + } + + public var description: String { + switch self { + case let .productSubscriptionAndroid(value): + return value.description + case let .productSubscriptionIos(value): + return value.description + } + } + + public var displayName: String? { + switch self { + case let .productSubscriptionAndroid(value): + return value.displayName + case let .productSubscriptionIos(value): + return value.displayName + } + } + + public var displayPrice: String { + switch self { + case let .productSubscriptionAndroid(value): + return value.displayPrice + case let .productSubscriptionIos(value): + return value.displayPrice + } + } + + public var id: String { + switch self { + case let .productSubscriptionAndroid(value): + return value.id + case let .productSubscriptionIos(value): + return value.id + } + } + + public var platform: IapPlatform { + switch self { + case let .productSubscriptionAndroid(value): + return value.platform + case let .productSubscriptionIos(value): + return value.platform + } + } + + public var price: Double? { + switch self { + case let .productSubscriptionAndroid(value): + return value.price + case let .productSubscriptionIos(value): + return value.price + } + } + + public var title: String { + switch self { + case let .productSubscriptionAndroid(value): + return value.title + case let .productSubscriptionIos(value): + return value.title + } + } + + public var type: ProductType { + switch self { + case let .productSubscriptionAndroid(value): + return value.type + case let .productSubscriptionIos(value): + return value.type + } + } +} + +public enum Purchase: Codable, PurchaseCommon { case purchaseAndroid(PurchaseAndroid) case purchaseIos(PurchaseIOS) -} -public enum ReceiptValidationResult { + public var id: String { + switch self { + case let .purchaseAndroid(value): + return value.id + case let .purchaseIos(value): + return value.id + } + } + + public var ids: [String]? { + switch self { + case let .purchaseAndroid(value): + return value.ids + case let .purchaseIos(value): + return value.ids + } + } + + public var isAutoRenewing: Bool { + switch self { + case let .purchaseAndroid(value): + return value.isAutoRenewing + case let .purchaseIos(value): + return value.isAutoRenewing + } + } + + public var platform: IapPlatform { + switch self { + case let .purchaseAndroid(value): + return value.platform + case let .purchaseIos(value): + return value.platform + } + } + + public var productId: String { + switch self { + case let .purchaseAndroid(value): + return value.productId + case let .purchaseIos(value): + return value.productId + } + } + + public var purchaseState: PurchaseState { + switch self { + case let .purchaseAndroid(value): + return value.purchaseState + case let .purchaseIos(value): + return value.purchaseState + } + } + + /// Unified purchase token (iOS JWS, Android purchaseToken) + public var purchaseToken: String? { + switch self { + case let .purchaseAndroid(value): + return value.purchaseToken + case let .purchaseIos(value): + return value.purchaseToken + } + } + + public var quantity: Int { + switch self { + case let .purchaseAndroid(value): + return value.quantity + case let .purchaseIos(value): + return value.quantity + } + } + + public var transactionDate: Double { + switch self { + case let .purchaseAndroid(value): + return value.transactionDate + case let .purchaseIos(value): + return value.transactionDate + } + } +} + +public enum ReceiptValidationResult: Codable { case receiptValidationResultAndroid(ReceiptValidationResultAndroid) case receiptValidationResultIos(ReceiptValidationResultIOS) } @@ -634,7 +945,7 @@ public protocol MutationResolver { /// Present the App Store code redemption sheet func presentCodeRedemptionSheetIOS() async throws -> VoidResult /// Initiate a purchase flow; rely on events for final state - func requestPurchase(params: PurchaseParams) async throws -> RequestPurchaseResult? + func requestPurchase(params: RequestPurchaseProps) async throws -> RequestPurchaseResult? /// Purchase the promoted product surfaced by the App Store func requestPurchaseOnPromotedProductIOS() async throws -> PurchaseIOS /// Restore completed purchases across platforms diff --git a/src/generated/types.dart b/src/generated/types.dart index c456f79..2aab363 100644 --- a/src/generated/types.dart +++ b/src/generated/types.dart @@ -2084,40 +2084,6 @@ class PurchaseOptions { } } -class PurchaseParams { - const PurchaseParams({ - /// Per-platform purchase request props - this.requestPurchase, - /// Per-platform subscription request props - this.requestSubscription, - /// Explicit purchase type hint (defaults to in-app) - this.type, - }); - - /// Per-platform purchase request props - final RequestPurchasePropsByPlatforms? requestPurchase; - /// Per-platform subscription request props - final RequestSubscriptionPropsByPlatforms? requestSubscription; - /// Explicit purchase type hint (defaults to in-app) - final ProductQueryType? type; - - factory PurchaseParams.fromJson(Map json) { - return PurchaseParams( - requestPurchase: json['requestPurchase'] != null ? RequestPurchasePropsByPlatforms.fromJson(json['requestPurchase'] as Map) : null, - requestSubscription: json['requestSubscription'] != null ? RequestSubscriptionPropsByPlatforms.fromJson(json['requestSubscription'] as Map) : null, - type: json['type'] != null ? ProductQueryType.fromJson(json['type'] as String) : null, - ); - } - - Map toJson() { - return { - 'requestPurchase': requestPurchase?.toJson(), - 'requestSubscription': requestSubscription?.toJson(), - 'type': type?.toJson(), - }; - } -} - class ReceiptValidationAndroidOptions { const ReceiptValidationAndroidOptions({ required this.accessToken, @@ -2265,33 +2231,86 @@ class RequestPurchaseIosProps { } class RequestPurchaseProps { - const RequestPurchaseProps({ - /// Android-specific purchase parameters - this.android, - /// iOS-specific purchase parameters - this.ios, - }); + RequestPurchaseProps({ + required this.request, + ProductQueryType? type, + }) : type = type ?? (request is RequestPurchasePropsRequestPurchase + ? ProductQueryType.InApp + : ProductQueryType.Subs) { + if (request is RequestPurchasePropsRequestPurchase && this.type != ProductQueryType.InApp) { + throw ArgumentError('type must be IN_APP when requestPurchase is provided'); + } + if (request is RequestPurchasePropsRequestSubscription && this.type != ProductQueryType.Subs) { + throw ArgumentError('type must be SUBS when requestSubscription is provided'); + } + } - /// Android-specific purchase parameters - final RequestPurchaseAndroidProps? android; - /// iOS-specific purchase parameters - final RequestPurchaseIosProps? ios; + final RequestPurchasePropsRequest request; + final ProductQueryType type; factory RequestPurchaseProps.fromJson(Map json) { - return RequestPurchaseProps( - android: json['android'] != null ? RequestPurchaseAndroidProps.fromJson(json['android'] as Map) : null, - ios: json['ios'] != null ? RequestPurchaseIosProps.fromJson(json['ios'] as Map) : null, - ); + final typeValue = json['type'] as String?; + final parsedType = typeValue != null ? ProductQueryType.fromJson(typeValue) : null; + final purchaseJson = json['requestPurchase'] as Map?; + if (purchaseJson != null) { + final request = RequestPurchasePropsRequestPurchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson)); + final finalType = parsedType ?? ProductQueryType.InApp; + if (finalType != ProductQueryType.InApp) { + throw ArgumentError('type must be IN_APP when requestPurchase is provided'); + } + return RequestPurchaseProps(request: request, type: finalType); + } + final subscriptionJson = json['requestSubscription'] as Map?; + if (subscriptionJson != null) { + final request = RequestPurchasePropsRequestSubscription(RequestSubscriptionPropsByPlatforms.fromJson(subscriptionJson)); + final finalType = parsedType ?? ProductQueryType.Subs; + if (finalType != ProductQueryType.Subs) { + throw ArgumentError('type must be SUBS when requestSubscription is provided'); + } + return RequestPurchaseProps(request: request, type: finalType); + } + throw ArgumentError('RequestPurchaseProps requires requestPurchase or requestSubscription'); } Map toJson() { - return { - 'android': android?.toJson(), - 'ios': ios?.toJson(), - }; + if (request is RequestPurchasePropsRequestPurchase) { + return { + 'requestPurchase': (request as RequestPurchasePropsRequestPurchase).value.toJson(), + 'type': type.toJson(), + }; + } + if (request is RequestPurchasePropsRequestSubscription) { + return { + 'requestSubscription': (request as RequestPurchasePropsRequestSubscription).value.toJson(), + 'type': type.toJson(), + }; + } + throw StateError('Unsupported RequestPurchaseProps request variant'); + } + + static RequestPurchaseProps inApp({required RequestPurchasePropsByPlatforms request}) { + return RequestPurchaseProps(request: RequestPurchasePropsRequestPurchase(request), type: ProductQueryType.InApp); + } + + static RequestPurchaseProps subs({required RequestSubscriptionPropsByPlatforms request}) { + return RequestPurchaseProps(request: RequestPurchasePropsRequestSubscription(request), type: ProductQueryType.Subs); } } +sealed class RequestPurchasePropsRequest { + const RequestPurchasePropsRequest(); +} + +class RequestPurchasePropsRequestPurchase extends RequestPurchasePropsRequest { + const RequestPurchasePropsRequestPurchase(this.value); + final RequestPurchasePropsByPlatforms value; +} + +class RequestPurchasePropsRequestSubscription extends RequestPurchasePropsRequest { + const RequestPurchasePropsRequestSubscription(this.value); + final RequestSubscriptionPropsByPlatforms value; +} + class RequestPurchasePropsByPlatforms { const RequestPurchasePropsByPlatforms({ /// Android-specific purchase parameters @@ -2444,7 +2463,7 @@ class RequestSubscriptionPropsByPlatforms { // MARK: - Unions -sealed class Product { +sealed class Product implements ProductCommon { const Product(); factory Product.fromJson(Map json) { @@ -2458,10 +2477,31 @@ sealed class Product { throw ArgumentError('Unknown __typename for Product: $typeName'); } + @override + String get currency; + @override + String? get debugDescription; + @override + String get description; + @override + String? get displayName; + @override + String get displayPrice; + @override + String get id; + @override + IapPlatform get platform; + @override + double? get price; + @override + String get title; + @override + ProductType get type; + Map toJson(); } -sealed class ProductSubscription { +sealed class ProductSubscription implements ProductCommon { const ProductSubscription(); factory ProductSubscription.fromJson(Map json) { @@ -2475,10 +2515,31 @@ sealed class ProductSubscription { throw ArgumentError('Unknown __typename for ProductSubscription: $typeName'); } + @override + String get currency; + @override + String? get debugDescription; + @override + String get description; + @override + String? get displayName; + @override + String get displayPrice; + @override + String get id; + @override + IapPlatform get platform; + @override + double? get price; + @override + String get title; + @override + ProductType get type; + Map toJson(); } -sealed class Purchase { +sealed class Purchase implements PurchaseCommon { const Purchase(); factory Purchase.fromJson(Map json) { @@ -2492,6 +2553,26 @@ sealed class Purchase { throw ArgumentError('Unknown __typename for Purchase: $typeName'); } + @override + String get id; + @override + List? get ids; + @override + bool get isAutoRenewing; + @override + IapPlatform get platform; + @override + String get productId; + @override + PurchaseState get purchaseState; + /// Unified purchase token (iOS JWS, Android purchaseToken) + @override + String? get purchaseToken; + @override + int get quantity; + @override + double get transactionDate; + Map toJson(); } @@ -2547,7 +2628,7 @@ abstract class MutationResolver { Future presentCodeRedemptionSheetIOS(); /// Initiate a purchase flow; rely on events for final state Future requestPurchase({ - required PurchaseParams params, + required RequestPurchaseProps params, }); /// Purchase the promoted product surfaced by the App Store Future requestPurchaseOnPromotedProductIOS(); diff --git a/src/generated/types.ts b/src/generated/types.ts index 56130f2..49b785d 100644 --- a/src/generated/types.ts +++ b/src/generated/types.ts @@ -196,7 +196,7 @@ export interface MutationFinishTransactionArgs { export interface MutationRequestPurchaseArgs { - params: PurchaseParams; + params: RequestPurchaseProps; } @@ -436,15 +436,6 @@ export interface PurchaseOptions { onlyIncludeActiveItemsIOS?: (boolean | null); } -export interface PurchaseParams { - /** Per-platform purchase request props */ - requestPurchase?: (RequestPurchasePropsByPlatforms | null); - /** Per-platform subscription request props */ - requestSubscription?: (RequestSubscriptionPropsByPlatforms | null); - /** Explicit purchase type hint (defaults to in-app) */ - type?: (ProductQueryType | null); -} - export type PurchaseState = 'deferred' | 'failed' | 'pending' | 'purchased' | 'restored' | 'unknown'; export interface Query { @@ -613,12 +604,17 @@ export interface RequestPurchaseIosProps { withOffer?: (DiscountOfferInputIOS | null); } -export interface RequestPurchaseProps { - /** Android-specific purchase parameters */ - android?: (RequestPurchaseAndroidProps | null); - /** iOS-specific purchase parameters */ - ios?: (RequestPurchaseIosProps | null); -} +export type RequestPurchaseProps = + | { + /** Per-platform purchase request props */ + request: RequestPurchasePropsByPlatforms; + type: 'in-app'; + } + | { + /** Per-platform subscription request props */ + request: RequestSubscriptionPropsByPlatforms; + type: 'subs'; + }; export interface RequestPurchasePropsByPlatforms { /** Android-specific purchase parameters */ diff --git a/src/type.graphql b/src/type.graphql index ddc1718..b227f4b 100644 --- a/src/type.graphql +++ b/src/type.graphql @@ -106,7 +106,7 @@ input PurchaseOptions { } # Parameters for requestPurchase -input PurchaseParams { +input RequestPurchaseProps { """ Per-platform purchase request props """ @@ -147,17 +147,6 @@ input DeepLinkOptions { } # Request props (platform-specific containers) -input RequestPurchaseProps { - """ - iOS-specific purchase parameters - """ - ios: RequestPurchaseIosProps - """ - Android-specific purchase parameters - """ - android: RequestPurchaseAndroidProps -} - input RequestPurchasePropsByPlatforms { """ iOS-specific purchase parameters