From 0f76398ee027eefb67e905f6de2bb1930d51672b Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:09:17 +0900 Subject: [PATCH 01/16] feat(gql): add offerToken to RequestPurchaseAndroidProps Add offerToken field to RequestPurchaseAndroidProps to enable purchasing one-time products with discount offers (Android 7.0+). This allows passing the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers to apply a discount offer to the purchase. Co-Authored-By: Claude Opus 4.5 --- packages/gql/src/type-android.graphql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql index 128c6875..cde79457 100644 --- a/packages/gql/src/type-android.graphql +++ b/packages/gql/src/type-android.graphql @@ -365,6 +365,12 @@ input RequestPurchaseAndroidProps { """ isOfferPersonalized: Boolean """ + Offer token for one-time purchase discounts (Android 7.0+). + Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers + to apply a discount offer to the purchase. + """ + offerToken: String + """ Developer billing option parameters for external payments flow (8.3.0+). When provided, the purchase flow will show a side-by-side choice between Google Play Billing and the developer's external payment option. From aca203b72373f63b2062a91837115c43172a792e Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:09:21 +0900 Subject: [PATCH 02/16] chore(gql): regenerate types for all platforms Regenerate TypeScript, Swift, Kotlin, Dart, GDScript types from updated GraphQL schema with offerToken field. Co-Authored-By: Claude Opus 4.5 --- packages/gql/src/generated/Types.kt | 9 +++++++++ packages/gql/src/generated/Types.swift | 6 ++++++ packages/gql/src/generated/types.dart | 7 +++++++ packages/gql/src/generated/types.gd | 6 ++++++ packages/gql/src/generated/types.ts | 6 ++++++ 5 files changed, 34 insertions(+) diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index b16f4bda..4c03e42b 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3592,6 +3592,12 @@ public data class RequestPurchaseAndroidProps( * Obfuscated profile ID */ val obfuscatedProfileIdAndroid: String? = null, + /** + * Offer token for one-time purchase discounts (Android 7.0+). + * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers + * to apply a discount offer to the purchase. + */ + val offerToken: String? = null, /** * List of product SKUs */ @@ -3603,6 +3609,7 @@ public data class RequestPurchaseAndroidProps( val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String + val offerToken = json["offerToken"] as? String val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } if (skus == null) return null return RequestPurchaseAndroidProps( @@ -3610,6 +3617,7 @@ public data class RequestPurchaseAndroidProps( isOfferPersonalized = isOfferPersonalized, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, + offerToken = offerToken, skus = skus, ) } @@ -3620,6 +3628,7 @@ public data class RequestPurchaseAndroidProps( "isOfferPersonalized" to isOfferPersonalized, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, + "offerToken" to offerToken, "skus" to skus, ) } diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 19407cb1..9ef6de02 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1389,6 +1389,10 @@ public struct RequestPurchaseAndroidProps: Codable { public var obfuscatedAccountIdAndroid: String? /// Obfuscated profile ID public var obfuscatedProfileIdAndroid: String? + /// Offer token for one-time purchase discounts (Android 7.0+). + /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers + /// to apply a discount offer to the purchase. + public var offerToken: String? /// List of product SKUs public var skus: [String] @@ -1397,12 +1401,14 @@ public struct RequestPurchaseAndroidProps: Codable { isOfferPersonalized: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, + offerToken: String? = nil, skus: [String] ) { self.developerBillingOption = developerBillingOption self.isOfferPersonalized = isOfferPersonalized self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid + self.offerToken = offerToken self.skus = skus } } diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index b873b3ef..c6822733 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3604,6 +3604,7 @@ class RequestPurchaseAndroidProps { this.isOfferPersonalized, this.obfuscatedAccountIdAndroid, this.obfuscatedProfileIdAndroid, + this.offerToken, required this.skus, }); @@ -3617,6 +3618,10 @@ class RequestPurchaseAndroidProps { final String? obfuscatedAccountIdAndroid; /// Obfuscated profile ID final String? obfuscatedProfileIdAndroid; + /// Offer token for one-time purchase discounts (Android 7.0+). + /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers + /// to apply a discount offer to the purchase. + final String? offerToken; /// List of product SKUs final List skus; @@ -3626,6 +3631,7 @@ class RequestPurchaseAndroidProps { isOfferPersonalized: json['isOfferPersonalized'] as bool?, obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, + offerToken: json['offerToken'] as String?, skus: (json['skus'] as List).map((e) => e as String).toList(), ); } @@ -3636,6 +3642,7 @@ class RequestPurchaseAndroidProps { 'isOfferPersonalized': isOfferPersonalized, 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, + 'offerToken': offerToken, 'skus': skus, }; } diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 3642e4f7..a61ffd23 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -3200,6 +3200,8 @@ class RequestPurchaseAndroidProps: var obfuscated_profile_id_android: String ## Personalized offer flag var is_offer_personalized: bool + ## Offer token for one-time purchase discounts (Android 7.0+). + var offer_token: String ## Developer billing option parameters for external payments flow (8.3.0+). var developer_billing_option: DeveloperBillingOptionParamsAndroid @@ -3213,6 +3215,8 @@ class RequestPurchaseAndroidProps: obj.obfuscated_profile_id_android = data["obfuscatedProfileIdAndroid"] if data.has("isOfferPersonalized") and data["isOfferPersonalized"] != null: obj.is_offer_personalized = data["isOfferPersonalized"] + if data.has("offerToken") and data["offerToken"] != null: + 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"]) @@ -3230,6 +3234,8 @@ class RequestPurchaseAndroidProps: dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android if is_offer_personalized != null: dict["isOfferPersonalized"] = is_offer_personalized + if offer_token != null: + dict["offerToken"] = offer_token if developer_billing_option != null: if developer_billing_option.has_method("to_dict"): dict["developerBillingOption"] = developer_billing_option.to_dict() diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index c646d5bc..c4cb98bc 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1200,6 +1200,12 @@ export interface RequestPurchaseAndroidProps { obfuscatedAccountIdAndroid?: (string | null); /** Obfuscated profile ID */ obfuscatedProfileIdAndroid?: (string | null); + /** + * Offer token for one-time purchase discounts (Android 7.0+). + * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers + * to apply a discount offer to the purchase. + */ + offerToken?: (string | null); /** List of product SKUs */ skus: string[]; } From 6a9a739f9449977fe2675571baa95a690c17c04a Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:09:25 +0900 Subject: [PATCH 03/16] chore(apple): sync generated types from gql Sync auto-generated Types.swift with updated GraphQL schema containing offerToken field. Co-Authored-By: Claude Opus 4.5 --- packages/apple/Sources/Models/Types.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 19407cb1..9ef6de02 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1389,6 +1389,10 @@ public struct RequestPurchaseAndroidProps: Codable { public var obfuscatedAccountIdAndroid: String? /// Obfuscated profile ID public var obfuscatedProfileIdAndroid: String? + /// Offer token for one-time purchase discounts (Android 7.0+). + /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers + /// to apply a discount offer to the purchase. + public var offerToken: String? /// List of product SKUs public var skus: [String] @@ -1397,12 +1401,14 @@ public struct RequestPurchaseAndroidProps: Codable { isOfferPersonalized: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, + offerToken: String? = nil, skus: [String] ) { self.developerBillingOption = developerBillingOption self.isOfferPersonalized = isOfferPersonalized self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid + self.offerToken = offerToken self.skus = skus } } From 1b361d331df153aac910b780fcc92b8086ac733c Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:09:31 +0900 Subject: [PATCH 04/16] feat(google): implement offerToken for one-time purchase discounts - Add offerToken to AndroidPurchaseArgs in Play and Horizon helpers - Pass offerToken from RequestPurchaseAndroidProps to purchase flow - Add offerToken handling in BillingFlowParams for InApp products - Validate offer token against available one-time purchase offers - Log warning for Horizon SDK (limited discount offer support) Co-Authored-By: Claude Opus 4.5 --- .../java/dev/hyo/openiap/OpenIapModule.kt | 7 ++ .../dev/hyo/openiap/helpers/SharedHelpers.kt | 3 + .../src/main/java/dev/hyo/openiap/Types.kt | 81 ++++++++++--------- .../java/dev/hyo/openiap/OpenIapModule.kt | 17 ++++ .../java/dev/hyo/openiap/helpers/Helpers.kt | 3 + 5 files changed, 75 insertions(+), 36 deletions(-) diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index 4aaf4857..c155ad4e 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -426,6 +426,13 @@ class OpenIapModule( } builder.setOfferToken(resolved) + } else if (androidArgs.type == ProductQueryType.InApp && !androidArgs.offerToken.isNullOrEmpty()) { + // Handle one-time purchase discount offers + // Note: Horizon SDK doesn't currently support one-time purchase discount offers, + // but we pass the offer token through in case future SDK versions add support. + OpenIapLog.d("Setting offer token for one-time product ${productDetails.productId}: ${androidArgs.offerToken}", TAG) + OpenIapLog.w("Note: Horizon SDK may not support one-time purchase discount offers", TAG) + builder.setOfferToken(androidArgs.offerToken) } paramsList += builder.build() diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt index faf6a2f5..e97bebf7 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt @@ -55,6 +55,7 @@ internal data class AndroidPurchaseArgs( val isOfferPersonalized: Boolean?, val obfuscatedAccountId: String?, val obfuscatedProfileId: String?, + val offerToken: String?, val purchaseTokenAndroid: String?, val replacementModeAndroid: Int?, val subscriptionOffers: List?, @@ -77,6 +78,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { isOfferPersonalized = params.isOfferPersonalized, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, + offerToken = params.offerToken, purchaseTokenAndroid = null, replacementModeAndroid = null, subscriptionOffers = null, @@ -99,6 +101,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { isOfferPersonalized = params.isOfferPersonalized, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, + offerToken = null, purchaseTokenAndroid = params.purchaseTokenAndroid, replacementModeAndroid = params.replacementModeAndroid, subscriptionOffers = params.subscriptionOffers, 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 3224cdec..3cb7aaf3 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 @@ -3187,8 +3187,8 @@ public data class AndroidSubscriptionOfferInput( val sku = json["sku"] as? String if (offerToken == null || sku == null) return null return AndroidSubscriptionOfferInput( - offerToken = offerToken!!, - sku = sku!!, + offerToken = offerToken, + sku = sku, ) } } @@ -3250,9 +3250,9 @@ public data class DeveloperBillingOptionParamsAndroid( val linkUri = json["linkUri"] as? String if (billingProgram == null || launchMode == null || linkUri == null) return null return DeveloperBillingOptionParamsAndroid( - billingProgram = billingProgram!!, - launchMode = launchMode!!, - linkUri = linkUri!!, + billingProgram = billingProgram, + launchMode = launchMode, + linkUri = linkUri, ) } } @@ -3295,11 +3295,11 @@ public data class DiscountOfferInputIOS( val timestamp = (json["timestamp"] as? Number)?.toDouble() if (identifier == null || keyIdentifier == null || nonce == null || signature == null || timestamp == null) return null return DiscountOfferInputIOS( - identifier = identifier!!, - keyIdentifier = keyIdentifier!!, - nonce = nonce!!, - signature = signature!!, - timestamp = timestamp!!, + identifier = identifier, + keyIdentifier = keyIdentifier, + nonce = nonce, + signature = signature, + timestamp = timestamp, ) } } @@ -3380,10 +3380,10 @@ public data class LaunchExternalLinkParamsAndroid( val linkUri = json["linkUri"] as? String if (billingProgram == null || launchMode == null || linkType == null || linkUri == null) return null return LaunchExternalLinkParamsAndroid( - billingProgram = billingProgram!!, - launchMode = launchMode!!, - linkType = linkType!!, - linkUri = linkUri!!, + billingProgram = billingProgram, + launchMode = launchMode, + linkType = linkType, + linkUri = linkUri, ) } } @@ -3406,7 +3406,7 @@ public data class ProductRequest( val type = (json["type"] as? String)?.let { ProductQueryType.fromJson(it) } if (skus == null) return null return ProductRequest( - skus = skus!!, + skus = skus, type = type, ) } @@ -3442,8 +3442,8 @@ public data class PromotionalOfferJWSInputIOS( val offerId = json["offerId"] as? String if (jws == null || offerId == null) return null return PromotionalOfferJWSInputIOS( - jws = jws!!, - offerId = offerId!!, + jws = jws, + offerId = offerId, ) } } @@ -3509,6 +3509,12 @@ public data class RequestPurchaseAndroidProps( * Obfuscated profile ID */ val obfuscatedProfileIdAndroid: String? = null, + /** + * Offer token for one-time purchase discounts (Android 7.0+). + * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers + * to apply a discount offer to the purchase. + */ + val offerToken: String? = null, /** * List of product SKUs */ @@ -3520,6 +3526,7 @@ public data class RequestPurchaseAndroidProps( val isOfferPersonalized = json["isOfferPersonalized"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String + val offerToken = json["offerToken"] as? String val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } if (skus == null) return null return RequestPurchaseAndroidProps( @@ -3527,7 +3534,8 @@ public data class RequestPurchaseAndroidProps( isOfferPersonalized = isOfferPersonalized, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, - skus = skus!!, + offerToken = offerToken, + skus = skus, ) } } @@ -3537,6 +3545,7 @@ public data class RequestPurchaseAndroidProps( "isOfferPersonalized" to isOfferPersonalized, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, + "offerToken" to offerToken, "skus" to skus, ) } @@ -3585,7 +3594,7 @@ public data class RequestPurchaseIosProps( andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically, appAccountToken = appAccountToken, quantity = quantity, - sku = sku!!, + sku = sku, withOffer = withOffer, ) } @@ -3750,7 +3759,7 @@ public data class RequestSubscriptionAndroidProps( val purchaseTokenAndroid = json["purchaseTokenAndroid"] as? String val replacementModeAndroid = (json["replacementModeAndroid"] as? Number)?.toInt() val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } - val subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AndroidSubscriptionOfferInput.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for AndroidSubscriptionOfferInput") } + 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 return RequestSubscriptionAndroidProps( @@ -3760,7 +3769,7 @@ public data class RequestSubscriptionAndroidProps( obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, purchaseTokenAndroid = purchaseTokenAndroid, replacementModeAndroid = replacementModeAndroid, - skus = skus!!, + skus = skus, subscriptionOffers = subscriptionOffers, subscriptionProductReplacementParams = subscriptionProductReplacementParams, ) @@ -3837,7 +3846,7 @@ public data class RequestSubscriptionIosProps( introductoryOfferEligibility = introductoryOfferEligibility, promotionalOfferJWS = promotionalOfferJWS, quantity = quantity, - sku = sku!!, + sku = sku, winBackOffer = winBackOffer, withOffer = withOffer, ) @@ -3913,7 +3922,7 @@ public data class RequestVerifyPurchaseWithIapkitAppleProps( val jws = json["jws"] as? String if (jws == null) return null return RequestVerifyPurchaseWithIapkitAppleProps( - jws = jws!!, + jws = jws, ) } } @@ -3934,7 +3943,7 @@ public data class RequestVerifyPurchaseWithIapkitGoogleProps( val purchaseToken = json["purchaseToken"] as? String if (purchaseToken == null) return null return RequestVerifyPurchaseWithIapkitGoogleProps( - purchaseToken = purchaseToken!!, + purchaseToken = purchaseToken, ) } } @@ -4002,8 +4011,8 @@ public data class SubscriptionProductReplacementParamsAndroid( val replacementMode = (json["replacementMode"] as? String)?.let { SubscriptionReplacementModeAndroid.fromJson(it) } ?: SubscriptionReplacementModeAndroid.UnknownReplacementMode if (oldProductId == null || replacementMode == null) return null return SubscriptionProductReplacementParamsAndroid( - oldProductId = oldProductId!!, - replacementMode = replacementMode!!, + oldProductId = oldProductId, + replacementMode = replacementMode, ) } } @@ -4029,7 +4038,7 @@ public data class VerifyPurchaseAppleOptions( val sku = json["sku"] as? String if (sku == null) return null return VerifyPurchaseAppleOptions( - sku = sku!!, + sku = sku, ) } } @@ -4078,11 +4087,11 @@ public data class VerifyPurchaseGoogleOptions( val sku = json["sku"] as? String if (accessToken == null || packageName == null || purchaseToken == null || sku == null) return null return VerifyPurchaseGoogleOptions( - accessToken = accessToken!!, + accessToken = accessToken, isSub = isSub, - packageName = packageName!!, - purchaseToken = purchaseToken!!, - sku = sku!!, + packageName = packageName, + purchaseToken = purchaseToken, + sku = sku, ) } } @@ -4125,9 +4134,9 @@ public data class VerifyPurchaseHorizonOptions( val userId = json["userId"] as? String if (accessToken == null || sku == null || userId == null) return null return VerifyPurchaseHorizonOptions( - accessToken = accessToken!!, - sku = sku!!, - userId = userId!!, + accessToken = accessToken, + sku = sku, + userId = userId, ) } } @@ -4188,7 +4197,7 @@ public data class VerifyPurchaseWithProviderProps( if (provider == null) return null return VerifyPurchaseWithProviderProps( iapkit = iapkit, - provider = provider!!, + provider = provider, ) } } @@ -4216,7 +4225,7 @@ public data class WinBackOfferInputIOS( val offerId = json["offerId"] as? String if (offerId == null) return null return WinBackOfferInputIOS( - offerId = offerId!!, + offerId = offerId, ) } } 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 bb33a16c..1cd30cd8 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 @@ -892,6 +892,23 @@ class OpenIapModule( applySubscriptionProductReplacementParams(builder, replacementParams) } } + } else if (androidArgs.type == ProductQueryType.InApp && !androidArgs.offerToken.isNullOrEmpty()) { + // Handle one-time purchase discount offers (Android 7.0+) + OpenIapLog.d("Setting offer token for one-time product ${productDetails.productId}: ${androidArgs.offerToken}", TAG) + + // Validate offer token exists in available one-time purchase offers + val oneTimePurchaseOffer = productDetails.oneTimePurchaseOfferDetails + val availableTokens = listOfNotNull(oneTimePurchaseOffer?.offerToken) + + if (availableTokens.isNotEmpty() && !availableTokens.contains(androidArgs.offerToken)) { + OpenIapLog.w("Invalid one-time offer token: ${androidArgs.offerToken} not in $availableTokens", TAG) + val err = OpenIapError.SkuOfferMismatch + for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } + currentPurchaseCallback?.invoke(Result.success(emptyList())) + return + } + + builder.setOfferToken(androidArgs.offerToken) } paramsList += builder.build() diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt index 87f4e406..af7f8cfa 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt @@ -118,6 +118,7 @@ internal data class AndroidPurchaseArgs( val isOfferPersonalized: Boolean?, val obfuscatedAccountId: String?, val obfuscatedProfileId: String?, + val offerToken: String?, val purchaseTokenAndroid: String?, val replacementModeAndroid: Int?, val subscriptionOffers: List?, @@ -138,6 +139,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { isOfferPersonalized = params.isOfferPersonalized, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, + offerToken = params.offerToken, purchaseTokenAndroid = null, replacementModeAndroid = null, subscriptionOffers = null, @@ -161,6 +163,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { isOfferPersonalized = params.isOfferPersonalized, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, + offerToken = null, purchaseTokenAndroid = params.purchaseTokenAndroid, replacementModeAndroid = params.replacementModeAndroid, subscriptionOffers = params.subscriptionOffers, From d404aca76c0085280aa2e90def000fd24d10739b Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:09:38 +0900 Subject: [PATCH 05/16] test(google): add unit tests for offerToken in one-time purchases Add 6 new test cases for RequestPurchaseAndroidProps offerToken: - Supports offerToken for one-time purchases - toJson includes offerToken - fromJson parses offerToken - Allows null offerToken - DiscountOffer offerTokenAndroid can be used for purchase - ProductAndroid discountOffers can provide offerToken for purchase Co-Authored-By: Claude Opus 4.5 --- .../hyo/openiap/StandardizedOfferTypesTest.kt | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt index 90233ba3..e5befd62 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt @@ -404,4 +404,123 @@ class StandardizedOfferTypesTest { assertEquals("sub_intro", product.subscriptionOffers.first().id) assertEquals(PaymentMode.FreeTrial, product.subscriptionOffers.first().paymentMode) } + + // MARK: - RequestPurchaseAndroidProps offerToken Tests + + @Test + fun `RequestPurchaseAndroidProps supports offerToken for one-time purchases`() { + val props = RequestPurchaseAndroidProps( + skus = listOf("premium_upgrade"), + offerToken = "discount_offer_token_abc123" + ) + + assertEquals(listOf("premium_upgrade"), props.skus) + assertEquals("discount_offer_token_abc123", props.offerToken) + assertNull(props.isOfferPersonalized) + assertNull(props.obfuscatedAccountIdAndroid) + } + + @Test + fun `RequestPurchaseAndroidProps toJson includes offerToken`() { + val props = RequestPurchaseAndroidProps( + skus = listOf("product_id"), + offerToken = "test_offer_token", + isOfferPersonalized = true + ) + + val json = props.toJson() + assertEquals(listOf("product_id"), json["skus"]) + assertEquals("test_offer_token", json["offerToken"]) + assertEquals(true, json["isOfferPersonalized"]) + } + + @Test + fun `RequestPurchaseAndroidProps fromJson parses offerToken`() { + val json = mapOf( + "skus" to listOf("sku_001"), + "offerToken" to "parsed_offer_token", + "obfuscatedAccountIdAndroid" to "account_123" + ) + + val props = RequestPurchaseAndroidProps.fromJson(json) + assertEquals(listOf("sku_001"), props?.skus) + assertEquals("parsed_offer_token", props?.offerToken) + assertEquals("account_123", props?.obfuscatedAccountIdAndroid) + } + + @Test + fun `RequestPurchaseAndroidProps allows null offerToken`() { + val props = RequestPurchaseAndroidProps( + skus = listOf("regular_product") + ) + + assertNull(props.offerToken) + + val json = props.toJson() + assertNull(json["offerToken"]) + } + + @Test + fun `DiscountOffer offerTokenAndroid can be used for purchase`() { + // Simulate fetching a product with discount offer + val discountOffer = DiscountOffer( + id = "summer_sale", + displayPrice = "$4.99", + price = 4.99, + currency = "USD", + type = DiscountOfferType.OneTime, + offerTokenAndroid = "summer_sale_offer_token_xyz", + percentageDiscountAndroid = 50 + ) + + // Create purchase props using the offer token from the discount offer + val purchaseProps = RequestPurchaseAndroidProps( + skus = listOf("premium_upgrade"), + offerToken = discountOffer.offerTokenAndroid + ) + + assertEquals("summer_sale_offer_token_xyz", purchaseProps.offerToken) + assertEquals(discountOffer.offerTokenAndroid, purchaseProps.offerToken) + } + + @Test + fun `ProductAndroid discountOffers can provide offerToken for purchase`() { + val discountOffer = DiscountOffer( + id = "flash_sale", + displayPrice = "$2.99", + price = 2.99, + currency = "USD", + type = DiscountOfferType.OneTime, + offerTokenAndroid = "flash_sale_token" + ) + + val product = ProductAndroid( + id = "consumable_gems", + title = "100 Gems", + description = "A pack of 100 gems", + displayName = "Gems Pack", + displayPrice = "$4.99", + price = 4.99, + currency = "USD", + platform = IapPlatform.Android, + type = ProductType.InApp, + nameAndroid = "100 Gems", + discountOffers = listOf(discountOffer), + subscriptionOffers = null, + oneTimePurchaseOfferDetailsAndroid = null, + subscriptionOfferDetailsAndroid = null + ) + + // Get the offer token from product's discount offers + val offerToken = product.discountOffers?.firstOrNull()?.offerTokenAndroid + + // Create purchase request with the offer token + val purchaseProps = RequestPurchaseAndroidProps( + skus = listOf(product.id), + offerToken = offerToken + ) + + assertEquals("consumable_gems", purchaseProps.skus.first()) + assertEquals("flash_sale_token", purchaseProps.offerToken) + } } From a275c05822db8644198f330654d67d42c76cf8fb Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:09:44 +0900 Subject: [PATCH 06/16] docs(google): add one-time purchase discount offers documentation Document offerToken usage for one-time purchase discounts: - Add section for One-Time Purchase Discount Offers (Android 7.0+) - Include basic usage example with fetchProducts and discountOffers - Add advanced example using RequestPurchaseProps directly - Note requirements for Billing Library 7.0+ and Play Console setup Co-Authored-By: Claude Opus 4.5 --- packages/google/README.md | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/google/README.md b/packages/google/README.md index f3846edb..e536d7d1 100644 --- a/packages/google/README.md +++ b/packages/google/README.md @@ -379,6 +379,63 @@ openIAP.requestPurchase( ) ``` +### One-Time Purchase Discount Offers (Android 7.0+) + +Google Play Billing Library 7.0+ supports discount offers for one-time (in-app) purchases. You can fetch products with discount information and apply them during purchase. + +```kotlin +// 1. Fetch products - discount offers are included automatically +val products = openIAP.fetchProducts(listOf("premium_upgrade")) +val product = products.firstOrNull() + +// 2. Check for available discount offers +val discountOffer = product?.discountOffers?.firstOrNull() + +if (discountOffer != null) { + // Display discount info to user + Log.d("IAP", "Discount available: ${discountOffer.displayPrice}") + Log.d("IAP", "Original price: ${discountOffer.fullPriceMicrosAndroid}") + Log.d("IAP", "Discount: ${discountOffer.percentageDiscountAndroid}%") + + // 3. Purchase with discount offer token + openIAP.requestPurchase( + activity = this, + sku = "premium_upgrade", + offerToken = discountOffer.offerTokenAndroid + ) +} else { + // Purchase at regular price (no offerToken) + openIAP.requestPurchase( + activity = this, + sku = "premium_upgrade" + ) +} +``` + +#### Using RequestPurchaseProps + +For more control, use `RequestPurchaseProps` directly: + +```kotlin +val purchaseResult = openIAP.requestPurchase( + props = RequestPurchaseProps( + request = RequestPurchaseProps.Request.Purchase( + RequestPurchasePropsByPlatforms( + google = RequestPurchaseAndroidProps( + skus = listOf("premium_upgrade"), + offerToken = discountOffer?.offerTokenAndroid, + isOfferPersonalized = true // Required in EU if offer is personalized + ) + ) + ), + type = ProductQueryType.InApp + ), + activity = this +) +``` + +> **Note**: One-time purchase discount offers require Google Play Billing Library 7.0+ and must be configured in Google Play Console. + ## ⚠️ Important Notes - This library requires Google Play Billing Library v8 From 7cf4473cabb3499fd1c5c47f4dc2a7bd96fba2c8 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:10:41 +0900 Subject: [PATCH 07/16] docs: add offerToken documentation for one-time purchase discounts - Add offerToken field to RequestPurchaseAndroidProps docs (types/request.tsx) - Fix requestPurchase examples in discount.tsx to use correct API format - Update TypeScript and Kotlin examples with proper parameter structure Co-Authored-By: Claude Opus 4.5 --- .../docs/src/pages/docs/features/discount.tsx | 23 +++++++++++-------- .../docs/src/pages/docs/types/request.tsx | 11 +++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx index a428fc9a..dafe5c62 100644 --- a/packages/docs/src/pages/docs/features/discount.tsx +++ b/packages/docs/src/pages/docs/features/discount.tsx @@ -853,11 +853,13 @@ async function purchaseWithOffer( const selectedOffer = offers[offerIndex]; await requestPurchase({ - type: 'inapp', + type: 'in-app', request: { - skus: [product.id], - // Include offerToken for discounted purchases - offerToken: selectedOffer.offerToken, + google: { + skus: [product.id], + // Include offerToken for discounted purchases (Android 7.0+) + offerToken: selectedOffer.offerToken, + }, }, }); }`} @@ -881,11 +883,14 @@ async function purchaseWithOffer( iapStore.requestPurchase( activity = activity, props = RequestPurchaseProps( - type = "inapp", - request = RequestPurchasePropsByPlatforms( - android = RequestPurchaseAndroidProps( - skus = listOf(product.id), - offerToken = selectedOffer.offerToken + type = ProductQueryType.InApp, + request = RequestPurchaseProps.Request.Purchase( + RequestPurchasePropsByPlatforms( + google = RequestPurchaseAndroidProps( + skus = listOf(product.id), + // Include offerToken for discounted purchases (Android 7.0+) + offerToken = selectedOffer.offerToken + ) ) ) ) diff --git a/packages/docs/src/pages/docs/types/request.tsx b/packages/docs/src/pages/docs/types/request.tsx index 8514ceef..cde51d18 100644 --- a/packages/docs/src/pages/docs/types/request.tsx +++ b/packages/docs/src/pages/docs/types/request.tsx @@ -523,6 +523,17 @@ await iap.request_purchase(subs_props)`} Array of product identifiers (required) + + + offerToken + + + Offer token for one-time purchase discounts (Android 7.0+). + Pass the offerToken from{' '} + oneTimePurchaseOfferDetailsAndroid or{' '} + discountOffers to apply a discount. + + obfuscatedAccountIdAndroid From c43e2575a80a4554e37a737c764e6d9f4b4e52b8 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:21:18 +0900 Subject: [PATCH 08/16] docs: update compiled knowledge base Regenerate context.md, llms.txt, and llms-full.txt with updated offerToken documentation. Co-Authored-By: Claude Opus 4.5 --- knowledge/_claude-context/context.md | 85 +++++++++++++++++++++++++--- packages/docs/public/llms-full.txt | 2 +- packages/docs/public/llms.txt | 2 +- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index bef00f74..74a77c20 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -1,7 +1,7 @@ # OpenIAP Project Context > **Auto-generated for Claude Code** -> Last updated: 2026-01-18T13:00:35.017Z +> Last updated: 2026-01-20T00:21:08.836Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -705,6 +705,60 @@ swift test # Run tests swift build # Build package ``` +### Objective-C Bridge (CRITICAL for kmp-iap) + +**IMPORTANT**: When updating iOS functions in `OpenIapModule.swift`, you **MUST** also update `OpenIapModule+ObjC.swift`. + +The Objective-C bridge (`OpenIapModule+ObjC.swift`) exposes Swift async functions to Objective-C/Kotlin for: +- **kmp-iap** (Kotlin Multiplatform via cinterop) +- Any other platform that requires Objective-C interoperability + +#### When to Update ObjC Bridge + +Update `OpenIapModule+ObjC.swift` when: +- [ ] Adding new public functions to `OpenIapModule.swift` +- [ ] Changing function signatures (parameters, return types) +- [ ] Adding new input options or parameters +- [ ] Changing existing function behavior + +#### Bridge Pattern + +Every Swift async function needs an Objective-C completion handler wrapper: + +```swift +// In OpenIapModule.swift (Swift async) +public func newFeatureIOS(param: String) async throws -> ResultType { + // implementation +} + +// In OpenIapModule+ObjC.swift (ObjC bridge - MUST ADD) +@objc func newFeatureIOSWithParam( + _ param: String, + completion: @escaping (Any?, Error?) -> Void +) { + Task { + do { + let result = try await newFeatureIOS(param: param) + let dictionary = OpenIapSerialization.encode(result) + completion(dictionary, nil) + } catch { + completion(nil, error) + } + } +} +``` + +#### Files to Update Together + +| Swift Function Changed | ObjC Bridge Required | +|------------------------|----------------------| +| `OpenIapModule.swift` | `OpenIapModule+ObjC.swift` | + +**Verification**: After updating, run: +```bash +swift build # Verifies ObjC bridge compiles +``` + --- ## Google Package (packages/google) @@ -1094,6 +1148,21 @@ This will: - **Separate versioning**: Apple and Google packages have independent versions - **Swift Package Manager**: Automatically works via Git tags, no separate deployment step +--- + +## Version File Management + +### openiap-versions.json + +**CRITICAL: NEVER manually edit `openiap-versions.json`** + +This file is automatically managed by CI/CD workflows during releases: +- Apple releases update `apple` version +- Google releases update `google` version +- GQL releases update `gql` and `docs` versions + +Manual edits will cause version conflicts and deployment issues. Always use the GitHub Actions workflows to update versions. + --- @@ -1490,15 +1559,15 @@ await endConnection(); Google Play Billing Library enables in-app purchases and subscriptions on Android devices. -## Google Play Billing Version History +## Version History | Version | Release Date | Key Features | |---------|--------------|--------------| -| 8.0 | 2025-06-30 | One-time product improvements, multiple purchase options/offers for one-time products, product-level status for unfetched products | -| 8.1 | 2025-11-06 | Minor release with bug fixes and improvements | +| 8.0 | 2025-06-30 | Auto-reconnect, product-level status codes, one-time products with multiple offers, sub-response codes | +| 8.1 | 2025-11-06 | Suspended subscriptions (`isSuspended`), `includeSuspended` parameter, pre-order details, product-level subscription replacement, `KEEP_EXISTING` mode | | 8.2 | 2025-12-09 | Billing Programs API (external content links, external offers), deprecates old External Offers API | | 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` | -| 8.3 | 2025-12-23 | External Payments program, developer billing options | +| 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options | **Current Version**: 8.3.0 (as of January 2026) @@ -2571,7 +2640,7 @@ This document provides external API reference for Apple's StoreKit 2 framework. | UI context for purchases | iOS 18.2 | Required for proper payment sheet display | | External purchase notice | iOS 18.2 | `presentExternalPurchaseNoticeSheetIOS` | | `appTransactionID` | iOS 18.4 | Globally unique app transaction identifier (back-deployed to iOS 15) | -| `originalPlatform` | iOS 18.4 | Original purchase platform | +| `originalPlatform` | iOS 18.4 | Original purchase platform (back-deployed to iOS 15) | | `Offer.Period` | iOS 18.4 | Offer period information | | `advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data | | Expanded offer codes | iOS 18.4 | For consumables/non-consumables | @@ -2762,10 +2831,8 @@ let result = try await product.purchase(confirmIn: window) ```swift let appTransaction = try await AppTransaction.shared -// appTransactionID: New in iOS 18.4 (back-deployed to iOS 15) +// New in iOS 18.4 (back-deployed to iOS 15) let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account - -// originalPlatform: New in iOS 18.4 (iOS 18.4+ only, NOT back-deployed) let originalPlatform = appTransaction.originalPlatform // Original purchase platform ``` diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index 4b8d40cf..069b0c55 100644 --- a/packages/docs/public/llms-full.txt +++ b/packages/docs/public/llms-full.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-01-18T13:00:35.102Z +> Generated: 2026-01-20T00:21:08.852Z ## Table of Contents 1. Installation diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index 3e545b9a..f2d31d37 100644 --- a/packages/docs/public/llms.txt +++ b/packages/docs/public/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-01-18T13:00:35.102Z +> Generated: 2026-01-20T00:21:08.852Z ## Installation From 628e913b6daa85c9ad4ef7ccebcb6e9ce7228256 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:25:12 +0900 Subject: [PATCH 09/16] fix(google): address PR review comments for offerToken validation - Use oneTimePurchaseOfferDetailsList instead of deprecated oneTimePurchaseOfferDetails - Add guard when no one-time purchase offers exist - Add validation for offerToken in Horizon implementation Co-Authored-By: Claude Opus 4.5 --- .../horizon/java/dev/hyo/openiap/OpenIapModule.kt | 10 ++++++++++ .../play/java/dev/hyo/openiap/OpenIapModule.kt | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index c155ad4e..44b24c2c 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -431,6 +431,16 @@ class OpenIapModule( // Note: Horizon SDK doesn't currently support one-time purchase discount offers, // but we pass the offer token through in case future SDK versions add support. OpenIapLog.d("Setting offer token for one-time product ${productDetails.productId}: ${androidArgs.offerToken}", TAG) + + // Validate offerToken format (basic sanity check) + if (androidArgs.offerToken.isBlank()) { + OpenIapLog.w("Invalid empty offerToken provided for ${productDetails.productId}", TAG) + val err = OpenIapError.SkuOfferMismatch + for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } + currentPurchaseCallback?.invoke(Result.success(emptyList())) + return + } + OpenIapLog.w("Note: Horizon SDK may not support one-time purchase discount offers", TAG) builder.setOfferToken(androidArgs.offerToken) } 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 1cd30cd8..bf05428f 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 @@ -897,10 +897,19 @@ class OpenIapModule( OpenIapLog.d("Setting offer token for one-time product ${productDetails.productId}: ${androidArgs.offerToken}", TAG) // Validate offer token exists in available one-time purchase offers - val oneTimePurchaseOffer = productDetails.oneTimePurchaseOfferDetails - val availableTokens = listOfNotNull(oneTimePurchaseOffer?.offerToken) + // Use oneTimePurchaseOfferDetailsList (Billing Library 7.0+) for discount offers + val oneTimePurchaseOffers = productDetails.oneTimePurchaseOfferDetailsList + val availableTokens = oneTimePurchaseOffers?.map { it.offerToken } ?: emptyList() - if (availableTokens.isNotEmpty() && !availableTokens.contains(androidArgs.offerToken)) { + if (availableTokens.isEmpty()) { + OpenIapLog.w("No one-time purchase offers available for ${productDetails.productId}, but offerToken was provided: ${androidArgs.offerToken}", TAG) + val err = OpenIapError.SkuOfferMismatch + for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } + currentPurchaseCallback?.invoke(Result.success(emptyList())) + return + } + + if (!availableTokens.contains(androidArgs.offerToken)) { OpenIapLog.w("Invalid one-time offer token: ${androidArgs.offerToken} not in $availableTokens", TAG) val err = OpenIapError.SkuOfferMismatch for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } From 93ec38f20c9e1e92f3488eb682c39e0e952df65c Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:25:56 +0900 Subject: [PATCH 10/16] fix(docs): add blank line before fenced code block (MD031) Co-Authored-By: Claude Opus 4.5 --- knowledge/_claude-context/context.md | 1 + 1 file changed, 1 insertion(+) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 74a77c20..11ae07df 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -755,6 +755,7 @@ public func newFeatureIOS(param: String) async throws -> ResultType { | `OpenIapModule.swift` | `OpenIapModule+ObjC.swift` | **Verification**: After updating, run: + ```bash swift build # Verifies ObjC bridge compiles ``` From bd9a1a360f41e980ffa53b254971d5dbee25da34 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 09:45:01 +0900 Subject: [PATCH 11/16] docs: simplify package READMEs, move API docs to openiap.dev - Remove detailed API documentation from packages/google/README.md - Remove detailed API documentation from packages/apple/README.md - Keep only essential info: installation, quick start, and links to docs site - All comprehensive documentation now lives at openiap.dev Co-Authored-By: Claude Opus 4.5 --- packages/apple/README.md | 362 ++-------------------------- packages/google/README.md | 487 +++----------------------------------- 2 files changed, 55 insertions(+), 794 deletions(-) diff --git a/packages/apple/README.md b/packages/apple/README.md index b2518bb9..e24a3e45 100644 --- a/packages/apple/README.md +++ b/packages/apple/README.md @@ -2,12 +2,12 @@
OpenIAP Apple Logo -
-
- A comprehensive Swift implementation of the OpenIAP specification for iOS, macOS, tvOS, and watchOS applications. +

Swift implementation of the OpenIAP specification for iOS, macOS, tvOS, and watchOS.

+
+ -
- -**OpenIAP** is a unified specification for in-app purchases across platforms, frameworks, and emerging technologies. This Apple ecosystem implementation standardizes IAP implementations to reduce fragmentation and enable consistent behavioral across all Apple platforms. - -In the AI coding era, having a unified IAP specification becomes increasingly important as developers build applications across multiple platforms and frameworks with automated tools. +## Documentation -## 🌐 Learn More +Visit [**openiap.dev**](https://openiap.dev) for complete documentation, API reference, guides, and examples. -Visit [**openiap.dev**](https://openiap.dev) for complete documentation, guides, and the full OpenIAP specification. +## Features -## ✨ Features +- StoreKit 2 support (iOS 15+) +- Cross-platform (iOS, macOS, tvOS, watchOS) +- Thread-safe with MainActor isolation +- Automatic transaction verification +- Event-driven purchase observation -- βœ… **StoreKit 2** support with full iOS 15+ compatibility -- βœ… **Cross-platform** support (iOS, macOS, tvOS, watchOS) -- βœ… **Thread-safe** operations with MainActor isolation -- βœ… **Explicit connection management** with automatic listener cleanup -- βœ… **Multiple API levels** - Use `OpenIapModule.shared` or `OpenIapStore` -- βœ… **Product management** with intelligent caching -- βœ… **Purchase handling** with automatic transaction verification - - Processes only StoreKit 2 verified transactions and emits updates. -- βœ… **Subscription management** with cancel/reactivate support - - Opens App Store manage subscriptions UI for user cancel/reactivate and detects state changes. -- βœ… **Receipt validation** and transaction security - - Provides Base64 receipt and JWS; verifies latest transaction via StoreKit and supports server-side validation. -- βœ… **Event-driven** purchase observation -- βœ… **Swift Package Manager** and **CocoaPods** support - -## πŸ“‹ Requirements +## Requirements | Platform | Minimum Version | | -------- | --------------- | @@ -57,11 +42,11 @@ Visit [**openiap.dev**](https://openiap.dev) for complete documentation, guides, | watchOS | 8.0+ | | Swift | 5.9+ | -## πŸ“¦ Installation +## Installation ### Swift Package Manager -Add OpenIAP to your `Package.swift`: +Add to your `Package.swift`: ```swift dependencies: [ @@ -69,12 +54,6 @@ dependencies: [ ] ``` -Or through Xcode: - -1. **File** β†’ **Add Package Dependencies** -2. Enter: `https://github.com/hyodotdev/openiap.git` -3. Select version and add to your target - ### CocoaPods Add to your `Podfile`: @@ -83,21 +62,9 @@ Add to your `Podfile`: pod 'openiap', '~> $version' ``` -Then run: - -```bash -pod install -``` - -> πŸ“Œ **Latest Version**: Check [`openiap-versions.json`](../../openiap-versions.json) for the current version, or see the badges above. - -## πŸš€ Quick Start +> Check [`openiap-versions.json`](../../openiap-versions.json) for the current version. -OpenIAP provides multiple ways to integrate in-app purchases, from super simple one-liners to advanced control. Choose the approach that fits your needs! - -### Option 1: Shared Instance (Simplest) - -Use `OpenIapModule.shared` for quick integration: +## Quick Start ```swift import OpenIAP @@ -112,301 +79,18 @@ let products = try await module.fetchProducts( ProductRequest(skus: ["premium", "coins"], type: .all) ) -// Make a purchase -let purchase = try await module.requestPurchase( - let purchase = try await module.requestPurchase(RequestPurchaseProps(request: .purchase(RequestPurchasePropsByPlatforms(android: nil, ios: RequestPurchaseIosProps(andDangerouslyFinishTransactionAutomatically: nil, appAccountToken: nil, quantity: 1, sku: "premium", withOffer: nil))), type: .inApp)) -) - -// Get available/restored purchases -let restored = try await module.getAvailablePurchases(nil) - // End connection when done _ = try await module.endConnection() ``` -### Option 2: OpenIapStore (SwiftUI Ready) - -For more control while keeping it simple: - -```swift -import OpenIAP - -@MainActor -class StoreViewModel: ObservableObject { - private let iapStore: OpenIapStore - - init() { - // Setup store with event handlers - self.iapStore = OpenIapStore( - onPurchaseSuccess: { purchase in - print("Purchase successful: \(purchase.productId)") - }, - onPurchaseError: { error in - print("Purchase failed: \(error.message)") - } - ) - - Task { - // Initialize connection - try await iapStore.initConnection() - - // Fetch products - try await iapStore.fetchProducts( - skus: ["product1", "product2"], - type: .inApp - ) - } - } - - deinit { - Task { - // End connection when done - try await iapStore.endConnection() - } - } -} -``` - -### Option 3: OpenIapModule Direct (Low-level) - -For complete control over the purchase flow: - -```swift -import OpenIAP - -@MainActor -func setupStore() async throws { - let module = OpenIapModule.shared - - // Initialize connection first - _ = try await module.initConnection() - - // Setup listeners - let subscription = module.purchaseUpdatedListener { purchase in - print("Purchase updated: \(purchase.productId)") - } - - // Fetch and purchase - let request = ProductRequest(skus: ["premium"], type: .all) - let products = try await module.fetchProducts(request) - - let purchase = try await store.requestPurchase(sku: "premium") - let purchase = try await module.requestPurchase(props) - - // When done, clean up - module.removeListener(subscription) - _ = try await module.endConnection() -} -``` - -## 🎯 API Architecture - -OpenIAP now has a **simplified, minimal API** with just 2 main components: - -### Core Components - -1. **OpenIapModule** (`OpenIapModule.swift`) - - - Core StoreKit 2 implementation - - Shared instance for simple usage - - Low-level instance methods for advanced control - -2. **OpenIapStore** (`OpenIapStore.swift`) - - SwiftUI-ready with `@Published` properties - - Explicit connection management (initConnection/endConnection) - - Event callbacks for purchase success/error - - Perfect for MVVM architecture - -### Why This Design? - -- **No Duplication**: Each component has a distinct purpose -- **Flexibility**: Use the shared module or the SwiftUI store -- **Simplicity**: Only 2 files to understand instead of 4+ -- **Compatibility**: Maintains openiap.dev spec compliance - -## πŸ§ͺ Testing - -### Run Tests - -```bash -# Via Swift Package Manager -swift test - -# Via Xcode -⌘U (Product β†’ Test) -``` - -### Test with Sandbox - -1. Configure your products in **App Store Connect** -2. Create a **Sandbox Apple ID** -3. Use test card: `4242 4242 4242 4242` - -### Server-Side Validation - -OpenIAP provides comprehensive transaction verification with server-side receipt validation: - -```swift -let store = OpenIapStore() -try await store.initConnection() - -// Request purchase (validate server-side first) -let purchase = try await store.requestPurchase( - try await store.requestPurchase(sku: "dev.hyo.premium") -) - -// Validate on your server using purchase.purchaseToken -// Then finish the transaction manually -_ = try await store.finishTransaction(purchase: purchase, isConsumable: false) -``` - -## πŸ”„ Connection Management - -The library provides explicit connection management with automatic listener cleanup. - -### Key Benefits - -1. **Explicit Connection Control**: You decide when to connect and disconnect -2. **Automatic Listener Cleanup**: Listeners are cleaned up on endConnection() -3. **Built-in Event Handling**: Purchase success/error callbacks are managed for you -4. **SwiftUI Ready**: Published properties for reactive UI updates -5. **Simplified API**: All common operations with sensible defaults - -### Usage Pattern - -```swift -class StoreViewModel: ObservableObject { - private let iapStore = OpenIapStore() - - init() { - Task { - // Initialize connection - try await iapStore.initConnection() - - // Fetch products - try await iapStore.fetchProducts(skus: productIds) - } - } - - deinit { - Task { - // End connection (listeners cleaned up automatically) - try await iapStore.endConnection() - } - } -} -``` - -## πŸ“š Data Models - -Our Swift data models are generated from the shared GraphQL schema in the [`openiap` monorepo](https://github.com/hyodotdev/openiap/tree/main/packages/gql). Run `./scripts/generate-types.sh` to update `Sources/Models/Types.swift`, and every consumerβ€”including the example appβ€”should rely on those generated definitions instead of hand-written structs. - -
-ProductIOS snapshot - -```swift -struct ProductIOS { - let id: String - let title: String - let description: String - let type: ProductType - let displayPrice: String - let currency: String - let price: Double? - let platform: IapPlatform - - // iOS-specific properties - let displayNameIOS: String - let typeIOS: ProductTypeIOS - let subscriptionInfoIOS: SubscriptionInfoIOS? - let discountsIOS: [DiscountIOS]? - let isFamilyShareableIOS: Bool -} -``` - -
- -
-PurchaseIOS snapshot - -```swift -struct PurchaseIOS { - let id: String - let productId: String - let transactionDate: Double - let purchaseToken: String? - let purchaseState: PurchaseState - let isAutoRenewing: Bool - let quantity: Int - let platform: IapPlatform - - // iOS-specific properties - let appAccountToken: String? - let environmentIOS: String? - let storefrontCountryCodeIOS: String? - let subscriptionGroupIdIOS: String? - let transactionReasonIOS: String? - let offerIOS: PurchaseOfferIOS? -} -``` - -
- -### DiscountOffer +For detailed usage, see the [documentation](https://openiap.dev). -```swift -struct DiscountOffer { - let identifier: String - let keyIdentifier: String - let nonce: String - let signature: String - let timestamp: String -} -``` +## License -## ⚑ Error Handling - -OpenIAP provides comprehensive error handling: - -```swift -// Unified error model -struct PurchaseError: LocalizedError { - let code: String - let message: String - let productId: String? +MIT License - see [LICENSE](../../LICENSE) for details. - var errorDescription: String? { message } -} +## Support -// Create errors with predefined codes -let error = PurchaseError(code: "E_USER_CANCELLED", message: "User cancelled the purchase") -``` - -## 🀝 Contributing - -We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. - -## πŸ“„ License - -This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. - -## πŸ”„ Best Practices - -1. **Choose the right API level**: Use `OpenIapModule.shared` for simple flows, or `OpenIapStore` for SwiftUI apps -2. **Handle errors appropriately**: Always check for user cancellations vs actual errors -3. **Validate receipts server-side**: Use `andDangerouslyFinishTransactionAutomatically: false` for server validation -4. **Test with Sandbox**: Always test purchases in App Store Connect Sandbox environment -5. **Monitor events**: Set up purchase listeners before making purchases - -## πŸ’¬ Support - -- πŸ“– **Documentation**: [openiap.dev](https://openiap.dev) -- πŸ› **Bug Reports**: [GitHub Issues](https://github.com/hyodotdev/openiap/issues) -- πŸ’‘ **Feature Requests**: [GitHub Discussions](https://github.com/hyodotdev/openiap/discussions) -- πŸ’¬ **Community**: [Discord](https://discord.gg/openiap) (Coming Soon) - ---- - -
- Built with ❀️ for the OpenIAP community -
+- [Documentation](https://openiap.dev) +- [GitHub Issues](https://github.com/hyodotdev/openiap/issues) +- [Discussions](https://github.com/hyodotdev/openiap/discussions) diff --git a/packages/google/README.md b/packages/google/README.md index e536d7d1..07870bd9 100644 --- a/packages/google/README.md +++ b/packages/google/README.md @@ -2,7 +2,7 @@
OpenIAP Google Logo - +

Android implementation of the OpenIAP specification using Google Play Billing.

@@ -16,33 +16,27 @@ Modern Android Kotlin library for in-app purchases using Google Play Billing Library v8. -## 🌐 Learn More - -Visit [**openiap.dev**](https://openiap.dev) for complete documentation, guides, and the full OpenIAP specification. - -## 🎯 Overview +## Documentation -OpenIAP GMS is a modern, type-safe Kotlin library that simplifies Google Play in-app billing integration. It provides a clean, coroutine-based API that handles all the complexity of Google Play Billing while offering robust error handling and real-time purchase tracking. +Visit [**openiap.dev**](https://openiap.dev) for complete documentation, API reference, guides, and examples. -## ✨ Features +## Features -- πŸ” **Google Play Billing v8** - Latest billing library with enhanced security -- ⚑ **Kotlin Coroutines** - Modern async/await API -- 🎯 **Type Safe** - Full Kotlin type safety with sealed classes -- πŸ”„ **Real-time Events** - Purchase update and error listeners -- 🧡 **Thread Safe** - Concurrent operations with proper synchronization -- πŸ“± **Easy Integration** - Simple singleton pattern with context management -- πŸ›‘οΈ **Robust Error Handling** - Comprehensive error types with detailed messages -- πŸš€ **Production Ready** - Used in production apps +- Google Play Billing v8 +- Kotlin Coroutines +- Type-safe API with sealed classes +- Real-time purchase events +- Thread-safe operations +- Comprehensive error handling -## πŸ“‹ Requirements +## Requirements - **Minimum SDK**: 21 (Android 5.0) - **Compile SDK**: 34+ - **Google Play Billing**: v8.0.0 - **Kotlin**: 1.9.20+ -## πŸ“¦ Installation +## Installation Add to your module's `build.gradle.kts`: @@ -52,468 +46,51 @@ dependencies { } ``` -Or `build.gradle`: - -```groovy -dependencies { - implementation 'io.github.hyochan.openiap:openiap-google:$version' -} -``` - -> πŸ“Œ **Latest Version**: Check [`openiap-versions.json`](../../openiap-versions.json) for the current version, or see the [Maven Central badge](https://central.sonatype.com/artifact/io.github.hyochan.openiap/openiap-google) above. - -## πŸš€ Quick Start +> Check [`openiap-versions.json`](../../openiap-versions.json) for the current version. -### 1. Initialize in Application +## Quick Start ```kotlin -class MyApplication : Application() { - override fun onCreate() { - super.onCreate() - OpenIAP.initialize(this) - } -} -``` +import dev.hyo.openiap.store.OpenIapStore -### 2. Basic Usage - -```kotlin class MainActivity : AppCompatActivity() { - private lateinit var openIAP: OpenIAP + private lateinit var iapStore: OpenIapStore override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - openIAP = OpenIAP.getInstance() - - // Set up listeners - openIAP.addPurchaseUpdateListener { purchase -> - handlePurchaseUpdate(purchase) - } - - openIAP.addPurchaseErrorListener { error -> - handlePurchaseError(error) - } + iapStore = OpenIapStore(this) - // Initialize connection lifecycleScope.launch { - try { - val connected = openIAP.initConnection() - if (connected) { - loadProducts() - } - } catch (e: OpenIapError) { - // Handle connection error - } - } - } - - private suspend fun loadProducts() { - try { - val products = openIAP.fetchProducts(listOf("premium_upgrade", "remove_ads")) - // Display products in UI - } catch (e: OpenIapError) { - // Handle error - } - } + // Initialize connection + iapStore.initConnection() - private suspend fun purchaseProduct(productId: String) { - try { - openIAP.requestPurchase( - activity = this, - sku = productId + // Fetch products + val products = iapStore.fetchProducts( + ProductRequest(skus = listOf("premium_upgrade")) ) - } catch (e: OpenIapError) { - // Handle purchase error } } - - private fun handlePurchaseUpdate(purchase: OpenIapPurchase) { - when (purchase.purchaseState) { - PurchaseState.Purchased -> { - // Acknowledge or consume the purchase - lifecycleScope.launch { - try { - purchase.purchaseToken?.let { token -> - openIAP.acknowledgePurchase(token) - // Or for consumables: openIAP.consumePurchase(token) - } - } catch (e: OpenIapError) { - // Handle error - } - } - } - PurchaseState.Pending -> { - // Purchase is pending (e.g., awaiting payment) - } - // Handle other states... - } - } - - override fun onDestroy() { - super.onDestroy() - openIAP.clearListeners() - openIAP.endConnection() - } } ``` -## πŸ“š API Reference - -### Core Methods - -#### Connection Management - -```kotlin -suspend fun initConnection(): Boolean -fun endConnection() -fun isReady(): Boolean -``` - -#### Product Management - -```kotlin -suspend fun fetchProducts(skus: List): List -suspend fun fetchProducts(type: String, skus: List): List -fun getCachedProduct(sku: String): ProductDetails? -fun getAllCachedProducts(): Map -``` - -#### Purchase Operations - -```kotlin -suspend fun requestPurchase( - activity: Activity, - sku: String, - offerToken: String? = null, - obfuscatedAccountId: String? = null, - obfuscatedProfileId: String? = null -) - -suspend fun requestPurchase(params: Map, activity: Activity) -suspend fun finishTransaction(purchase: OpenIapPurchase, isConsumable: Boolean? = null) -suspend fun getAvailablePurchases(): List -suspend fun getAvailablePurchases(options: Map?): List // options ignored on Android -suspend fun getAvailableItemsByType(type: String): List -suspend fun acknowledgePurchase(purchaseToken: String): Boolean -suspend fun consumePurchase(purchaseToken: String): Boolean -``` - -> Note: Use `"in-app"` for in-app product types. The legacy alias `"inapp"` remains available for compatibility but will be removed in version 1.2.0. - -#### Store Information - -```kotlin -suspend fun getStorefront(): String -``` - -### Subscription Management - -```kotlin -suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List -suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean -fun deepLinkToSubscriptions(): Boolean -``` - -### Event Listeners - -```kotlin -fun addPurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener) -fun removePurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener) -fun addPurchaseErrorListener(listener: OpenIapPurchaseErrorListener) -fun removePurchaseErrorListener(listener: OpenIapPurchaseErrorListener) - -// Convenience methods -fun addListener(listener: OpenIapListener) -fun removeListener(listener: OpenIapListener) -fun clearListeners() -``` - -### Data Models - -#### OpenIapProduct - -```kotlin -data class OpenIapProduct( - val id: String, - val title: String, - val description: String, - val price: Double?, - val displayPrice: String, - val currency: String, - val type: ProductType, - val platform: String = "android", - val displayName: String?, - val debugDescription: String?, - val nameAndroid: String?, - val oneTimePurchaseOfferDetails: OneTimePurchaseOfferDetails?, - val subscriptionOfferDetails: List? -) -``` - -#### OpenIapPurchase - -```kotlin -data class OpenIapPurchase( - val id: String, // transactionId - val productId: String, - val ids: List?, // alias of productIds - val transactionDate: Double, - val transactionReceipt: String, - val purchaseToken: String?, - val platform: String = "android", - val quantity: Int = 1, - val transactionId: String?, - val purchaseTime: Long, - val purchaseState: PurchaseState, - val isAutoRenewing: Boolean, - // ... Android-specific fields - val isAcknowledgedAndroid: Boolean?, - val autoRenewingAndroid: Boolean?, - // ... many more fields -) -``` - -#### Error Handling - -```kotlin -sealed class OpenIapError : Exception { - object UserCancelled : OpenIapError() - object ItemAlreadyOwned : OpenIapError() - object ItemNotOwned : OpenIapError() - data class ProductNotFound(val productId: String) : OpenIapError() - data class PurchaseFailed(override val message: String) : OpenIapError() - // ... many more error types -} -``` +For detailed usage, see the [documentation](https://openiap.dev). -## πŸ”„ Purchase Flow +## Sample App -1. **Initialize**: Call `initConnection()` -2. **Fetch Products**: Use `fetchProducts()` to load available items -3. **Request Purchase**: Call `requestPurchase()` with the product SKU -4. **Handle Events**: Listen for purchase updates via listeners -5. **Process Purchase**: Acknowledge non-consumables or consume consumables -6. **Server Verification**: Always verify purchases on your backend - -## πŸ›‘οΈ Security Best Practices - -- **Server-Side Verification**: Always verify purchases on your backend server -- **Acknowledge Promptly**: Acknowledge non-consumable purchases within 3 days -- **Consume Consumables**: Consume consumable purchases after granting content -- **Handle All States**: Implement proper handling for all purchase states -- **Error Handling**: Implement comprehensive error handling - -## πŸ§ͺ Testing - -The library includes a comprehensive sample app demonstrating all features: +Run the included sample app: ```bash -git clone https://github.com/hyodotdev/openiap.git -cd openiap/packages/google +cd packages/google ./gradlew :Example:installDebug ``` -### Test Products - -For development, use Google Play's test SKUs: - -- `android.test.purchased` - Always succeeds -- `android.test.canceled` - Always cancels -- `android.test.item_unavailable` - Always fails - -For production testing, configure products in Google Play Console and use internal testing. - -## πŸ“± Sample App - -The included sample app (`Example/` directory) demonstrates: - -- βœ… Connection management with retry logic -- βœ… Product listing and purchase flow -- βœ… Real-time purchase event handling -- βœ… Purchase history and management -- βœ… Error handling and user feedback -- βœ… Android-specific billing features - -## πŸ”§ Advanced Usage - -### Custom Error Handling - -```kotlin -try { - openIAP.requestPurchase(this, "premium_upgrade") -} catch (e: OpenIapError) { - when (e) { - OpenIapError.UserCancelled -> { - // User cancelled, no action needed - } - OpenIapError.ItemAlreadyOwned -> { - // Item already purchased - showMessage("You already own this item!") - } - is OpenIapError.ProductNotFound -> { - // Product not available - showError("Product ${e.productId} not found") - } - // Handle other error types... - else -> { - showError("Purchase failed: ${e.message}") - } - } -} -``` - -### Subscription Offers - -```kotlin -// Get subscription offers -val product = openIAP.getCachedProduct("monthly_subscription") -val offers = product?.subscriptionOfferDetails - -// Purchase with specific offer -val offerToken = offers?.firstOrNull()?.offerToken -openIAP.requestPurchase( - activity = this, - sku = "monthly_subscription", - offerToken = offerToken -) -``` - -### One-Time Purchase Discount Offers (Android 7.0+) - -Google Play Billing Library 7.0+ supports discount offers for one-time (in-app) purchases. You can fetch products with discount information and apply them during purchase. - -```kotlin -// 1. Fetch products - discount offers are included automatically -val products = openIAP.fetchProducts(listOf("premium_upgrade")) -val product = products.firstOrNull() - -// 2. Check for available discount offers -val discountOffer = product?.discountOffers?.firstOrNull() - -if (discountOffer != null) { - // Display discount info to user - Log.d("IAP", "Discount available: ${discountOffer.displayPrice}") - Log.d("IAP", "Original price: ${discountOffer.fullPriceMicrosAndroid}") - Log.d("IAP", "Discount: ${discountOffer.percentageDiscountAndroid}%") - - // 3. Purchase with discount offer token - openIAP.requestPurchase( - activity = this, - sku = "premium_upgrade", - offerToken = discountOffer.offerTokenAndroid - ) -} else { - // Purchase at regular price (no offerToken) - openIAP.requestPurchase( - activity = this, - sku = "premium_upgrade" - ) -} -``` +## License -#### Using RequestPurchaseProps +MIT License - see [LICENSE](../../LICENSE) for details. -For more control, use `RequestPurchaseProps` directly: +## Support -```kotlin -val purchaseResult = openIAP.requestPurchase( - props = RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - google = RequestPurchaseAndroidProps( - skus = listOf("premium_upgrade"), - offerToken = discountOffer?.offerTokenAndroid, - isOfferPersonalized = true // Required in EU if offer is personalized - ) - ) - ), - type = ProductQueryType.InApp - ), - activity = this -) -``` - -> **Note**: One-time purchase discount offers require Google Play Billing Library 7.0+ and must be configured in Google Play Console. - -## ⚠️ Important Notes - -- This library requires Google Play Billing Library v8 -- Test with real Google Play Console products for production -- Always verify purchases server-side for security -- Handle all purchase states properly -- Clean up listeners and connections in `onDestroy()` - -## πŸ”§ Troubleshooting - -### Common Issues - -1. **Product not found** - - - Ensure products are configured in Google Play Console - - App must be uploaded to Google Play Console (even as draft) - - Wait up to 24 hours for products to become available - -2. **Billing unavailable** - - - Verify Google Play Services are installed and updated - - Check that app is signed with release key for testing - - Ensure billing permissions are in AndroidManifest.xml - -3. **Purchase not triggering** - - Use real device with Google Play Store - - Avoid emulators without Google Play Services - - Check that test account has payment method - -### Debug Mode - -Enable verbose logging to see detailed billing operations: - -```kotlin -// In development builds -if (BuildConfig.DEBUG) { - Log.d("OpenIAP", "Debug mode enabled") -} -``` - -## πŸ“„ License - -```txt -MIT License - -Copyright (c) 2025 hyo.dev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## 🀝 Contributing - -Contributions are welcome! Please read our contributing guidelines and submit pull requests. - -## πŸ“ž Support - -- **Issues**: [GitHub Issues](https://github.com/hyodotdev/openiap/issues) -- **Discussions**: [OpenIAP Discussions](https://github.com/hyodotdev/openiap/discussions) - ---- - -
- Built with ❀️ for the OpenIAP community - -
+- [Documentation](https://openiap.dev) +- [GitHub Issues](https://github.com/hyodotdev/openiap/issues) +- [Discussions](https://github.com/hyodotdev/openiap/discussions) From 938b8711257cfcb9e37d1182549b6c99d8c46fe0 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 10:09:47 +0900 Subject: [PATCH 12/16] fix: use offerTokenAndroid and isOfferPersonalizedAndroid naming convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply platform suffix convention to Android-specific fields in RequestPurchaseAndroidProps and RequestSubscriptionAndroidProps: - offerToken β†’ offerTokenAndroid - isOfferPersonalized β†’ isOfferPersonalizedAndroid Update knowledge/internal/01-naming-conventions.md with clear rules for platform-specific field naming in GraphQL input types. Co-Authored-By: Claude Opus 4.5 --- knowledge/internal/01-naming-conventions.md | 57 +++++++++++++++++++ packages/apple/Sources/Models/Types.swift | 24 ++++---- .../docs/src/pages/docs/features/discount.tsx | 8 +-- .../docs/src/pages/docs/types/request.tsx | 4 +- .../martie/screens/SubscriptionFlowScreen.kt | 12 ++-- .../java/dev/hyo/openiap/OpenIapViewModel.kt | 4 +- .../dev/hyo/openiap/helpers/SharedHelpers.kt | 6 +- .../src/main/java/dev/hyo/openiap/Types.kt | 30 +++++----- .../java/dev/hyo/openiap/OpenIapViewModel.kt | 4 +- .../java/dev/hyo/openiap/helpers/Helpers.kt | 6 +- .../hyo/openiap/StandardizedOfferTypesTest.kt | 44 +++++++------- packages/gql/src/generated/Types.kt | 30 +++++----- packages/gql/src/generated/Types.swift | 24 ++++---- packages/gql/src/generated/types.dart | 30 +++++----- packages/gql/src/generated/types.gd | 34 +++++------ packages/gql/src/generated/types.ts | 16 ++++-- packages/gql/src/type-android.graphql | 12 ++-- 17 files changed, 210 insertions(+), 135 deletions(-) diff --git a/knowledge/internal/01-naming-conventions.md b/knowledge/internal/01-naming-conventions.md index 9fb3f863..44778e27 100644 --- a/knowledge/internal/01-naming-conventions.md +++ b/knowledge/internal/01-naming-conventions.md @@ -54,6 +54,63 @@ fun buildModuleAndroid() **Exception**: Only use `Android` suffix for types that are part of a cross-platform API (e.g., `ProductAndroid`, `PurchaseAndroid` that contrast with iOS types). +## Platform-Specific Field Naming (CRITICAL) + +> **This is the most commonly violated rule. Pay extra attention.** + +### GraphQL Input Types (API Fields) + +All platform-specific fields in GraphQL input types MUST use the platform suffix: + +```graphql +# CORRECT - All Android-specific fields have Android suffix +input RequestPurchaseAndroidProps { + skus: [String!]! # Cross-platform, no suffix + offerTokenAndroid: String # Android-only feature + isOfferPersonalizedAndroid: Boolean # Android-only feature + obfuscatedAccountIdAndroid: String # Android-only feature + obfuscatedProfileIdAndroid: String # Android-only feature + developerBillingOption: DeveloperBillingOptionParamsAndroid # Type has suffix +} + +# INCORRECT - Missing Android suffix +input RequestPurchaseAndroidProps { + offerToken: String # ❌ Should be offerTokenAndroid + isOfferPersonalized: Boolean # ❌ Should be isOfferPersonalizedAndroid +} +``` + +### Why This Matters + +1. **Cross-platform consumers** (React Native, Flutter, etc.) see all fields +2. Without suffix, it's unclear which platform the field applies to +3. Consistency makes documentation and code generation predictable + +### Field Suffix Rules + +| Field Location | Suffix Required? | Example | +|----------------|------------------|---------| +| Android-only input type | YES for Android features | `offerTokenAndroid`, `isOfferPersonalizedAndroid` | +| iOS-only input type | YES for iOS features | `appAccountTokenIOS`, `uuid` β†’ `appAccountTokenIOS` | +| Cross-platform type | YES for platform-specific | `nameAndroid` in `ProductAndroid` | +| Internal implementation | NO (not API) | `val offerToken` in Kotlin data class | + +### Internal vs API Fields + +- **API fields** (GraphQL schema): ALWAYS use platform suffix +- **Internal fields** (Kotlin/Swift data classes not exposed): No suffix needed + +```kotlin +// Internal helper data class - no suffix needed +internal data class AndroidPurchaseArgs( + val offerToken: String?, // Internal, no suffix OK + val isOfferPersonalized: Boolean? // Internal, no suffix OK +) + +// But when reading from API props, use the suffixed names: +val offerToken = params.offerTokenAndroid // βœ“ API uses suffix +``` + ### Cross-Platform Functions Functions available on BOTH platforms have **NO** platform suffix: diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 9ef6de02..d76ef2e4 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1383,8 +1383,9 @@ public struct RequestPurchaseAndroidProps: Codable { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. public var developerBillingOption: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag - public var isOfferPersonalized: Bool? + /// Personalized offer flag (Android). + /// When true, indicates the price was customized for this user. + public var isOfferPersonalizedAndroid: Bool? /// Obfuscated account ID public var obfuscatedAccountIdAndroid: String? /// Obfuscated profile ID @@ -1392,23 +1393,23 @@ public struct RequestPurchaseAndroidProps: Codable { /// Offer token for one-time purchase discounts (Android 7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. - public var offerToken: String? + public var offerTokenAndroid: String? /// List of product SKUs public var skus: [String] public init( developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalized: Bool? = nil, + isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, - offerToken: String? = nil, + offerTokenAndroid: String? = nil, skus: [String] ) { self.developerBillingOption = developerBillingOption - self.isOfferPersonalized = isOfferPersonalized + self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid - self.offerToken = offerToken + self.offerTokenAndroid = offerTokenAndroid self.skus = skus } } @@ -1552,8 +1553,9 @@ public struct RequestSubscriptionAndroidProps: Codable { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. public var developerBillingOption: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag - public var isOfferPersonalized: Bool? + /// Personalized offer flag (Android). + /// When true, indicates the price was customized for this user. + public var isOfferPersonalizedAndroid: Bool? /// Obfuscated account ID public var obfuscatedAccountIdAndroid: String? /// Obfuscated profile ID @@ -1573,7 +1575,7 @@ public struct RequestSubscriptionAndroidProps: Codable { public init( developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalized: Bool? = nil, + isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, purchaseTokenAndroid: String? = nil, @@ -1583,7 +1585,7 @@ public struct RequestSubscriptionAndroidProps: Codable { subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = nil ) { self.developerBillingOption = developerBillingOption - self.isOfferPersonalized = isOfferPersonalized + self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid self.purchaseTokenAndroid = purchaseTokenAndroid diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx index dafe5c62..b97dc287 100644 --- a/packages/docs/src/pages/docs/features/discount.tsx +++ b/packages/docs/src/pages/docs/features/discount.tsx @@ -857,8 +857,8 @@ async function purchaseWithOffer( request: { google: { skus: [product.id], - // Include offerToken for discounted purchases (Android 7.0+) - offerToken: selectedOffer.offerToken, + // Include offerTokenAndroid for discounted purchases (Android 7.0+) + offerTokenAndroid: selectedOffer.offerToken, }, }, }); @@ -888,8 +888,8 @@ async function purchaseWithOffer( RequestPurchasePropsByPlatforms( google = RequestPurchaseAndroidProps( skus = listOf(product.id), - // Include offerToken for discounted purchases (Android 7.0+) - offerToken = selectedOffer.offerToken + // Include offerTokenAndroid for discounted purchases (Android 7.0+) + offerTokenAndroid = selectedOffer.offerToken ) ) ) diff --git a/packages/docs/src/pages/docs/types/request.tsx b/packages/docs/src/pages/docs/types/request.tsx index cde51d18..bdfcab6e 100644 --- a/packages/docs/src/pages/docs/types/request.tsx +++ b/packages/docs/src/pages/docs/types/request.tsx @@ -525,7 +525,7 @@ await iap.request_purchase(subs_props)`} - offerToken + offerTokenAndroid Offer token for one-time purchase discounts (Android 7.0+). @@ -548,7 +548,7 @@ await iap.request_purchase(subs_props)`} - isOfferPersonalized + isOfferPersonalizedAndroid True if offer is personalized (EU compliance) diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt index 011bcb34..c1f0759c 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt @@ -785,7 +785,7 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, purchaseTokenAndroid = purchaseToken, @@ -1018,7 +1018,7 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, purchaseTokenAndroid = purchaseToken, @@ -1167,7 +1167,7 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, purchaseTokenAndroid = purchaseToken, @@ -1184,7 +1184,7 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Purchase( RequestPurchasePropsByPlatforms( android = RequestPurchaseAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, skus = listOf(product.id) @@ -1446,7 +1446,7 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, purchaseTokenAndroid = null, @@ -1463,7 +1463,7 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Purchase( RequestPurchasePropsByPlatforms( android = RequestPurchaseAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, skus = listOf(product.id) diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt index aa9e9f7a..d08d59bc 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt @@ -46,7 +46,7 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { val props = when (type) { ProductQueryType.InApp -> { val android = RequestPurchaseAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, skus = skus @@ -60,7 +60,7 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { } ProductQueryType.Subs -> { val android = RequestSubscriptionAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, purchaseTokenAndroid = null, diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt index e97bebf7..01decbaa 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt @@ -75,10 +75,10 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { ?: throw IllegalArgumentException("Google purchase parameters are required (use 'google' field)") AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalized, + isOfferPersonalized = params.isOfferPersonalizedAndroid, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, - offerToken = params.offerToken, + offerToken = params.offerTokenAndroid, purchaseTokenAndroid = null, replacementModeAndroid = null, subscriptionOffers = null, @@ -98,7 +98,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { // Both can be provided together - they serve different purposes and are not mutually exclusive AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalized, + isOfferPersonalized = params.isOfferPersonalizedAndroid, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, offerToken = null, 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 3cb7aaf3..0dd5426c 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 @@ -3498,9 +3498,10 @@ public data class RequestPurchaseAndroidProps( */ val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag + * Personalized offer flag (Android). + * When true, indicates the price was customized for this user. */ - val isOfferPersonalized: Boolean? = null, + val isOfferPersonalizedAndroid: Boolean? = null, /** * Obfuscated account ID */ @@ -3514,7 +3515,7 @@ public data class RequestPurchaseAndroidProps( * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers * to apply a discount offer to the purchase. */ - val offerToken: String? = null, + val offerTokenAndroid: String? = null, /** * List of product SKUs */ @@ -3523,18 +3524,18 @@ 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 isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String - val offerToken = json["offerToken"] as? String + val offerTokenAndroid = json["offerTokenAndroid"] as? String val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } if (skus == null) return null return RequestPurchaseAndroidProps( developerBillingOption = developerBillingOption, - isOfferPersonalized = isOfferPersonalized, + isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, - offerToken = offerToken, + offerTokenAndroid = offerTokenAndroid, skus = skus, ) } @@ -3542,10 +3543,10 @@ public data class RequestPurchaseAndroidProps( fun toJson(): Map = mapOf( "developerBillingOption" to developerBillingOption?.toJson(), - "isOfferPersonalized" to isOfferPersonalized, + "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, - "offerToken" to offerToken, + "offerTokenAndroid" to offerTokenAndroid, "skus" to skus, ) } @@ -3716,9 +3717,10 @@ public data class RequestSubscriptionAndroidProps( */ val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag + * Personalized offer flag (Android). + * When true, indicates the price was customized for this user. */ - val isOfferPersonalized: Boolean? = null, + val isOfferPersonalizedAndroid: Boolean? = null, /** * Obfuscated account ID */ @@ -3753,7 +3755,7 @@ 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 isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String val purchaseTokenAndroid = json["purchaseTokenAndroid"] as? String @@ -3764,7 +3766,7 @@ public data class RequestSubscriptionAndroidProps( if (skus == null) return null return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, - isOfferPersonalized = isOfferPersonalized, + isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, purchaseTokenAndroid = purchaseTokenAndroid, @@ -3778,7 +3780,7 @@ public data class RequestSubscriptionAndroidProps( fun toJson(): Map = mapOf( "developerBillingOption" to developerBillingOption?.toJson(), - "isOfferPersonalized" to isOfferPersonalized, + "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, "purchaseTokenAndroid" to purchaseTokenAndroid, diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt index aa9e9f7a..d08d59bc 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt @@ -46,7 +46,7 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { val props = when (type) { ProductQueryType.InApp -> { val android = RequestPurchaseAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, skus = skus @@ -60,7 +60,7 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { } ProductQueryType.Subs -> { val android = RequestSubscriptionAndroidProps( - isOfferPersonalized = null, + isOfferPersonalizedAndroid = null, obfuscatedAccountIdAndroid = null, obfuscatedProfileIdAndroid = null, purchaseTokenAndroid = null, diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt index af7f8cfa..c0044d19 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt @@ -136,10 +136,10 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { ?: throw IllegalArgumentException("Google purchase parameters are required (use 'google' field)") AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalized, + isOfferPersonalized = params.isOfferPersonalizedAndroid, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, - offerToken = params.offerToken, + offerToken = params.offerTokenAndroid, purchaseTokenAndroid = null, replacementModeAndroid = null, subscriptionOffers = null, @@ -160,7 +160,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { // Both can be provided together - they serve different purposes and are not mutually exclusive AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalized, + isOfferPersonalized = params.isOfferPersonalizedAndroid, obfuscatedAccountId = params.obfuscatedAccountIdAndroid, obfuscatedProfileId = params.obfuscatedProfileIdAndroid, offerToken = null, diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt index e5befd62..53108816 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt @@ -405,59 +405,59 @@ class StandardizedOfferTypesTest { assertEquals(PaymentMode.FreeTrial, product.subscriptionOffers.first().paymentMode) } - // MARK: - RequestPurchaseAndroidProps offerToken Tests + // MARK: - RequestPurchaseAndroidProps offerTokenAndroid Tests @Test - fun `RequestPurchaseAndroidProps supports offerToken for one-time purchases`() { + fun `RequestPurchaseAndroidProps supports offerTokenAndroid for one-time purchases`() { val props = RequestPurchaseAndroidProps( skus = listOf("premium_upgrade"), - offerToken = "discount_offer_token_abc123" + offerTokenAndroid = "discount_offer_token_abc123" ) assertEquals(listOf("premium_upgrade"), props.skus) - assertEquals("discount_offer_token_abc123", props.offerToken) - assertNull(props.isOfferPersonalized) + assertEquals("discount_offer_token_abc123", props.offerTokenAndroid) + assertNull(props.isOfferPersonalizedAndroid) assertNull(props.obfuscatedAccountIdAndroid) } @Test - fun `RequestPurchaseAndroidProps toJson includes offerToken`() { + fun `RequestPurchaseAndroidProps toJson includes offerTokenAndroid`() { val props = RequestPurchaseAndroidProps( skus = listOf("product_id"), - offerToken = "test_offer_token", - isOfferPersonalized = true + offerTokenAndroid = "test_offer_token", + isOfferPersonalizedAndroid = true ) val json = props.toJson() assertEquals(listOf("product_id"), json["skus"]) - assertEquals("test_offer_token", json["offerToken"]) - assertEquals(true, json["isOfferPersonalized"]) + assertEquals("test_offer_token", json["offerTokenAndroid"]) + assertEquals(true, json["isOfferPersonalizedAndroid"]) } @Test - fun `RequestPurchaseAndroidProps fromJson parses offerToken`() { + fun `RequestPurchaseAndroidProps fromJson parses offerTokenAndroid`() { val json = mapOf( "skus" to listOf("sku_001"), - "offerToken" to "parsed_offer_token", + "offerTokenAndroid" to "parsed_offer_token", "obfuscatedAccountIdAndroid" to "account_123" ) val props = RequestPurchaseAndroidProps.fromJson(json) assertEquals(listOf("sku_001"), props?.skus) - assertEquals("parsed_offer_token", props?.offerToken) + assertEquals("parsed_offer_token", props?.offerTokenAndroid) assertEquals("account_123", props?.obfuscatedAccountIdAndroid) } @Test - fun `RequestPurchaseAndroidProps allows null offerToken`() { + fun `RequestPurchaseAndroidProps allows null offerTokenAndroid`() { val props = RequestPurchaseAndroidProps( skus = listOf("regular_product") ) - assertNull(props.offerToken) + assertNull(props.offerTokenAndroid) val json = props.toJson() - assertNull(json["offerToken"]) + assertNull(json["offerTokenAndroid"]) } @Test @@ -476,15 +476,15 @@ class StandardizedOfferTypesTest { // Create purchase props using the offer token from the discount offer val purchaseProps = RequestPurchaseAndroidProps( skus = listOf("premium_upgrade"), - offerToken = discountOffer.offerTokenAndroid + offerTokenAndroid = discountOffer.offerTokenAndroid ) - assertEquals("summer_sale_offer_token_xyz", purchaseProps.offerToken) - assertEquals(discountOffer.offerTokenAndroid, purchaseProps.offerToken) + assertEquals("summer_sale_offer_token_xyz", purchaseProps.offerTokenAndroid) + assertEquals(discountOffer.offerTokenAndroid, purchaseProps.offerTokenAndroid) } @Test - fun `ProductAndroid discountOffers can provide offerToken for purchase`() { + fun `ProductAndroid discountOffers can provide offerTokenAndroid for purchase`() { val discountOffer = DiscountOffer( id = "flash_sale", displayPrice = "$2.99", @@ -517,10 +517,10 @@ class StandardizedOfferTypesTest { // Create purchase request with the offer token val purchaseProps = RequestPurchaseAndroidProps( skus = listOf(product.id), - offerToken = offerToken + offerTokenAndroid = offerToken ) assertEquals("consumable_gems", purchaseProps.skus.first()) - assertEquals("flash_sale_token", purchaseProps.offerToken) + assertEquals("flash_sale_token", purchaseProps.offerTokenAndroid) } } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 4c03e42b..4b738095 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3581,9 +3581,10 @@ public data class RequestPurchaseAndroidProps( */ val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag + * Personalized offer flag (Android). + * When true, indicates the price was customized for this user. */ - val isOfferPersonalized: Boolean? = null, + val isOfferPersonalizedAndroid: Boolean? = null, /** * Obfuscated account ID */ @@ -3597,7 +3598,7 @@ public data class RequestPurchaseAndroidProps( * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers * to apply a discount offer to the purchase. */ - val offerToken: String? = null, + val offerTokenAndroid: String? = null, /** * List of product SKUs */ @@ -3606,18 +3607,18 @@ 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 isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String - val offerToken = json["offerToken"] as? String + val offerTokenAndroid = json["offerTokenAndroid"] as? String val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } if (skus == null) return null return RequestPurchaseAndroidProps( developerBillingOption = developerBillingOption, - isOfferPersonalized = isOfferPersonalized, + isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, - offerToken = offerToken, + offerTokenAndroid = offerTokenAndroid, skus = skus, ) } @@ -3625,10 +3626,10 @@ public data class RequestPurchaseAndroidProps( fun toJson(): Map = mapOf( "developerBillingOption" to developerBillingOption?.toJson(), - "isOfferPersonalized" to isOfferPersonalized, + "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, - "offerToken" to offerToken, + "offerTokenAndroid" to offerTokenAndroid, "skus" to skus, ) } @@ -3799,9 +3800,10 @@ public data class RequestSubscriptionAndroidProps( */ val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag + * Personalized offer flag (Android). + * When true, indicates the price was customized for this user. */ - val isOfferPersonalized: Boolean? = null, + val isOfferPersonalizedAndroid: Boolean? = null, /** * Obfuscated account ID */ @@ -3836,7 +3838,7 @@ 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 isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String val purchaseTokenAndroid = json["purchaseTokenAndroid"] as? String @@ -3847,7 +3849,7 @@ public data class RequestSubscriptionAndroidProps( if (skus == null) return null return RequestSubscriptionAndroidProps( developerBillingOption = developerBillingOption, - isOfferPersonalized = isOfferPersonalized, + isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, purchaseTokenAndroid = purchaseTokenAndroid, @@ -3861,7 +3863,7 @@ public data class RequestSubscriptionAndroidProps( fun toJson(): Map = mapOf( "developerBillingOption" to developerBillingOption?.toJson(), - "isOfferPersonalized" to isOfferPersonalized, + "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, "purchaseTokenAndroid" to purchaseTokenAndroid, diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 9ef6de02..d76ef2e4 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1383,8 +1383,9 @@ public struct RequestPurchaseAndroidProps: Codable { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. public var developerBillingOption: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag - public var isOfferPersonalized: Bool? + /// Personalized offer flag (Android). + /// When true, indicates the price was customized for this user. + public var isOfferPersonalizedAndroid: Bool? /// Obfuscated account ID public var obfuscatedAccountIdAndroid: String? /// Obfuscated profile ID @@ -1392,23 +1393,23 @@ public struct RequestPurchaseAndroidProps: Codable { /// Offer token for one-time purchase discounts (Android 7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. - public var offerToken: String? + public var offerTokenAndroid: String? /// List of product SKUs public var skus: [String] public init( developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalized: Bool? = nil, + isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, - offerToken: String? = nil, + offerTokenAndroid: String? = nil, skus: [String] ) { self.developerBillingOption = developerBillingOption - self.isOfferPersonalized = isOfferPersonalized + self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid - self.offerToken = offerToken + self.offerTokenAndroid = offerTokenAndroid self.skus = skus } } @@ -1552,8 +1553,9 @@ public struct RequestSubscriptionAndroidProps: Codable { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. public var developerBillingOption: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag - public var isOfferPersonalized: Bool? + /// Personalized offer flag (Android). + /// When true, indicates the price was customized for this user. + public var isOfferPersonalizedAndroid: Bool? /// Obfuscated account ID public var obfuscatedAccountIdAndroid: String? /// Obfuscated profile ID @@ -1573,7 +1575,7 @@ public struct RequestSubscriptionAndroidProps: Codable { public init( developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalized: Bool? = nil, + isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, purchaseTokenAndroid: String? = nil, @@ -1583,7 +1585,7 @@ public struct RequestSubscriptionAndroidProps: Codable { subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = nil ) { self.developerBillingOption = developerBillingOption - self.isOfferPersonalized = isOfferPersonalized + self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid self.purchaseTokenAndroid = purchaseTokenAndroid diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index c6822733..d4b6ebb8 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3601,10 +3601,10 @@ class PurchaseOptions { class RequestPurchaseAndroidProps { const RequestPurchaseAndroidProps({ this.developerBillingOption, - this.isOfferPersonalized, + this.isOfferPersonalizedAndroid, this.obfuscatedAccountIdAndroid, this.obfuscatedProfileIdAndroid, - this.offerToken, + this.offerTokenAndroid, required this.skus, }); @@ -3612,8 +3612,9 @@ class RequestPurchaseAndroidProps { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. final DeveloperBillingOptionParamsAndroid? developerBillingOption; - /// Personalized offer flag - final bool? isOfferPersonalized; + /// Personalized offer flag (Android). + /// When true, indicates the price was customized for this user. + final bool? isOfferPersonalizedAndroid; /// Obfuscated account ID final String? obfuscatedAccountIdAndroid; /// Obfuscated profile ID @@ -3621,17 +3622,17 @@ class RequestPurchaseAndroidProps { /// Offer token for one-time purchase discounts (Android 7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. - final String? offerToken; + final String? offerTokenAndroid; /// List of product SKUs final List skus; factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, - isOfferPersonalized: json['isOfferPersonalized'] as bool?, + isOfferPersonalizedAndroid: json['isOfferPersonalizedAndroid'] as bool?, obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, - offerToken: json['offerToken'] as String?, + offerTokenAndroid: json['offerTokenAndroid'] as String?, skus: (json['skus'] as List).map((e) => e as String).toList(), ); } @@ -3639,10 +3640,10 @@ class RequestPurchaseAndroidProps { Map toJson() { return { 'developerBillingOption': developerBillingOption?.toJson(), - 'isOfferPersonalized': isOfferPersonalized, + 'isOfferPersonalizedAndroid': isOfferPersonalizedAndroid, 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, - 'offerToken': offerToken, + 'offerTokenAndroid': offerTokenAndroid, 'skus': skus, }; } @@ -3803,7 +3804,7 @@ class RequestPurchasePropsByPlatforms { class RequestSubscriptionAndroidProps { const RequestSubscriptionAndroidProps({ this.developerBillingOption, - this.isOfferPersonalized, + this.isOfferPersonalizedAndroid, this.obfuscatedAccountIdAndroid, this.obfuscatedProfileIdAndroid, this.purchaseTokenAndroid, @@ -3817,8 +3818,9 @@ class RequestSubscriptionAndroidProps { /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. final DeveloperBillingOptionParamsAndroid? developerBillingOption; - /// Personalized offer flag - final bool? isOfferPersonalized; + /// Personalized offer flag (Android). + /// When true, indicates the price was customized for this user. + final bool? isOfferPersonalizedAndroid; /// Obfuscated account ID final String? obfuscatedAccountIdAndroid; /// Obfuscated profile ID @@ -3839,7 +3841,7 @@ class RequestSubscriptionAndroidProps { factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, - isOfferPersonalized: json['isOfferPersonalized'] as bool?, + isOfferPersonalizedAndroid: json['isOfferPersonalizedAndroid'] as bool?, obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, purchaseTokenAndroid: json['purchaseTokenAndroid'] as String?, @@ -3853,7 +3855,7 @@ class RequestSubscriptionAndroidProps { Map toJson() { return { 'developerBillingOption': developerBillingOption?.toJson(), - 'isOfferPersonalized': isOfferPersonalized, + 'isOfferPersonalizedAndroid': isOfferPersonalizedAndroid, 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, 'purchaseTokenAndroid': purchaseTokenAndroid, diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index a61ffd23..bd283acb 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -3198,10 +3198,10 @@ class RequestPurchaseAndroidProps: var obfuscated_account_id_android: String ## Obfuscated profile ID var obfuscated_profile_id_android: String - ## Personalized offer flag - var is_offer_personalized: bool + ## Personalized offer flag (Android). + var is_offer_personalized_android: bool ## Offer token for one-time purchase discounts (Android 7.0+). - var offer_token: String + var offer_token_android: String ## Developer billing option parameters for external payments flow (8.3.0+). var developer_billing_option: DeveloperBillingOptionParamsAndroid @@ -3213,10 +3213,10 @@ class RequestPurchaseAndroidProps: obj.obfuscated_account_id_android = data["obfuscatedAccountIdAndroid"] if data.has("obfuscatedProfileIdAndroid") and data["obfuscatedProfileIdAndroid"] != null: obj.obfuscated_profile_id_android = data["obfuscatedProfileIdAndroid"] - if data.has("isOfferPersonalized") and data["isOfferPersonalized"] != null: - obj.is_offer_personalized = data["isOfferPersonalized"] - if data.has("offerToken") and data["offerToken"] != null: - obj.offer_token = data["offerToken"] + if data.has("isOfferPersonalizedAndroid") and data["isOfferPersonalizedAndroid"] != null: + obj.is_offer_personalized_android = data["isOfferPersonalizedAndroid"] + if data.has("offerTokenAndroid") and data["offerTokenAndroid"] != null: + obj.offer_token_android = data["offerTokenAndroid"] if data.has("developerBillingOption") and data["developerBillingOption"] != null: if data["developerBillingOption"] is Dictionary: obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) @@ -3232,10 +3232,10 @@ class RequestPurchaseAndroidProps: dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android if obfuscated_profile_id_android != null: dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - if is_offer_personalized != null: - dict["isOfferPersonalized"] = is_offer_personalized - if offer_token != null: - dict["offerToken"] = offer_token + if is_offer_personalized_android != null: + dict["isOfferPersonalizedAndroid"] = is_offer_personalized_android + if offer_token_android != null: + dict["offerTokenAndroid"] = offer_token_android if developer_billing_option != null: if developer_billing_option.has_method("to_dict"): dict["developerBillingOption"] = developer_billing_option.to_dict() @@ -3414,8 +3414,8 @@ class RequestSubscriptionAndroidProps: var obfuscated_account_id_android: String ## Obfuscated profile ID var obfuscated_profile_id_android: String - ## Personalized offer flag - var is_offer_personalized: bool + ## Personalized offer flag (Android). + var is_offer_personalized_android: bool ## Purchase token for upgrades/downgrades var purchase_token_android: String ## Replacement mode for subscription changes @@ -3435,8 +3435,8 @@ class RequestSubscriptionAndroidProps: obj.obfuscated_account_id_android = data["obfuscatedAccountIdAndroid"] if data.has("obfuscatedProfileIdAndroid") and data["obfuscatedProfileIdAndroid"] != null: obj.obfuscated_profile_id_android = data["obfuscatedProfileIdAndroid"] - if data.has("isOfferPersonalized") and data["isOfferPersonalized"] != null: - obj.is_offer_personalized = data["isOfferPersonalized"] + if data.has("isOfferPersonalizedAndroid") and data["isOfferPersonalizedAndroid"] != null: + obj.is_offer_personalized_android = data["isOfferPersonalizedAndroid"] if data.has("purchaseTokenAndroid") and data["purchaseTokenAndroid"] != null: obj.purchase_token_android = data["purchaseTokenAndroid"] if data.has("replacementModeAndroid") and data["replacementModeAndroid"] != null: @@ -3469,8 +3469,8 @@ class RequestSubscriptionAndroidProps: dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android if obfuscated_profile_id_android != null: dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - if is_offer_personalized != null: - dict["isOfferPersonalized"] = is_offer_personalized + if is_offer_personalized_android != null: + dict["isOfferPersonalizedAndroid"] = is_offer_personalized_android if purchase_token_android != null: dict["purchaseTokenAndroid"] = purchase_token_android if replacement_mode_android != null: diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index c4cb98bc..ce29106b 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1194,8 +1194,11 @@ export interface RequestPurchaseAndroidProps { * Google Play Billing and the developer's external payment option. */ developerBillingOption?: (DeveloperBillingOptionParamsAndroid | null); - /** Personalized offer flag */ - isOfferPersonalized?: (boolean | null); + /** + * Personalized offer flag (Android). + * When true, indicates the price was customized for this user. + */ + isOfferPersonalizedAndroid?: (boolean | null); /** Obfuscated account ID */ obfuscatedAccountIdAndroid?: (string | null); /** Obfuscated profile ID */ @@ -1205,7 +1208,7 @@ export interface RequestPurchaseAndroidProps { * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers * to apply a discount offer to the purchase. */ - offerToken?: (string | null); + offerTokenAndroid?: (string | null); /** List of product SKUs */ skus: string[]; } @@ -1277,8 +1280,11 @@ export interface RequestSubscriptionAndroidProps { * Google Play Billing and the developer's external payment option. */ developerBillingOption?: (DeveloperBillingOptionParamsAndroid | null); - /** Personalized offer flag */ - isOfferPersonalized?: (boolean | null); + /** + * Personalized offer flag (Android). + * When true, indicates the price was customized for this user. + */ + isOfferPersonalizedAndroid?: (boolean | null); /** Obfuscated account ID */ obfuscatedAccountIdAndroid?: (string | null); /** Obfuscated profile ID */ diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql index cde79457..4e2a61ef 100644 --- a/packages/gql/src/type-android.graphql +++ b/packages/gql/src/type-android.graphql @@ -361,15 +361,16 @@ input RequestPurchaseAndroidProps { """ obfuscatedProfileIdAndroid: String """ - Personalized offer flag + Personalized offer flag (Android). + When true, indicates the price was customized for this user. """ - isOfferPersonalized: Boolean + isOfferPersonalizedAndroid: Boolean """ Offer token for one-time purchase discounts (Android 7.0+). Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers to apply a discount offer to the purchase. """ - offerToken: String + offerTokenAndroid: String """ Developer billing option parameters for external payments flow (8.3.0+). When provided, the purchase flow will show a side-by-side choice between @@ -392,9 +393,10 @@ input RequestSubscriptionAndroidProps { """ obfuscatedProfileIdAndroid: String """ - Personalized offer flag + Personalized offer flag (Android). + When true, indicates the price was customized for this user. """ - isOfferPersonalized: Boolean + isOfferPersonalizedAndroid: Boolean """ Purchase token for upgrades/downgrades """ From f0b4a6e3257579e3cf183ae775074fe09ae0a746 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 10:29:07 +0900 Subject: [PATCH 13/16] fix(gql): rename developerBillingOption to developerBillingOptionAndroid Android-specific fields in GraphQL input types must use the Android suffix to clearly indicate they are platform-specific. This is consistent with other fields like obfuscatedAccountIdAndroid. Changes: - Rename developerBillingOption to developerBillingOptionAndroid - Update all documentation code examples - Update Example app AlternativeBillingScreen - Regenerate types for all platforms - Update llms.txt and llms-full.txt Co-Authored-By: Claude Opus 4.5 --- knowledge/_claude-context/context.md | 60 +- packages/apple/Sources/Models/Types.swift | 12 +- packages/docs/public/llms-full.txt | 2 +- packages/docs/public/llms.txt | 2 +- packages/docs/src/pages/docs/events.tsx | 2 +- .../pages/docs/features/external-purchase.tsx | 8 +- .../docs/src/pages/docs/types/alternative.tsx | 6 +- .../docs/src/pages/docs/updates/notes.tsx | 2 +- .../screens/AlternativeBillingScreen.kt | 2 +- .../src/main/java/dev/hyo/openiap/Types.kt | 16 +- .../java/dev/hyo/openiap/OpenIapModule.kt | 5 +- .../java/dev/hyo/openiap/helpers/Helpers.kt | 4 +- .../openiap/BillingLibraryClassPathTest.kt | 748 ++++++++++++++++++ packages/gql/src/generated/Types.kt | 16 +- packages/gql/src/generated/Types.swift | 12 +- packages/gql/src/generated/types.dart | 16 +- packages/gql/src/generated/types.gd | 36 +- packages/gql/src/generated/types.ts | 4 +- packages/gql/src/type-android.graphql | 4 +- 19 files changed, 881 insertions(+), 76 deletions(-) create mode 100644 packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 11ae07df..c99583a3 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -1,7 +1,7 @@ # OpenIAP Project Context > **Auto-generated for Claude Code** -> Last updated: 2026-01-20T00:21:08.836Z +> Last updated: 2026-01-20T01:17:35.950Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -72,6 +72,63 @@ fun buildModuleAndroid() **Exception**: Only use `Android` suffix for types that are part of a cross-platform API (e.g., `ProductAndroid`, `PurchaseAndroid` that contrast with iOS types). +## Platform-Specific Field Naming (CRITICAL) + +> **This is the most commonly violated rule. Pay extra attention.** + +### GraphQL Input Types (API Fields) + +All platform-specific fields in GraphQL input types MUST use the platform suffix: + +```graphql +# CORRECT - All Android-specific fields have Android suffix +input RequestPurchaseAndroidProps { + skus: [String!]! # Cross-platform, no suffix + offerTokenAndroid: String # Android-only feature + isOfferPersonalizedAndroid: Boolean # Android-only feature + obfuscatedAccountIdAndroid: String # Android-only feature + obfuscatedProfileIdAndroid: String # Android-only feature + developerBillingOption: DeveloperBillingOptionParamsAndroid # Type has suffix +} + +# INCORRECT - Missing Android suffix +input RequestPurchaseAndroidProps { + offerToken: String # ❌ Should be offerTokenAndroid + isOfferPersonalized: Boolean # ❌ Should be isOfferPersonalizedAndroid +} +``` + +### Why This Matters + +1. **Cross-platform consumers** (React Native, Flutter, etc.) see all fields +2. Without suffix, it's unclear which platform the field applies to +3. Consistency makes documentation and code generation predictable + +### Field Suffix Rules + +| Field Location | Suffix Required? | Example | +|----------------|------------------|---------| +| Android-only input type | YES for Android features | `offerTokenAndroid`, `isOfferPersonalizedAndroid` | +| iOS-only input type | YES for iOS features | `appAccountTokenIOS`, `uuid` β†’ `appAccountTokenIOS` | +| Cross-platform type | YES for platform-specific | `nameAndroid` in `ProductAndroid` | +| Internal implementation | NO (not API) | `val offerToken` in Kotlin data class | + +### Internal vs API Fields + +- **API fields** (GraphQL schema): ALWAYS use platform suffix +- **Internal fields** (Kotlin/Swift data classes not exposed): No suffix needed + +```kotlin +// Internal helper data class - no suffix needed +internal data class AndroidPurchaseArgs( + val offerToken: String?, // Internal, no suffix OK + val isOfferPersonalized: Boolean? // Internal, no suffix OK +) + +// But when reading from API props, use the suffixed names: +val offerToken = params.offerTokenAndroid // βœ“ API uses suffix +``` + ### Cross-Platform Functions Functions available on BOTH platforms have **NO** platform suffix: @@ -755,7 +812,6 @@ public func newFeatureIOS(param: String) async throws -> ResultType { | `OpenIapModule.swift` | `OpenIapModule+ObjC.swift` | **Verification**: After updating, run: - ```bash swift build # Verifies ObjC bridge compiles ``` diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index d76ef2e4..152e9163 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1382,7 +1382,7 @@ public struct RequestPurchaseAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? /// Personalized offer flag (Android). /// When true, indicates the price was customized for this user. public var isOfferPersonalizedAndroid: Bool? @@ -1398,14 +1398,14 @@ public struct RequestPurchaseAndroidProps: Codable { public var skus: [String] public init( - developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, offerTokenAndroid: String? = nil, skus: [String] ) { - self.developerBillingOption = developerBillingOption + self.developerBillingOptionAndroid = developerBillingOptionAndroid self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid @@ -1552,7 +1552,7 @@ public struct RequestSubscriptionAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? /// Personalized offer flag (Android). /// When true, indicates the price was customized for this user. public var isOfferPersonalizedAndroid: Bool? @@ -1574,7 +1574,7 @@ public struct RequestSubscriptionAndroidProps: Codable { public var subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? public init( - developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, @@ -1584,7 +1584,7 @@ public struct RequestSubscriptionAndroidProps: Codable { subscriptionOffers: [AndroidSubscriptionOfferInput]? = nil, subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = nil ) { - self.developerBillingOption = developerBillingOption + self.developerBillingOptionAndroid = developerBillingOptionAndroid self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index 069b0c55..b01960bc 100644 --- a/packages/docs/public/llms-full.txt +++ b/packages/docs/public/llms-full.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-01-20T00:21:08.852Z +> Generated: 2026-01-20T01:17:35.964Z ## Table of Contents 1. Installation diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index f2d31d37..f74823ba 100644 --- a/packages/docs/public/llms.txt +++ b/packages/docs/public/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-01-20T00:21:08.852Z +> Generated: 2026-01-20T01:17:35.964Z ## Installation diff --git a/packages/docs/src/pages/docs/events.tsx b/packages/docs/src/pages/docs/events.tsx index bb7d4f0d..5d7b014f 100644 --- a/packages/docs/src/pages/docs/events.tsx +++ b/packages/docs/src/pages/docs/events.tsx @@ -996,7 +996,7 @@ subscription.cancel();`} Setup AlternativeBillingModeAndroid.UserChoice - enableBillingProgram(EXTERNAL_PAYMENTS) + developerBillingOption in requestPurchase + enableBillingProgram(EXTERNAL_PAYMENTS) + developerBillingOptionAndroid in requestPurchase diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index c6e57127..27b79f18 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -1538,7 +1538,7 @@ async function handlePurchaseWithExternalPayments(productId: string) { await requestPurchase({ google: { skus: [productId], - developerBillingOption: { + developerBillingOptionAndroid: { billingProgram: 'EXTERNAL_PAYMENTS', linkUri: 'https://your-payment-site.com/checkout', launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', @@ -1626,7 +1626,7 @@ suspend fun handlePurchaseWithExternalPayments(productId: String) { RequestPurchasePropsByPlatforms( google = RequestPurchaseAndroidProps( skus = listOf(productId), - developerBillingOption = DeveloperBillingOptionParamsAndroid( + developerBillingOptionAndroid = DeveloperBillingOptionParamsAndroid( billingProgram = BillingProgramAndroid.ExternalPayments, linkUri = "https://your-payment-site.com/checkout", launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp @@ -1701,7 +1701,7 @@ Future handlePurchaseWithExternalPayments(String productId) async { // User will see side-by-side choice dialog await FlutterInappPurchase.instance.requestPurchase( productId, - developerBillingOption: DeveloperBillingOptionParamsAndroid( + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid( billingProgram: BillingProgramAndroid.externalPayments, linkUri: 'https://your-payment-site.com/checkout', launchMode: DeveloperBillingLaunchModeAndroid.launchInExternalBrowserOrApp, @@ -1804,7 +1804,7 @@ func handle_purchase_with_external_payments(product_id: String) -> void: 3 - requestPurchase(developerBillingOption: ...) + requestPurchase(developerBillingOptionAndroid: ...) Launch purchase with developer billing option configured diff --git a/packages/docs/src/pages/docs/types/alternative.tsx b/packages/docs/src/pages/docs/types/alternative.tsx index 095b2a07..2314ab0b 100644 --- a/packages/docs/src/pages/docs/types/alternative.tsx +++ b/packages/docs/src/pages/docs/types/alternative.tsx @@ -873,7 +873,7 @@ if (result.isAvailable) { await requestPurchase({ google: { skus: ['product_id'], - developerBillingOption: { + developerBillingOptionAndroid: { billingProgram: 'EXTERNAL_PAYMENTS', linkUri: 'https://your-site.com/checkout', launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', @@ -910,7 +910,7 @@ if (result.isAvailable) { RequestPurchasePropsByPlatforms( google = RequestPurchaseAndroidProps( skus = listOf("product_id"), - developerBillingOption = DeveloperBillingOptionParamsAndroid( + developerBillingOptionAndroid = DeveloperBillingOptionParamsAndroid( billingProgram = BillingProgramAndroid.ExternalPayments, linkUri = "https://your-site.com/checkout", launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp @@ -946,7 +946,7 @@ if (result.isAvailable) { // Purchase with developer billing option await FlutterInappPurchase.instance.requestPurchase( 'product_id', - developerBillingOption: DeveloperBillingOptionParamsAndroid( + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid( billingProgram: BillingProgramAndroid.externalPayments, linkUri: 'https://your-site.com/checkout', launchMode: DeveloperBillingLaunchModeAndroid.launchInExternalBrowserOrApp, diff --git a/packages/docs/src/pages/docs/updates/notes.tsx b/packages/docs/src/pages/docs/updates/notes.tsx index b110d962..5c7dd0df 100644 --- a/packages/docs/src/pages/docs/updates/notes.tsx +++ b/packages/docs/src/pages/docs/updates/notes.tsx @@ -573,7 +573,7 @@ let products = try await OpenIapModule.shared.fetchProducts(request)`} - New listener for when user selects developer billing
  • - developerBillingOption{' '} + developerBillingOptionAndroid{' '} - New field in RequestPurchaseAndroidProps and RequestSubscriptionAndroidProps
  • diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt index 9b703cb5..7cd5aa1a 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt @@ -960,7 +960,7 @@ fun AlternativeBillingScreen(navController: NavController) { RequestPurchasePropsByPlatforms( google = RequestPurchaseAndroidProps( skus = listOf(selectedProduct!!.id), - developerBillingOption = DeveloperBillingOptionParamsAndroid( + developerBillingOptionAndroid = DeveloperBillingOptionParamsAndroid( billingProgram = BillingProgramAndroid.ExternalPayments, linkUri = "https://example.com/checkout?product=${selectedProduct!!.id}", launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp 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 0dd5426c..5f41cd71 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 @@ -3496,7 +3496,7 @@ public data class RequestPurchaseAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, /** * Personalized offer flag (Android). * When true, indicates the price was customized for this user. @@ -3523,7 +3523,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String @@ -3531,7 +3531,7 @@ public data class RequestPurchaseAndroidProps( val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } if (skus == null) return null return RequestPurchaseAndroidProps( - developerBillingOption = developerBillingOption, + developerBillingOptionAndroid = developerBillingOptionAndroid, isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, @@ -3542,7 +3542,7 @@ public data class RequestPurchaseAndroidProps( } fun toJson(): Map = mapOf( - "developerBillingOption" to developerBillingOption?.toJson(), + "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, @@ -3715,7 +3715,7 @@ public data class RequestSubscriptionAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, /** * Personalized offer flag (Android). * When true, indicates the price was customized for this user. @@ -3754,7 +3754,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String @@ -3765,7 +3765,7 @@ public data class RequestSubscriptionAndroidProps( val subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } if (skus == null) return null return RequestSubscriptionAndroidProps( - developerBillingOption = developerBillingOption, + developerBillingOptionAndroid = developerBillingOptionAndroid, isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, @@ -3779,7 +3779,7 @@ public data class RequestSubscriptionAndroidProps( } fun toJson(): Map = mapOf( - "developerBillingOption" to developerBillingOption?.toJson(), + "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, 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 bf05428f..e2604326 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 @@ -1580,11 +1580,12 @@ class OpenIapModule( } // Build SubscriptionProductReplacementParams using reflection + // Note: SubscriptionProductReplacementParams is nested under ProductDetailsParams (Billing Library 8.1.0+) val replacementParamsClass = Class.forName( - "com.android.billingclient.api.BillingFlowParams\$SubscriptionProductReplacementParams" + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams" ) val replacementBuilderClass = Class.forName( - "com.android.billingclient.api.BillingFlowParams\$SubscriptionProductReplacementParams\$Builder" + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams\$Builder" ) // Create new builder diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt index c0044d19..fcd2b9e2 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt @@ -144,7 +144,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { replacementModeAndroid = null, subscriptionOffers = null, subscriptionProductReplacementParams = null, - developerBillingOption = params.developerBillingOption, + developerBillingOption = params.developerBillingOptionAndroid, type = type, useAlternativeBilling = useAlternativeBilling ) @@ -168,7 +168,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { replacementModeAndroid = params.replacementModeAndroid, subscriptionOffers = params.subscriptionOffers, subscriptionProductReplacementParams = params.subscriptionProductReplacementParams, - developerBillingOption = params.developerBillingOption, + developerBillingOption = params.developerBillingOptionAndroid, type = type, useAlternativeBilling = useAlternativeBilling ) diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt new file mode 100644 index 00000000..d723e609 --- /dev/null +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt @@ -0,0 +1,748 @@ +package dev.hyo.openiap + +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +/** + * Tests to verify that reflection-based class paths used in OpenIapModule + * match the actual Google Play Billing Library class structure. + * + * These tests prevent issues like #70 where SubscriptionProductReplacementParams + * was referenced at the wrong path (missing ProductDetailsParams in the hierarchy). + * + * IMPORTANT: Every Class.forName() and getMethod() call in OpenIapModule.kt + * should have a corresponding test here to catch API changes early. + * + * @see Issue #70 + */ +class BillingLibraryClassPathTest { + + // ============================================================================ + // MARK: - SubscriptionProductReplacementParams (Billing Library 8.1.0+) + // Used in: OpenIapModule.applySubscriptionProductReplacementParams() + // ============================================================================ + + @Test + fun `SubscriptionProductReplacementParams class exists at correct path`() { + // Issue #70: Was incorrectly using BillingFlowParams$SubscriptionProductReplacementParams + // Correct path: BillingFlowParams$ProductDetailsParams$SubscriptionProductReplacementParams + val className = "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams" + assertClassExists(className, "8.1.0+") + } + + @Test + fun `SubscriptionProductReplacementParams Builder class exists at correct path`() { + val className = "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams\$Builder" + assertClassExists(className, "8.1.0+") + } + + @Test + fun `SubscriptionProductReplacementParams has newBuilder method`() { + assertClassHasMethod( + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams", + "newBuilder" + ) + } + + @Test + fun `SubscriptionProductReplacementParams Builder has setOldProductId method`() { + assertClassHasMethod( + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams\$Builder", + "setOldProductId", + String::class.java + ) + } + + @Test + fun `SubscriptionProductReplacementParams Builder has setReplacementMode method`() { + assertClassHasMethod( + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams\$Builder", + "setReplacementMode", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `SubscriptionProductReplacementParams Builder has build method`() { + assertClassHasMethod( + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams\$Builder", + "build" + ) + } + + @Test + fun `WRONG path for SubscriptionProductReplacementParams should NOT exist`() { + // This is the WRONG path that was causing Issue #70 + val wrongClassName = "com.android.billingclient.api.BillingFlowParams\$SubscriptionProductReplacementParams" + assertClassDoesNotExist(wrongClassName) + } + + @Test + fun `SubscriptionProductReplacementParams ReplacementMode annotation exists`() { + val className = "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams\$ReplacementMode" + try { + val clazz = Class.forName(className) + assertNotNull("ReplacementMode annotation should exist", clazz) + assertTrue("ReplacementMode should be an annotation", clazz.isAnnotation) + } catch (e: ClassNotFoundException) { + fail("ReplacementMode annotation not found: $className") + } + } + + // ============================================================================ + // MARK: - ProductDetailsParams (base class) + // Used in: OpenIapModule for subscription replacement params + // ============================================================================ + + @Test + fun `ProductDetailsParams class exists`() { + assertClassExists( + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams", + "5.0+" + ) + } + + @Test + fun `ProductDetailsParams Builder class exists`() { + assertClassExists( + "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$Builder", + "5.0+" + ) + } + + @Test + fun `ProductDetailsParams Builder has setSubscriptionProductReplacementParams method`() { + val builderClassName = "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$Builder" + val replacementParamsClassName = "com.android.billingclient.api.BillingFlowParams\$ProductDetailsParams\$SubscriptionProductReplacementParams" + + try { + val builderClass = Class.forName(builderClassName) + val replacementParamsClass = Class.forName(replacementParamsClassName) + val setMethod = builderClass.getMethod("setSubscriptionProductReplacementParams", replacementParamsClass) + assertNotNull("setSubscriptionProductReplacementParams method should exist", setMethod) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("setSubscriptionProductReplacementParams method not found. Requires Billing Library 8.1.0+") + } + } + + // ============================================================================ + // MARK: - SubscriptionUpdateParams (legacy) + // Used for backwards compatibility + // ============================================================================ + + @Test + fun `SubscriptionUpdateParams class exists for legacy support`() { + assertClassExists( + "com.android.billingclient.api.BillingFlowParams\$SubscriptionUpdateParams", + "any version" + ) + } + + // ============================================================================ + // MARK: - AlternativeBillingOnlyAvailabilityListener (Billing Library 6.0+) + // Used in: OpenIapModule.checkAlternativeBillingAvailability() + // ============================================================================ + + @Test + fun `AlternativeBillingOnlyAvailabilityListener class exists`() { + assertClassExists( + "com.android.billingclient.api.AlternativeBillingOnlyAvailabilityListener", + "6.0+" + ) + } + + @Test + fun `AlternativeBillingOnlyAvailabilityListener has callback method`() { + assertClassHasMethod( + "com.android.billingclient.api.AlternativeBillingOnlyAvailabilityListener", + "onAlternativeBillingOnlyAvailabilityResponse", + com.android.billingclient.api.BillingResult::class.java + ) + } + + // ============================================================================ + // MARK: - AlternativeBillingOnlyInformationDialogListener (Billing Library 6.0+) + // Used in: OpenIapModule.showAlternativeBillingInformationDialog() + // ============================================================================ + + @Test + fun `AlternativeBillingOnlyInformationDialogListener class exists`() { + assertClassExists( + "com.android.billingclient.api.AlternativeBillingOnlyInformationDialogListener", + "6.0+" + ) + } + + @Test + fun `AlternativeBillingOnlyInformationDialogListener has callback method`() { + assertClassHasMethod( + "com.android.billingclient.api.AlternativeBillingOnlyInformationDialogListener", + "onAlternativeBillingOnlyInformationDialogResponse", + com.android.billingclient.api.BillingResult::class.java + ) + } + + // ============================================================================ + // MARK: - AlternativeBillingOnlyReportingDetailsListener (Billing Library 6.0+) + // Used in: OpenIapModule.createAlternativeBillingReportingToken() + // ============================================================================ + + @Test + fun `AlternativeBillingOnlyReportingDetailsListener class exists`() { + assertClassExists( + "com.android.billingclient.api.AlternativeBillingOnlyReportingDetailsListener", + "6.0+" + ) + } + + @Test + fun `AlternativeBillingOnlyReportingDetailsListener has callback method`() { + // The callback receives BillingResult and AlternativeBillingOnlyReportingDetails + val listenerClass = Class.forName("com.android.billingclient.api.AlternativeBillingOnlyReportingDetailsListener") + val methods = listenerClass.methods.filter { it.name == "onAlternativeBillingOnlyTokenResponse" } + assertTrue( + "onAlternativeBillingOnlyTokenResponse method should exist", + methods.isNotEmpty() + ) + } + + // ============================================================================ + // MARK: - BillingProgramAvailabilityListener (Billing Library 7.0+/8.2.0+) + // Used in: OpenIapModule.isBillingProgramAvailable() + // ============================================================================ + + @Test + fun `BillingProgramAvailabilityListener class exists`() { + assertClassExists( + "com.android.billingclient.api.BillingProgramAvailabilityListener", + "7.0+" + ) + } + + @Test + fun `BillingProgramAvailabilityListener has callback method`() { + // Callback receives (BillingResult, BillingProgramAvailabilityDetails) + val listenerClass = Class.forName("com.android.billingclient.api.BillingProgramAvailabilityListener") + val methods = listenerClass.declaredMethods.filter { it.name == "onBillingProgramAvailabilityResponse" } + assertTrue( + "onBillingProgramAvailabilityResponse method should exist", + methods.isNotEmpty() + ) + // Verify it has 2 parameters + val method = methods.first() + assertTrue( + "onBillingProgramAvailabilityResponse should have 2 parameters (BillingResult, BillingProgramAvailabilityDetails)", + method.parameterTypes.size == 2 + ) + } + + // ============================================================================ + // MARK: - BillingProgramReportingDetailsListener (Billing Library 8.2.0+) + // Used in: OpenIapModule.createBillingProgramReportingDetails() + // ============================================================================ + + @Test + fun `BillingProgramReportingDetailsListener class exists`() { + assertClassExists( + "com.android.billingclient.api.BillingProgramReportingDetailsListener", + "8.2.0+" + ) + } + + @Test + fun `BillingProgramReportingDetailsListener has callback method`() { + // The callback receives (BillingResult, BillingProgramReportingDetails) + // Note: Actual method name is onCreateBillingProgramReportingDetailsResponse + val listenerClass = Class.forName("com.android.billingclient.api.BillingProgramReportingDetailsListener") + val methods = listenerClass.declaredMethods.filter { it.name == "onCreateBillingProgramReportingDetailsResponse" } + assertTrue( + "onCreateBillingProgramReportingDetailsResponse method should exist", + methods.isNotEmpty() + ) + } + + @Test + fun `BillingProgramReportingDetailsParams class exists`() { + // Required parameter for createBillingProgramReportingDetailsAsync in 8.3.0+ + assertClassExists( + "com.android.billingclient.api.BillingProgramReportingDetailsParams", + "8.3.0+" + ) + } + + // ============================================================================ + // MARK: - LaunchExternalLinkParams (Billing Library 6.0+/8.2.0+) + // Used in: OpenIapModule.launchExternalLink() + // ============================================================================ + + @Test + fun `LaunchExternalLinkParams class exists`() { + assertClassExists( + "com.android.billingclient.api.LaunchExternalLinkParams", + "6.0+" + ) + } + + @Test + fun `LaunchExternalLinkParams Builder class exists`() { + assertClassExists( + "com.android.billingclient.api.LaunchExternalLinkParams\$Builder", + "6.0+" + ) + } + + @Test + fun `LaunchExternalLinkParams has newBuilder method`() { + assertClassHasMethod( + "com.android.billingclient.api.LaunchExternalLinkParams", + "newBuilder" + ) + } + + @Test + fun `LaunchExternalLinkParams Builder has setBillingProgram method`() { + assertClassHasMethod( + "com.android.billingclient.api.LaunchExternalLinkParams\$Builder", + "setBillingProgram", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `LaunchExternalLinkParams Builder has setLaunchMode method`() { + assertClassHasMethod( + "com.android.billingclient.api.LaunchExternalLinkParams\$Builder", + "setLaunchMode", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `LaunchExternalLinkParams Builder has setLinkType method`() { + assertClassHasMethod( + "com.android.billingclient.api.LaunchExternalLinkParams\$Builder", + "setLinkType", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `LaunchExternalLinkParams Builder has setLinkUri method`() { + assertClassHasMethod( + "com.android.billingclient.api.LaunchExternalLinkParams\$Builder", + "setLinkUri", + android.net.Uri::class.java + ) + } + + @Test + fun `LaunchExternalLinkParams Builder has build method`() { + assertClassHasMethod( + "com.android.billingclient.api.LaunchExternalLinkParams\$Builder", + "build" + ) + } + + // ============================================================================ + // MARK: - LaunchExternalLinkResponseListener (Billing Library 6.0+) + // Used in: OpenIapModule.launchExternalLink() + // ============================================================================ + + @Test + fun `LaunchExternalLinkResponseListener class exists`() { + assertClassExists( + "com.android.billingclient.api.LaunchExternalLinkResponseListener", + "6.0+" + ) + } + + @Test + fun `LaunchExternalLinkResponseListener has callback method`() { + assertClassHasMethod( + "com.android.billingclient.api.LaunchExternalLinkResponseListener", + "onLaunchExternalLinkResponse", + com.android.billingclient.api.BillingResult::class.java + ) + } + + // ============================================================================ + // MARK: - UserChoiceBillingListener (Billing Library 5.0+) + // Used in: OpenIapModule alternative billing mode USER_CHOICE + // ============================================================================ + + @Test + fun `UserChoiceBillingListener class exists`() { + assertClassExists( + "com.android.billingclient.api.UserChoiceBillingListener", + "5.0+" + ) + } + + @Test + fun `UserChoiceBillingListener has callback method`() { + // The callback receives UserChoiceDetails + val listenerClass = Class.forName("com.android.billingclient.api.UserChoiceBillingListener") + val methods = listenerClass.methods.filter { it.name == "userSelectedAlternativeBilling" } + assertTrue( + "userSelectedAlternativeBilling method should exist", + methods.isNotEmpty() + ) + } + + // ============================================================================ + // MARK: - DeveloperProvidedBillingListener (Billing Library 8.3.0+) + // Used in: OpenIapModule.enableExternalPaymentsProgram() + // ============================================================================ + + @Test + fun `DeveloperProvidedBillingListener class exists`() { + assertClassExists( + "com.android.billingclient.api.DeveloperProvidedBillingListener", + "8.3.0+" + ) + } + + @Test + fun `DeveloperProvidedBillingListener has callback method`() { + // The callback receives DeveloperProvidedBillingDetails + val listenerClass = Class.forName("com.android.billingclient.api.DeveloperProvidedBillingListener") + val methods = listenerClass.methods.filter { it.name == "onUserSelectedDeveloperBilling" } + assertTrue( + "onUserSelectedDeveloperBilling method should exist", + methods.isNotEmpty() + ) + } + + // ============================================================================ + // MARK: - EnableBillingProgramParams (Billing Library 8.3.0+) + // Used in: OpenIapModule.enableExternalPaymentsProgram() + // ============================================================================ + + @Test + fun `EnableBillingProgramParams class exists`() { + assertClassExists( + "com.android.billingclient.api.EnableBillingProgramParams", + "8.3.0+" + ) + } + + @Test + fun `EnableBillingProgramParams Builder class exists`() { + assertClassExists( + "com.android.billingclient.api.EnableBillingProgramParams\$Builder", + "8.3.0+" + ) + } + + @Test + fun `EnableBillingProgramParams has newBuilder method`() { + assertClassHasMethod( + "com.android.billingclient.api.EnableBillingProgramParams", + "newBuilder" + ) + } + + @Test + fun `EnableBillingProgramParams Builder has setBillingProgram method`() { + assertClassHasMethod( + "com.android.billingclient.api.EnableBillingProgramParams\$Builder", + "setBillingProgram", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `EnableBillingProgramParams Builder has setDeveloperProvidedBillingListener method`() { + val builderClassName = "com.android.billingclient.api.EnableBillingProgramParams\$Builder" + val listenerClassName = "com.android.billingclient.api.DeveloperProvidedBillingListener" + + try { + val builderClass = Class.forName(builderClassName) + val listenerClass = Class.forName(listenerClassName) + val method = builderClass.getMethod("setDeveloperProvidedBillingListener", listenerClass) + assertNotNull("setDeveloperProvidedBillingListener method should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("setDeveloperProvidedBillingListener method not found. Requires Billing Library 8.3.0+") + } + } + + @Test + fun `EnableBillingProgramParams Builder has build method`() { + assertClassHasMethod( + "com.android.billingclient.api.EnableBillingProgramParams\$Builder", + "build" + ) + } + + // ============================================================================ + // MARK: - DeveloperBillingOptionParams (Billing Library 8.3.0+) + // Used in: OpenIapModule.applyDeveloperBillingOption() + // ============================================================================ + + @Test + fun `DeveloperBillingOptionParams class exists`() { + assertClassExists( + "com.android.billingclient.api.DeveloperBillingOptionParams", + "8.3.0+" + ) + } + + @Test + fun `DeveloperBillingOptionParams Builder class exists`() { + assertClassExists( + "com.android.billingclient.api.DeveloperBillingOptionParams\$Builder", + "8.3.0+" + ) + } + + @Test + fun `DeveloperBillingOptionParams has newBuilder method`() { + assertClassHasMethod( + "com.android.billingclient.api.DeveloperBillingOptionParams", + "newBuilder" + ) + } + + @Test + fun `DeveloperBillingOptionParams Builder has setBillingProgram method`() { + assertClassHasMethod( + "com.android.billingclient.api.DeveloperBillingOptionParams\$Builder", + "setBillingProgram", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `DeveloperBillingOptionParams Builder has setLinkUri method`() { + assertClassHasMethod( + "com.android.billingclient.api.DeveloperBillingOptionParams\$Builder", + "setLinkUri", + android.net.Uri::class.java + ) + } + + @Test + fun `DeveloperBillingOptionParams Builder has setLaunchMode method`() { + assertClassHasMethod( + "com.android.billingclient.api.DeveloperBillingOptionParams\$Builder", + "setLaunchMode", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `DeveloperBillingOptionParams Builder has build method`() { + assertClassHasMethod( + "com.android.billingclient.api.DeveloperBillingOptionParams\$Builder", + "build" + ) + } + + // ============================================================================ + // MARK: - BillingFlowParams.Builder (for enableDeveloperBillingOption) + // Used in: OpenIapModule.applyDeveloperBillingOption() + // ============================================================================ + + @Test + fun `BillingFlowParams Builder has enableDeveloperBillingOption method`() { + val builderClassName = "com.android.billingclient.api.BillingFlowParams\$Builder" + val paramsClassName = "com.android.billingclient.api.DeveloperBillingOptionParams" + + try { + val builderClass = Class.forName(builderClassName) + val paramsClass = Class.forName(paramsClassName) + val method = builderClass.getMethod("enableDeveloperBillingOption", paramsClass) + assertNotNull("enableDeveloperBillingOption method should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("enableDeveloperBillingOption method not found. Requires Billing Library 8.3.0+") + } + } + + // ============================================================================ + // MARK: - BillingClient.Builder (for enableUserChoiceBilling and enableBillingProgram) + // Used in: OpenIapModule connection setup + // ============================================================================ + + @Test + fun `BillingClient Builder class exists`() { + assertClassExists( + "com.android.billingclient.api.BillingClient\$Builder", + "any version" + ) + } + + @Test + fun `BillingClient Builder has enableUserChoiceBilling method`() { + val builderClassName = "com.android.billingclient.api.BillingClient\$Builder" + val listenerClassName = "com.android.billingclient.api.UserChoiceBillingListener" + + try { + val builderClass = Class.forName(builderClassName) + val listenerClass = Class.forName(listenerClassName) + val method = builderClass.getMethod("enableUserChoiceBilling", listenerClass) + assertNotNull("enableUserChoiceBilling method should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("enableUserChoiceBilling method not found. Requires Billing Library 5.0+") + } + } + + @Test + fun `BillingClient Builder has enableBillingProgram method`() { + val builderClassName = "com.android.billingclient.api.BillingClient\$Builder" + val paramsClassName = "com.android.billingclient.api.EnableBillingProgramParams" + + try { + val builderClass = Class.forName(builderClassName) + val paramsClass = Class.forName(paramsClassName) + val method = builderClass.getMethod("enableBillingProgram", paramsClass) + assertNotNull("enableBillingProgram method should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("enableBillingProgram method not found. Requires Billing Library 8.3.0+") + } + } + + // ============================================================================ + // MARK: - BillingClient methods (called via reflection) + // ============================================================================ + + @Test + fun `BillingClient has isBillingProgramAvailableAsync method`() { + val clientClassName = "com.android.billingclient.api.BillingClient" + val listenerClassName = "com.android.billingclient.api.BillingProgramAvailabilityListener" + + try { + val clientClass = Class.forName(clientClassName) + val listenerClass = Class.forName(listenerClassName) + val method = clientClass.getMethod( + "isBillingProgramAvailableAsync", + Int::class.javaPrimitiveType, + listenerClass + ) + assertNotNull("isBillingProgramAvailableAsync method should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("isBillingProgramAvailableAsync method not found. Requires Billing Library 8.2.0+") + } + } + + @Test + fun `BillingClient has createBillingProgramReportingDetailsAsync method`() { + // Note: In Billing Library 8.3.0+, this method takes (BillingProgramReportingDetailsParams, Listener) + // OpenIapModule currently uses (int, Listener) which may need updating + val clientClassName = "com.android.billingclient.api.BillingClient" + val paramsClassName = "com.android.billingclient.api.BillingProgramReportingDetailsParams" + val listenerClassName = "com.android.billingclient.api.BillingProgramReportingDetailsListener" + + try { + val clientClass = Class.forName(clientClassName) + val paramsClass = Class.forName(paramsClassName) + val listenerClass = Class.forName(listenerClassName) + val method = clientClass.getMethod( + "createBillingProgramReportingDetailsAsync", + paramsClass, + listenerClass + ) + assertNotNull("createBillingProgramReportingDetailsAsync method should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("createBillingProgramReportingDetailsAsync(BillingProgramReportingDetailsParams, Listener) not found. Requires Billing Library 8.3.0+") + } + } + + @Test + fun `BillingClient has launchExternalLink method`() { + val clientClassName = "com.android.billingclient.api.BillingClient" + val paramsClassName = "com.android.billingclient.api.LaunchExternalLinkParams" + val listenerClassName = "com.android.billingclient.api.LaunchExternalLinkResponseListener" + + try { + val clientClass = Class.forName(clientClassName) + val paramsClass = Class.forName(paramsClassName) + val listenerClass = Class.forName(listenerClassName) + val method = clientClass.getMethod( + "launchExternalLink", + android.app.Activity::class.java, + paramsClass, + listenerClass + ) + assertNotNull("launchExternalLink method should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: ${e.message}") + } catch (e: NoSuchMethodException) { + fail("launchExternalLink method not found. Requires Billing Library 8.2.0+") + } + } + + // ============================================================================ + // MARK: - Core Billing Classes + // ============================================================================ + + @Test + fun `BillingClient class exists`() { + assertClassExists("com.android.billingclient.api.BillingClient", "any version") + } + + @Test + fun `BillingFlowParams class exists`() { + assertClassExists("com.android.billingclient.api.BillingFlowParams", "any version") + } + + @Test + fun `BillingResult class exists`() { + assertClassExists("com.android.billingclient.api.BillingResult", "any version") + } + + // ============================================================================ + // MARK: - Helper Methods + // ============================================================================ + + private fun assertClassExists(className: String, minVersion: String) { + try { + val clazz = Class.forName(className) + assertNotNull("$className should exist", clazz) + } catch (e: ClassNotFoundException) { + fail("$className not found. Requires Billing Library $minVersion") + } + } + + private fun assertClassDoesNotExist(className: String) { + try { + Class.forName(className) + fail("Class should NOT exist at: $className") + } catch (e: ClassNotFoundException) { + // Expected - the class should not exist + assertTrue("Class correctly does not exist at $className", true) + } + } + + private fun assertClassHasMethod( + className: String, + methodName: String, + vararg paramTypes: Class<*> + ) { + try { + val clazz = Class.forName(className) + val method = clazz.getMethod(methodName, *paramTypes) + assertNotNull("$className.$methodName should exist", method) + } catch (e: ClassNotFoundException) { + fail("Class not found: $className") + } catch (e: NoSuchMethodException) { + val params = paramTypes.joinToString(", ") { it.simpleName } + fail("Method not found: $className.$methodName($params)") + } + } +} diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 4b738095..4813d301 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3579,7 +3579,7 @@ public data class RequestPurchaseAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, /** * Personalized offer flag (Android). * When true, indicates the price was customized for this user. @@ -3606,7 +3606,7 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String @@ -3614,7 +3614,7 @@ public data class RequestPurchaseAndroidProps( val skus = (json["skus"] as? List<*>)?.mapNotNull { it as? String } if (skus == null) return null return RequestPurchaseAndroidProps( - developerBillingOption = developerBillingOption, + developerBillingOptionAndroid = developerBillingOptionAndroid, isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, @@ -3625,7 +3625,7 @@ public data class RequestPurchaseAndroidProps( } fun toJson(): Map = mapOf( - "developerBillingOption" to developerBillingOption?.toJson(), + "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, @@ -3798,7 +3798,7 @@ public data class RequestSubscriptionAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, /** * Personalized offer flag (Android). * When true, indicates the price was customized for this user. @@ -3837,7 +3837,7 @@ public data class RequestSubscriptionAndroidProps( ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOption = (json["developerBillingOption"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } + val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String @@ -3848,7 +3848,7 @@ public data class RequestSubscriptionAndroidProps( val subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as? Map)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) } if (skus == null) return null return RequestSubscriptionAndroidProps( - developerBillingOption = developerBillingOption, + developerBillingOptionAndroid = developerBillingOptionAndroid, isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, @@ -3862,7 +3862,7 @@ public data class RequestSubscriptionAndroidProps( } fun toJson(): Map = mapOf( - "developerBillingOption" to developerBillingOption?.toJson(), + "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index d76ef2e4..152e9163 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1382,7 +1382,7 @@ public struct RequestPurchaseAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? /// Personalized offer flag (Android). /// When true, indicates the price was customized for this user. public var isOfferPersonalizedAndroid: Bool? @@ -1398,14 +1398,14 @@ public struct RequestPurchaseAndroidProps: Codable { public var skus: [String] public init( - developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, offerTokenAndroid: String? = nil, skus: [String] ) { - self.developerBillingOption = developerBillingOption + self.developerBillingOptionAndroid = developerBillingOptionAndroid self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid @@ -1552,7 +1552,7 @@ public struct RequestSubscriptionAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? /// Personalized offer flag (Android). /// When true, indicates the price was customized for this user. public var isOfferPersonalizedAndroid: Bool? @@ -1574,7 +1574,7 @@ public struct RequestSubscriptionAndroidProps: Codable { public var subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? public init( - developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, isOfferPersonalizedAndroid: Bool? = nil, obfuscatedAccountIdAndroid: String? = nil, obfuscatedProfileIdAndroid: String? = nil, @@ -1584,7 +1584,7 @@ public struct RequestSubscriptionAndroidProps: Codable { subscriptionOffers: [AndroidSubscriptionOfferInput]? = nil, subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = nil ) { - self.developerBillingOption = developerBillingOption + self.developerBillingOptionAndroid = developerBillingOptionAndroid self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index d4b6ebb8..452f904f 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3600,7 +3600,7 @@ class PurchaseOptions { class RequestPurchaseAndroidProps { const RequestPurchaseAndroidProps({ - this.developerBillingOption, + this.developerBillingOptionAndroid, this.isOfferPersonalizedAndroid, this.obfuscatedAccountIdAndroid, this.obfuscatedProfileIdAndroid, @@ -3611,7 +3611,7 @@ class RequestPurchaseAndroidProps { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - final DeveloperBillingOptionParamsAndroid? developerBillingOption; + final DeveloperBillingOptionParamsAndroid? developerBillingOptionAndroid; /// Personalized offer flag (Android). /// When true, indicates the price was customized for this user. final bool? isOfferPersonalizedAndroid; @@ -3628,7 +3628,7 @@ class RequestPurchaseAndroidProps { factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOptionAndroid: json['developerBillingOptionAndroid'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOptionAndroid'] as Map) : null, isOfferPersonalizedAndroid: json['isOfferPersonalizedAndroid'] as bool?, obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, @@ -3639,7 +3639,7 @@ class RequestPurchaseAndroidProps { Map toJson() { return { - 'developerBillingOption': developerBillingOption?.toJson(), + 'developerBillingOptionAndroid': developerBillingOptionAndroid?.toJson(), 'isOfferPersonalizedAndroid': isOfferPersonalizedAndroid, 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, @@ -3803,7 +3803,7 @@ class RequestPurchasePropsByPlatforms { class RequestSubscriptionAndroidProps { const RequestSubscriptionAndroidProps({ - this.developerBillingOption, + this.developerBillingOptionAndroid, this.isOfferPersonalizedAndroid, this.obfuscatedAccountIdAndroid, this.obfuscatedProfileIdAndroid, @@ -3817,7 +3817,7 @@ class RequestSubscriptionAndroidProps { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - final DeveloperBillingOptionParamsAndroid? developerBillingOption; + final DeveloperBillingOptionParamsAndroid? developerBillingOptionAndroid; /// Personalized offer flag (Android). /// When true, indicates the price was customized for this user. final bool? isOfferPersonalizedAndroid; @@ -3840,7 +3840,7 @@ class RequestSubscriptionAndroidProps { factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + developerBillingOptionAndroid: json['developerBillingOptionAndroid'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOptionAndroid'] as Map) : null, isOfferPersonalizedAndroid: json['isOfferPersonalizedAndroid'] as bool?, obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, @@ -3854,7 +3854,7 @@ class RequestSubscriptionAndroidProps { Map toJson() { return { - 'developerBillingOption': developerBillingOption?.toJson(), + 'developerBillingOptionAndroid': developerBillingOptionAndroid?.toJson(), 'isOfferPersonalizedAndroid': isOfferPersonalizedAndroid, 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index bd283acb..8a327690 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -3203,7 +3203,7 @@ class RequestPurchaseAndroidProps: ## Offer token for one-time purchase discounts (Android 7.0+). var offer_token_android: String ## Developer billing option parameters for external payments flow (8.3.0+). - var developer_billing_option: DeveloperBillingOptionParamsAndroid + var developer_billing_option_android: DeveloperBillingOptionParamsAndroid static func from_dict(data: Dictionary) -> RequestPurchaseAndroidProps: var obj = RequestPurchaseAndroidProps.new() @@ -3217,11 +3217,11 @@ class RequestPurchaseAndroidProps: obj.is_offer_personalized_android = data["isOfferPersonalizedAndroid"] if data.has("offerTokenAndroid") and data["offerTokenAndroid"] != null: obj.offer_token_android = data["offerTokenAndroid"] - if data.has("developerBillingOption") and data["developerBillingOption"] != null: - if data["developerBillingOption"] is Dictionary: - obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + if data.has("developerBillingOptionAndroid") and data["developerBillingOptionAndroid"] != null: + if data["developerBillingOptionAndroid"] is Dictionary: + obj.developer_billing_option_android = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOptionAndroid"]) else: - obj.developer_billing_option = data["developerBillingOption"] + obj.developer_billing_option_android = data["developerBillingOptionAndroid"] return obj func to_dict() -> Dictionary: @@ -3236,11 +3236,11 @@ class RequestPurchaseAndroidProps: dict["isOfferPersonalizedAndroid"] = is_offer_personalized_android if offer_token_android != null: dict["offerTokenAndroid"] = offer_token_android - if developer_billing_option != null: - if developer_billing_option.has_method("to_dict"): - dict["developerBillingOption"] = developer_billing_option.to_dict() + if developer_billing_option_android != null: + if developer_billing_option_android.has_method("to_dict"): + dict["developerBillingOptionAndroid"] = developer_billing_option_android.to_dict() else: - dict["developerBillingOption"] = developer_billing_option + dict["developerBillingOptionAndroid"] = developer_billing_option_android return dict class RequestPurchaseIosProps: @@ -3425,7 +3425,7 @@ class RequestSubscriptionAndroidProps: ## Product-level replacement parameters (8.1.0+) var subscription_product_replacement_params: SubscriptionProductReplacementParamsAndroid ## Developer billing option parameters for external payments flow (8.3.0+). - var developer_billing_option: DeveloperBillingOptionParamsAndroid + var developer_billing_option_android: DeveloperBillingOptionParamsAndroid static func from_dict(data: Dictionary) -> RequestSubscriptionAndroidProps: var obj = RequestSubscriptionAndroidProps.new() @@ -3454,11 +3454,11 @@ class RequestSubscriptionAndroidProps: obj.subscription_product_replacement_params = SubscriptionProductReplacementParamsAndroid.from_dict(data["subscriptionProductReplacementParams"]) else: obj.subscription_product_replacement_params = data["subscriptionProductReplacementParams"] - if data.has("developerBillingOption") and data["developerBillingOption"] != null: - if data["developerBillingOption"] is Dictionary: - obj.developer_billing_option = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOption"]) + if data.has("developerBillingOptionAndroid") and data["developerBillingOptionAndroid"] != null: + if data["developerBillingOptionAndroid"] is Dictionary: + obj.developer_billing_option_android = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOptionAndroid"]) else: - obj.developer_billing_option = data["developerBillingOption"] + obj.developer_billing_option_android = data["developerBillingOptionAndroid"] return obj func to_dict() -> Dictionary: @@ -3488,11 +3488,11 @@ class RequestSubscriptionAndroidProps: dict["subscriptionProductReplacementParams"] = subscription_product_replacement_params.to_dict() else: dict["subscriptionProductReplacementParams"] = subscription_product_replacement_params - if developer_billing_option != null: - if developer_billing_option.has_method("to_dict"): - dict["developerBillingOption"] = developer_billing_option.to_dict() + if developer_billing_option_android != null: + if developer_billing_option_android.has_method("to_dict"): + dict["developerBillingOptionAndroid"] = developer_billing_option_android.to_dict() else: - dict["developerBillingOption"] = developer_billing_option + dict["developerBillingOptionAndroid"] = developer_billing_option_android return dict class RequestSubscriptionIosProps: diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index ce29106b..74f0e4c0 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1193,7 +1193,7 @@ export interface RequestPurchaseAndroidProps { * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - developerBillingOption?: (DeveloperBillingOptionParamsAndroid | null); + developerBillingOptionAndroid?: (DeveloperBillingOptionParamsAndroid | null); /** * Personalized offer flag (Android). * When true, indicates the price was customized for this user. @@ -1279,7 +1279,7 @@ export interface RequestSubscriptionAndroidProps { * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - developerBillingOption?: (DeveloperBillingOptionParamsAndroid | null); + developerBillingOptionAndroid?: (DeveloperBillingOptionParamsAndroid | null); /** * Personalized offer flag (Android). * When true, indicates the price was customized for this user. diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql index 4e2a61ef..ad13adc9 100644 --- a/packages/gql/src/type-android.graphql +++ b/packages/gql/src/type-android.graphql @@ -376,7 +376,7 @@ input RequestPurchaseAndroidProps { When provided, the purchase flow will show a side-by-side choice between Google Play Billing and the developer's external payment option. """ - developerBillingOption: DeveloperBillingOptionParamsAndroid + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid } input RequestSubscriptionAndroidProps { @@ -420,7 +420,7 @@ input RequestSubscriptionAndroidProps { When provided, the purchase flow will show a side-by-side choice between Google Play Billing and the developer's external payment option. """ - developerBillingOption: DeveloperBillingOptionParamsAndroid + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid } # Subscription Replacement (Google Play Billing Library 8.1.0+) From 0c90a0604d69112b4d2b55855d58b0c367e2c642 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 11:04:03 +0900 Subject: [PATCH 14/16] fix(google): correct Billing Library reflection paths and add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix SubscriptionProductReplacementParams class path (Issue #70) - Was: BillingFlowParams$SubscriptionProductReplacementParams - Now: BillingFlowParams$ProductDetailsParams$SubscriptionProductReplacementParams - Fix createBillingProgramReportingDetails bugs - Callback method: onBillingProgramReportingDetailsResponse β†’ onCreateBillingProgramReportingDetailsResponse - Method signature: (int, Listener) β†’ (BillingProgramReportingDetailsParams, Listener) - Update KDoc version: 8.2.0+ β†’ 8.3.0+ - Add BillingLibraryClassPathTest with 65+ tests - Validates all reflection-based class paths match actual Billing Library - Tests callback method names and signatures - Prevents future issues like #70 - Update docs with BillingProgramReportingDetailsParams note Fixes #70 Co-Authored-By: Claude Opus 4.5 --- packages/docs/src/pages/docs/apis/android.tsx | 7 ++++ .../java/dev/hyo/openiap/OpenIapModule.kt | 32 ++++++++++++--- .../openiap/BillingLibraryClassPathTest.kt | 41 +++++++++++++++++-- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/android.tsx b/packages/docs/src/pages/docs/apis/android.tsx index 694438ed..42122044 100644 --- a/packages/docs/src/pages/docs/apis/android.tsx +++ b/packages/docs/src/pages/docs/apis/android.tsx @@ -353,6 +353,13 @@ suspend fun launchExternalLink( Step 3: Create reporting details after successful payment. Returns external transaction token for reporting.

    +
    +

    + Note: This API uses{' '} + BillingProgramReportingDetailsParams internally, which + requires Billing Library 8.3.0+. OpenIAP handles this automatically. +

    +
    {`// Returns BillingProgramReportingDetailsAndroid with externalTransactionToken // Token must be reported to Google Play backend within 24 hours // Throws OpenIapError.NotPrepared if billing client not ready 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 e2604326..84a7ef1f 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 @@ -499,9 +499,11 @@ class OpenIapModule( } /** - * Create reporting details for transactions made outside of Google Play Billing (8.2.0+) + * Create reporting details for transactions made outside of Google Play Billing (8.3.0+) * This is the new API that replaces createAlternativeBillingReportingToken for external offers. * + * Note: This method uses BillingProgramReportingDetailsParams which was introduced in 8.3.0. + * * @param program The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER) * @return Reporting details containing the external transaction token */ @@ -526,7 +528,8 @@ class OpenIapModule( listenerClass.classLoader, arrayOf(listenerClass) ) { _, method, args -> - if (method.name == "onBillingProgramReportingDetailsResponse") { + // Note: Callback method name is onCreateBillingProgramReportingDetailsResponse (not onBillingProgramReportingDetailsResponse) + if (method.name == "onCreateBillingProgramReportingDetailsResponse") { val result = args?.get(0) as? BillingResult val details = args?.getOrNull(1) @@ -556,14 +559,33 @@ class OpenIapModule( null } + // Build BillingProgramReportingDetailsParams using reflection (Billing Library 8.3.0+) + val paramsClass = Class.forName("com.android.billingclient.api.BillingProgramReportingDetailsParams") + val paramsBuilderClass = Class.forName("com.android.billingclient.api.BillingProgramReportingDetailsParams\$Builder") + + val newBuilderMethod = paramsClass.getMethod("newBuilder") + val paramsBuilder = newBuilderMethod.invoke(null) + + // Set billing program + val setBillingProgramMethod = paramsBuilderClass.getMethod("setBillingProgram", Int::class.javaPrimitiveType) + setBillingProgramMethod.invoke(paramsBuilder, billingProgramConstant) + + // Build the params + val buildMethod = paramsBuilderClass.getMethod("build") + val reportingParams = buildMethod.invoke(paramsBuilder) + + // Call createBillingProgramReportingDetailsAsync with (BillingProgramReportingDetailsParams, Listener) val method = client.javaClass.getMethod( "createBillingProgramReportingDetailsAsync", - Int::class.javaPrimitiveType, + paramsClass, listenerClass ) - method.invoke(client, billingProgramConstant, listener) + method.invoke(client, reportingParams, listener) } catch (e: NoSuchMethodException) { - OpenIapLog.e("createBillingProgramReportingDetailsAsync not found. Requires Billing Library 8.2.0+", e, TAG) + OpenIapLog.e("createBillingProgramReportingDetailsAsync not found. Requires Billing Library 8.3.0+", e, TAG) + throw OpenIapError.FeatureNotSupported + } catch (e: ClassNotFoundException) { + OpenIapLog.e("BillingProgramReportingDetailsParams not found. Requires Billing Library 8.3.0+", e, TAG) throw OpenIapError.FeatureNotSupported } catch (e: Exception) { OpenIapLog.e("Failed to create billing program reporting details: ${e.message}", e, TAG) diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt index d723e609..7059bda1 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/BillingLibraryClassPathTest.kt @@ -241,15 +241,16 @@ class BillingLibraryClassPathTest { } // ============================================================================ - // MARK: - BillingProgramReportingDetailsListener (Billing Library 8.2.0+) + // MARK: - BillingProgramReportingDetailsListener (Billing Library 8.3.0+) // Used in: OpenIapModule.createBillingProgramReportingDetails() + // Note: Requires BillingProgramReportingDetailsParams in 8.3.0+ // ============================================================================ @Test fun `BillingProgramReportingDetailsListener class exists`() { assertClassExists( "com.android.billingclient.api.BillingProgramReportingDetailsListener", - "8.2.0+" + "8.3.0+" ) } @@ -274,6 +275,39 @@ class BillingLibraryClassPathTest { ) } + @Test + fun `BillingProgramReportingDetailsParams Builder class exists`() { + assertClassExists( + "com.android.billingclient.api.BillingProgramReportingDetailsParams\$Builder", + "8.3.0+" + ) + } + + @Test + fun `BillingProgramReportingDetailsParams has newBuilder method`() { + assertClassHasMethod( + "com.android.billingclient.api.BillingProgramReportingDetailsParams", + "newBuilder" + ) + } + + @Test + fun `BillingProgramReportingDetailsParams Builder has setBillingProgram method`() { + assertClassHasMethod( + "com.android.billingclient.api.BillingProgramReportingDetailsParams\$Builder", + "setBillingProgram", + Int::class.javaPrimitiveType!! + ) + } + + @Test + fun `BillingProgramReportingDetailsParams Builder has build method`() { + assertClassHasMethod( + "com.android.billingclient.api.BillingProgramReportingDetailsParams\$Builder", + "build" + ) + } + // ============================================================================ // MARK: - LaunchExternalLinkParams (Billing Library 6.0+/8.2.0+) // Used in: OpenIapModule.launchExternalLink() @@ -640,8 +674,7 @@ class BillingLibraryClassPathTest { @Test fun `BillingClient has createBillingProgramReportingDetailsAsync method`() { - // Note: In Billing Library 8.3.0+, this method takes (BillingProgramReportingDetailsParams, Listener) - // OpenIapModule currently uses (int, Listener) which may need updating + // Billing Library 8.3.0+: Takes (BillingProgramReportingDetailsParams, Listener) val clientClassName = "com.android.billingclient.api.BillingClient" val paramsClassName = "com.android.billingclient.api.BillingProgramReportingDetailsParams" val listenerClassName = "com.android.billingclient.api.BillingProgramReportingDetailsListener" From ec9e6a5489e460d3cd0c609ec734343639467fc7 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 11:14:20 +0900 Subject: [PATCH 15/16] fix: address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix developerBillingOption β†’ developerBillingOptionAndroid in docs - Add multi-SKU guard for one-time purchases with offerToken (A single offerToken cannot be applied to multiple SKUs) Co-Authored-By: Claude Opus 4.5 --- knowledge/_claude-context/context.md | 2 +- knowledge/internal/01-naming-conventions.md | 2 +- .../play/java/dev/hyo/openiap/OpenIapModule.kt | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index c99583a3..2e93b86a 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -88,7 +88,7 @@ input RequestPurchaseAndroidProps { isOfferPersonalizedAndroid: Boolean # Android-only feature obfuscatedAccountIdAndroid: String # Android-only feature obfuscatedProfileIdAndroid: String # Android-only feature - developerBillingOption: DeveloperBillingOptionParamsAndroid # Type has suffix + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid # Android-specific field } # INCORRECT - Missing Android suffix diff --git a/knowledge/internal/01-naming-conventions.md b/knowledge/internal/01-naming-conventions.md index 44778e27..255dd75c 100644 --- a/knowledge/internal/01-naming-conventions.md +++ b/knowledge/internal/01-naming-conventions.md @@ -70,7 +70,7 @@ input RequestPurchaseAndroidProps { isOfferPersonalizedAndroid: Boolean # Android-only feature obfuscatedAccountIdAndroid: String # Android-only feature obfuscatedProfileIdAndroid: String # Android-only feature - developerBillingOption: DeveloperBillingOptionParamsAndroid # Type has suffix + developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid # Android-specific field } # INCORRECT - Missing Android suffix 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 84a7ef1f..e2995868 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 @@ -867,6 +867,21 @@ class OpenIapModule( val paramsList = mutableListOf() val requestedOffersBySku = mutableMapOf>() + // Reject multi-SKU one-time purchase requests when offerToken is provided + // A single offerToken cannot be applied to multiple SKUs + if (androidArgs.type == ProductQueryType.InApp && + !androidArgs.offerToken.isNullOrEmpty() && + androidArgs.skus.size > 1) { + OpenIapLog.w( + "offerTokenAndroid requires a single SKU. Provided SKUs: ${androidArgs.skus}", + TAG + ) + val err = OpenIapError.SkuOfferMismatch + for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } + currentPurchaseCallback?.invoke(Result.success(emptyList())) + return + } + if (androidArgs.type == ProductQueryType.Subs) { for (offer in androidArgs.subscriptionOffers.orEmpty()) { if (offer.offerToken.isNotEmpty()) { From 7eeb8d0d75da83e4d1e61527360c8506b95fa08b Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 20 Jan 2026 11:34:27 +0900 Subject: [PATCH 16/16] refactor: remove Android suffix from input type fields BREAKING CHANGE: Fields inside RequestPurchaseAndroidProps and RequestSubscriptionAndroidProps no longer use Android suffix. Before: google: { offerTokenAndroid: "...", isOfferPersonalizedAndroid: true } After: google: { offerToken: "...", isOfferPersonalized: true } Renamed fields: - offerTokenAndroid -> offerToken - isOfferPersonalizedAndroid -> isOfferPersonalized - obfuscatedAccountIdAndroid -> obfuscatedAccountId - obfuscatedProfileIdAndroid -> obfuscatedProfileId - purchaseTokenAndroid -> purchaseToken - replacementModeAndroid -> replacementMode - developerBillingOptionAndroid -> developerBillingOption Note: Response type fields (DiscountOffer.offerTokenAndroid, PurchaseAndroid.obfuscatedAccountIdAndroid, etc.) retain their suffix as they are part of cross-platform return types. Co-Authored-By: Claude Opus 4.5 --- knowledge/_claude-context/context.md | 31 ++-- knowledge/internal/01-naming-conventions.md | 54 +++---- packages/apple/Sources/Models/Types.swift | 74 +++++----- packages/docs/src/pages/docs/events.tsx | 2 +- .../docs/src/pages/docs/features/discount.tsx | 4 +- .../pages/docs/features/external-purchase.tsx | 8 +- .../subscription-upgrade-downgrade.tsx | 14 +- .../docs/src/pages/docs/types/alternative.tsx | 6 +- .../docs/src/pages/docs/types/request.tsx | 14 +- .../screens/AlternativeBillingScreen.kt | 2 +- .../martie/screens/SubscriptionFlowScreen.kt | 50 +++---- .../java/dev/hyo/openiap/OpenIapModule.kt | 10 +- .../java/dev/hyo/openiap/OpenIapViewModel.kt | 16 +-- .../dev/hyo/openiap/helpers/SharedHelpers.kt | 28 ++-- .../src/main/java/dev/hyo/openiap/Types.kt | 96 ++++++------- .../java/dev/hyo/openiap/OpenIapModule.kt | 12 +- .../java/dev/hyo/openiap/OpenIapViewModel.kt | 16 +-- .../java/dev/hyo/openiap/helpers/Helpers.kt | 32 ++--- .../hyo/openiap/StandardizedOfferTypesTest.kt | 48 +++---- packages/gql/src/generated/Types.kt | 96 ++++++------- packages/gql/src/generated/Types.swift | 74 +++++----- packages/gql/src/generated/types.dart | 96 ++++++------- packages/gql/src/generated/types.gd | 134 +++++++++--------- packages/gql/src/generated/types.ts | 30 ++-- packages/gql/src/type-android.graphql | 30 ++-- 25 files changed, 490 insertions(+), 487 deletions(-) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 2e93b86a..2ec75d8c 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -78,39 +78,40 @@ fun buildModuleAndroid() ### GraphQL Input Types (API Fields) -All platform-specific fields in GraphQL input types MUST use the platform suffix: +Fields inside platform-specific input types do NOT need platform suffix (the type name already indicates the platform): ```graphql -# CORRECT - All Android-specific fields have Android suffix +# CORRECT - Fields inside AndroidProps don't need Android suffix input RequestPurchaseAndroidProps { skus: [String!]! # Cross-platform, no suffix - offerTokenAndroid: String # Android-only feature - isOfferPersonalizedAndroid: Boolean # Android-only feature - obfuscatedAccountIdAndroid: String # Android-only feature - obfuscatedProfileIdAndroid: String # Android-only feature - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid # Android-specific field + offerToken: String # No suffix - already in Android type + isOfferPersonalized: Boolean # No suffix - already in Android type + obfuscatedAccountId: String # No suffix - already in Android type + obfuscatedProfileId: String # No suffix - already in Android type + developerBillingOption: DeveloperBillingOptionParamsAndroid # Type has suffix (cross-platform type) } -# INCORRECT - Missing Android suffix +# INCORRECT - Redundant Android suffix inside Android-specific type input RequestPurchaseAndroidProps { - offerToken: String # ❌ Should be offerTokenAndroid - isOfferPersonalized: Boolean # ❌ Should be isOfferPersonalizedAndroid + offerTokenAndroid: String # ❌ Redundant - type already indicates Android + isOfferPersonalizedAndroid: Boolean # ❌ Redundant - type already indicates Android } ``` ### Why This Matters -1. **Cross-platform consumers** (React Native, Flutter, etc.) see all fields -2. Without suffix, it's unclear which platform the field applies to -3. Consistency makes documentation and code generation predictable +1. **Parent type context**: `RequestPurchaseAndroidProps` already indicates Android +2. **Cleaner API**: `google: { offerToken: "..." }` is cleaner than `google: { offerTokenAndroid: "..." }` +3. **Type names still use suffix**: Cross-platform types like `DeveloperBillingOptionParamsAndroid` keep the suffix ### Field Suffix Rules | Field Location | Suffix Required? | Example | |----------------|------------------|---------| -| Android-only input type | YES for Android features | `offerTokenAndroid`, `isOfferPersonalizedAndroid` | -| iOS-only input type | YES for iOS features | `appAccountTokenIOS`, `uuid` β†’ `appAccountTokenIOS` | +| Inside Android-only input type | NO | `offerToken` in `RequestPurchaseAndroidProps` | +| Inside iOS-only input type | NO | `appAccountToken` in `RequestPurchaseIOSProps` | | Cross-platform type | YES for platform-specific | `nameAndroid` in `ProductAndroid` | +| Cross-platform type reference | YES | `developerBillingOption: DeveloperBillingOptionParamsAndroid` | | Internal implementation | NO (not API) | `val offerToken` in Kotlin data class | ### Internal vs API Fields diff --git a/knowledge/internal/01-naming-conventions.md b/knowledge/internal/01-naming-conventions.md index 255dd75c..a0c2516d 100644 --- a/knowledge/internal/01-naming-conventions.md +++ b/knowledge/internal/01-naming-conventions.md @@ -60,55 +60,57 @@ fun buildModuleAndroid() ### GraphQL Input Types (API Fields) -All platform-specific fields in GraphQL input types MUST use the platform suffix: +Fields inside platform-specific input types do NOT need platform suffix (the type name already indicates the platform): ```graphql -# CORRECT - All Android-specific fields have Android suffix +# CORRECT - Fields inside AndroidProps don't need Android suffix input RequestPurchaseAndroidProps { skus: [String!]! # Cross-platform, no suffix - offerTokenAndroid: String # Android-only feature - isOfferPersonalizedAndroid: Boolean # Android-only feature - obfuscatedAccountIdAndroid: String # Android-only feature - obfuscatedProfileIdAndroid: String # Android-only feature - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid # Android-specific field + offerToken: String # No suffix - already in Android type + isOfferPersonalized: Boolean # No suffix - already in Android type + obfuscatedAccountId: String # No suffix - already in Android type + obfuscatedProfileId: String # No suffix - already in Android type + developerBillingOption: DeveloperBillingOptionParamsAndroid # Type has suffix (cross-platform type) } -# INCORRECT - Missing Android suffix +# INCORRECT - Redundant Android suffix inside Android-specific type input RequestPurchaseAndroidProps { - offerToken: String # ❌ Should be offerTokenAndroid - isOfferPersonalized: Boolean # ❌ Should be isOfferPersonalizedAndroid + offerTokenAndroid: String # ❌ Redundant - type already indicates Android + isOfferPersonalizedAndroid: Boolean # ❌ Redundant - type already indicates Android } ``` ### Why This Matters -1. **Cross-platform consumers** (React Native, Flutter, etc.) see all fields -2. Without suffix, it's unclear which platform the field applies to -3. Consistency makes documentation and code generation predictable +1. **Parent type context**: `RequestPurchaseAndroidProps` already indicates Android +2. **Cleaner API**: `google: { offerToken: "..." }` is cleaner than `google: { offerTokenAndroid: "..." }` +3. **Type names still use suffix**: Cross-platform types like `DeveloperBillingOptionParamsAndroid` keep the suffix ### Field Suffix Rules | Field Location | Suffix Required? | Example | |----------------|------------------|---------| -| Android-only input type | YES for Android features | `offerTokenAndroid`, `isOfferPersonalizedAndroid` | -| iOS-only input type | YES for iOS features | `appAccountTokenIOS`, `uuid` β†’ `appAccountTokenIOS` | +| Inside Android-only input type | NO | `offerToken` in `RequestPurchaseAndroidProps` | +| Inside iOS-only input type | NO | `appAccountToken` in `RequestPurchaseIOSProps` | | Cross-platform type | YES for platform-specific | `nameAndroid` in `ProductAndroid` | +| Cross-platform type reference | YES | `developerBillingOption: DeveloperBillingOptionParamsAndroid` | | Internal implementation | NO (not API) | `val offerToken` in Kotlin data class | -### Internal vs API Fields +### Type vs Field Suffix -- **API fields** (GraphQL schema): ALWAYS use platform suffix -- **Internal fields** (Kotlin/Swift data classes not exposed): No suffix needed +- **Type names**: Cross-platform types ALWAYS use platform suffix (`DeveloperBillingOptionParamsAndroid`) +- **Fields in platform-specific inputs**: NO suffix needed (parent type indicates platform) +- **Fields in cross-platform types**: Use suffix for platform-specific fields ```kotlin -// Internal helper data class - no suffix needed -internal data class AndroidPurchaseArgs( - val offerToken: String?, // Internal, no suffix OK - val isOfferPersonalized: Boolean? // Internal, no suffix OK -) - -// But when reading from API props, use the suffixed names: -val offerToken = params.offerTokenAndroid // βœ“ API uses suffix +// Cross-platform SDK usage +requestPurchase { + google { + skus = listOf("product_id") + offerToken = "discount_offer_token" // βœ“ Clean - no redundant suffix + isOfferPersonalized = false + } +} ``` ### Cross-Platform Functions diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 152e9163..0ce716da 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1382,34 +1382,34 @@ public struct RequestPurchaseAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag (Android). + public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + /// Personalized offer flag. /// When true, indicates the price was customized for this user. - public var isOfferPersonalizedAndroid: Bool? + public var isOfferPersonalized: Bool? /// Obfuscated account ID - public var obfuscatedAccountIdAndroid: String? + public var obfuscatedAccountId: String? /// Obfuscated profile ID - public var obfuscatedProfileIdAndroid: String? - /// Offer token for one-time purchase discounts (Android 7.0+). + public var obfuscatedProfileId: String? + /// Offer token for one-time purchase discounts (7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. - public var offerTokenAndroid: String? + public var offerToken: String? /// List of product SKUs public var skus: [String] public init( - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalizedAndroid: Bool? = nil, - obfuscatedAccountIdAndroid: String? = nil, - obfuscatedProfileIdAndroid: String? = nil, - offerTokenAndroid: String? = nil, + developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + isOfferPersonalized: Bool? = nil, + obfuscatedAccountId: String? = nil, + obfuscatedProfileId: String? = nil, + offerToken: String? = nil, skus: [String] ) { - self.developerBillingOptionAndroid = developerBillingOptionAndroid - self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid - self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid - self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid - self.offerTokenAndroid = offerTokenAndroid + self.developerBillingOption = developerBillingOption + self.isOfferPersonalized = isOfferPersonalized + self.obfuscatedAccountId = obfuscatedAccountId + self.obfuscatedProfileId = obfuscatedProfileId + self.offerToken = offerToken self.skus = skus } } @@ -1552,44 +1552,44 @@ public struct RequestSubscriptionAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag (Android). + public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + /// Personalized offer flag. /// When true, indicates the price was customized for this user. - public var isOfferPersonalizedAndroid: Bool? + public var isOfferPersonalized: Bool? /// Obfuscated account ID - public var obfuscatedAccountIdAndroid: String? + public var obfuscatedAccountId: String? /// Obfuscated profile ID - public var obfuscatedProfileIdAndroid: String? + public var obfuscatedProfileId: String? /// Purchase token for upgrades/downgrades - public var purchaseTokenAndroid: String? + public var purchaseToken: String? /// Replacement mode for subscription changes /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) - public var replacementModeAndroid: Int? + public var replacementMode: Int? /// List of subscription SKUs public var skus: [String] /// Subscription offers public var subscriptionOffers: [AndroidSubscriptionOfferInput]? /// Product-level replacement parameters (8.1.0+) - /// Use this instead of replacementModeAndroid for item-level replacement + /// Use this instead of replacementMode for item-level replacement public var subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? public init( - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalizedAndroid: Bool? = nil, - obfuscatedAccountIdAndroid: String? = nil, - obfuscatedProfileIdAndroid: String? = nil, - purchaseTokenAndroid: String? = nil, - replacementModeAndroid: Int? = nil, + developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + isOfferPersonalized: Bool? = nil, + obfuscatedAccountId: String? = nil, + obfuscatedProfileId: String? = nil, + purchaseToken: String? = nil, + replacementMode: Int? = nil, skus: [String], subscriptionOffers: [AndroidSubscriptionOfferInput]? = nil, subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = nil ) { - self.developerBillingOptionAndroid = developerBillingOptionAndroid - self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid - self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid - self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid - self.purchaseTokenAndroid = purchaseTokenAndroid - self.replacementModeAndroid = replacementModeAndroid + self.developerBillingOption = developerBillingOption + self.isOfferPersonalized = isOfferPersonalized + self.obfuscatedAccountId = obfuscatedAccountId + self.obfuscatedProfileId = obfuscatedProfileId + self.purchaseToken = purchaseToken + self.replacementMode = replacementMode self.skus = skus self.subscriptionOffers = subscriptionOffers self.subscriptionProductReplacementParams = subscriptionProductReplacementParams diff --git a/packages/docs/src/pages/docs/events.tsx b/packages/docs/src/pages/docs/events.tsx index 5d7b014f..bb7d4f0d 100644 --- a/packages/docs/src/pages/docs/events.tsx +++ b/packages/docs/src/pages/docs/events.tsx @@ -996,7 +996,7 @@ subscription.cancel();`} Setup AlternativeBillingModeAndroid.UserChoice - enableBillingProgram(EXTERNAL_PAYMENTS) + developerBillingOptionAndroid in requestPurchase + enableBillingProgram(EXTERNAL_PAYMENTS) + developerBillingOption in requestPurchase diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx index b97dc287..bf78b34c 100644 --- a/packages/docs/src/pages/docs/features/discount.tsx +++ b/packages/docs/src/pages/docs/features/discount.tsx @@ -858,7 +858,7 @@ async function purchaseWithOffer( google: { skus: [product.id], // Include offerTokenAndroid for discounted purchases (Android 7.0+) - offerTokenAndroid: selectedOffer.offerToken, + offerToken: selectedOffer.offerToken, }, }, }); @@ -889,7 +889,7 @@ async function purchaseWithOffer( google = RequestPurchaseAndroidProps( skus = listOf(product.id), // Include offerTokenAndroid for discounted purchases (Android 7.0+) - offerTokenAndroid = selectedOffer.offerToken + offerToken = selectedOffer.offerToken ) ) ) diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index 27b79f18..c6e57127 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -1538,7 +1538,7 @@ async function handlePurchaseWithExternalPayments(productId: string) { await requestPurchase({ google: { skus: [productId], - developerBillingOptionAndroid: { + developerBillingOption: { billingProgram: 'EXTERNAL_PAYMENTS', linkUri: 'https://your-payment-site.com/checkout', launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', @@ -1626,7 +1626,7 @@ suspend fun handlePurchaseWithExternalPayments(productId: String) { RequestPurchasePropsByPlatforms( google = RequestPurchaseAndroidProps( skus = listOf(productId), - developerBillingOptionAndroid = DeveloperBillingOptionParamsAndroid( + developerBillingOption = DeveloperBillingOptionParamsAndroid( billingProgram = BillingProgramAndroid.ExternalPayments, linkUri = "https://your-payment-site.com/checkout", launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp @@ -1701,7 +1701,7 @@ Future handlePurchaseWithExternalPayments(String productId) async { // User will see side-by-side choice dialog await FlutterInappPurchase.instance.requestPurchase( productId, - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid( + developerBillingOption: DeveloperBillingOptionParamsAndroid( billingProgram: BillingProgramAndroid.externalPayments, linkUri: 'https://your-payment-site.com/checkout', launchMode: DeveloperBillingLaunchModeAndroid.launchInExternalBrowserOrApp, @@ -1804,7 +1804,7 @@ func handle_purchase_with_external_payments(product_id: String) -> void: 3 - requestPurchase(developerBillingOptionAndroid: ...) + requestPurchase(developerBillingOption: ...) Launch purchase with developer billing option configured diff --git a/packages/docs/src/pages/docs/features/subscription-upgrade-downgrade.tsx b/packages/docs/src/pages/docs/features/subscription-upgrade-downgrade.tsx index a290fedc..613cec2f 100644 --- a/packages/docs/src/pages/docs/features/subscription-upgrade-downgrade.tsx +++ b/packages/docs/src/pages/docs/features/subscription-upgrade-downgrade.tsx @@ -1212,8 +1212,8 @@ if (currentSub) { // Upgrade to premium with time proration await requestPurchase({ sku: 'premium_monthly', - purchaseTokenAndroid: currentSub.purchaseToken, - replacementModeAndroid: 1, // WITH_TIME_PRORATION + purchaseToken: currentSub.purchaseToken, + replacementMode: 1, // WITH_TIME_PRORATION }); console.log('βœ… Upgrade initiated'); @@ -1376,8 +1376,8 @@ if (premiumPurchase) { // Downgrade - takes effect at next billing cycle await requestPurchase({ sku: 'basic_monthly', - purchaseTokenAndroid: premiumPurchase.purchaseToken, - replacementModeAndroid: 6, // DEFERRED - Change at renewal + purchaseToken: premiumPurchase.purchaseToken, + replacementMode: 6, // DEFERRED - Change at renewal }); console.log('βœ… Downgrade scheduled for next billing cycle'); @@ -1703,7 +1703,7 @@ for purchase in purchases:
    1. Specify replacement mode when needed: Pass{' '} - replacementModeAndroid when you want to + replacementMode when you want to override the default configured in Google Play Console
    2. @@ -1764,8 +1764,8 @@ async function changeSubscription( try { await requestPurchase({ sku: newSku, - purchaseTokenAndroid: currentSub.purchaseToken, - replacementModeAndroid: replacementMode, + purchaseToken: currentSub.purchaseToken, + replacementMode: replacementMode, }); // If DEFERRED, store pending change in your backend diff --git a/packages/docs/src/pages/docs/types/alternative.tsx b/packages/docs/src/pages/docs/types/alternative.tsx index 2314ab0b..095b2a07 100644 --- a/packages/docs/src/pages/docs/types/alternative.tsx +++ b/packages/docs/src/pages/docs/types/alternative.tsx @@ -873,7 +873,7 @@ if (result.isAvailable) { await requestPurchase({ google: { skus: ['product_id'], - developerBillingOptionAndroid: { + developerBillingOption: { billingProgram: 'EXTERNAL_PAYMENTS', linkUri: 'https://your-site.com/checkout', launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', @@ -910,7 +910,7 @@ if (result.isAvailable) { RequestPurchasePropsByPlatforms( google = RequestPurchaseAndroidProps( skus = listOf("product_id"), - developerBillingOptionAndroid = DeveloperBillingOptionParamsAndroid( + developerBillingOption = DeveloperBillingOptionParamsAndroid( billingProgram = BillingProgramAndroid.ExternalPayments, linkUri = "https://your-site.com/checkout", launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp @@ -946,7 +946,7 @@ if (result.isAvailable) { // Purchase with developer billing option await FlutterInappPurchase.instance.requestPurchase( 'product_id', - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid( + developerBillingOption: DeveloperBillingOptionParamsAndroid( billingProgram: BillingProgramAndroid.externalPayments, linkUri: 'https://your-site.com/checkout', launchMode: DeveloperBillingLaunchModeAndroid.launchInExternalBrowserOrApp, diff --git a/packages/docs/src/pages/docs/types/request.tsx b/packages/docs/src/pages/docs/types/request.tsx index bdfcab6e..90f1659d 100644 --- a/packages/docs/src/pages/docs/types/request.tsx +++ b/packages/docs/src/pages/docs/types/request.tsx @@ -525,10 +525,10 @@ await iap.request_purchase(subs_props)`} - offerTokenAndroid + offerToken - Offer token for one-time purchase discounts (Android 7.0+). + Offer token for one-time purchase discounts (7.0+). Pass the offerToken from{' '} oneTimePurchaseOfferDetailsAndroid or{' '} discountOffers to apply a discount. @@ -536,19 +536,19 @@ await iap.request_purchase(subs_props)`} - obfuscatedAccountIdAndroid + obfuscatedAccountId Obfuscated user account ID - obfuscatedProfileIdAndroid + obfuscatedProfileId Obfuscated user profile ID - isOfferPersonalizedAndroid + isOfferPersonalized True if offer is personalized (EU compliance) @@ -594,13 +594,13 @@ await iap.request_purchase(subs_props)`} - purchaseTokenAndroid + purchaseToken Existing subscription token for upgrade/downgrade - replacementModeAndroid + replacementMode How to handle subscription change (proration mode) diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt index 7cd5aa1a..9b703cb5 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt @@ -960,7 +960,7 @@ fun AlternativeBillingScreen(navController: NavController) { RequestPurchasePropsByPlatforms( google = RequestPurchaseAndroidProps( skus = listOf(selectedProduct!!.id), - developerBillingOptionAndroid = DeveloperBillingOptionParamsAndroid( + developerBillingOption = DeveloperBillingOptionParamsAndroid( billingProgram = BillingProgramAndroid.ExternalPayments, linkUri = "https://example.com/checkout?product=${selectedProduct!!.id}", launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt index c1f0759c..d5dfe780 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt @@ -785,10 +785,10 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, - purchaseTokenAndroid = purchaseToken, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, + purchaseToken = purchaseToken, // New 8.1.0+ API: per-product replacement params subscriptionProductReplacementParams = SubscriptionProductReplacementParamsAndroid( oldProductId = IapConstants.PREMIUM_PRODUCT_ID, @@ -1018,11 +1018,11 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, - purchaseTokenAndroid = purchaseToken, - replacementModeAndroid = replacementMode, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, + purchaseToken = purchaseToken, + replacementMode = replacementMode, skus = listOf(PREMIUM_SUBSCRIPTION_PRODUCT_ID), subscriptionOffers = offerInputs ) @@ -1167,11 +1167,11 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, - purchaseTokenAndroid = purchaseToken, - replacementModeAndroid = replacementMode, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, + purchaseToken = purchaseToken, + replacementMode = replacementMode, skus = listOf(product.id), subscriptionOffers = subscriptionOffers ) @@ -1184,9 +1184,9 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Purchase( RequestPurchasePropsByPlatforms( android = RequestPurchaseAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, skus = listOf(product.id) ) ) @@ -1446,11 +1446,11 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Subscription( RequestSubscriptionPropsByPlatforms( android = RequestSubscriptionAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, - purchaseTokenAndroid = null, - replacementModeAndroid = null, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, + purchaseToken = null, + replacementMode = null, skus = listOf(product.id), subscriptionOffers = null ) @@ -1463,9 +1463,9 @@ fun SubscriptionFlowScreen( request = RequestPurchaseProps.Request.Purchase( RequestPurchasePropsByPlatforms( android = RequestPurchaseAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, skus = listOf(product.id) ) ) diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index 44b24c2c..dc6f8da0 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -455,22 +455,22 @@ class OpenIapModule( androidArgs.obfuscatedAccountId?.let { flowBuilder.setObfuscatedAccountId(it) } // For subscription upgrades/downgrades, purchaseToken and obfuscatedProfileId are mutually exclusive - if (androidArgs.type == ProductQueryType.Subs && !androidArgs.purchaseTokenAndroid.isNullOrBlank()) { + if (androidArgs.type == ProductQueryType.Subs && !androidArgs.purchaseToken.isNullOrBlank()) { // This is a subscription upgrade/downgrade - do not set obfuscatedProfileId OpenIapLog.d("=== Subscription Upgrade Flow ===", TAG) - OpenIapLog.d(" - Old Token: ${androidArgs.purchaseTokenAndroid.take(10)}...", TAG) + OpenIapLog.d(" - Old Token: ${androidArgs.purchaseToken.take(10)}...", TAG) OpenIapLog.d(" - Target SKUs: ${androidArgs.skus}", TAG) - OpenIapLog.d(" - Replacement mode: ${androidArgs.replacementModeAndroid}", TAG) + OpenIapLog.d(" - Replacement mode: ${androidArgs.replacementMode}", TAG) OpenIapLog.d(" - Product Details Count: ${paramsList.size}", TAG) paramsList.forEachIndexed { idx, params -> OpenIapLog.d(" - Product[$idx]: SKU=${details[idx].productId}, offerToken=...", TAG) } val updateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder() - .setOldPurchaseToken(androidArgs.purchaseTokenAndroid) + .setOldPurchaseToken(androidArgs.purchaseToken) // Set replacement mode - this is critical for upgrades - val replacementMode = androidArgs.replacementModeAndroid ?: 5 // Default to CHARGE_FULL_PRICE + val replacementMode = androidArgs.replacementMode ?: 5 // Default to CHARGE_FULL_PRICE updateParamsBuilder.setSubscriptionReplacementMode(replacementMode) OpenIapLog.d(" - Final replacement mode: $replacementMode", TAG) diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt index d08d59bc..a96c1e18 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapViewModel.kt @@ -46,9 +46,9 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { val props = when (type) { ProductQueryType.InApp -> { val android = RequestPurchaseAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, skus = skus ) RequestPurchaseProps( @@ -60,11 +60,11 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { } ProductQueryType.Subs -> { val android = RequestSubscriptionAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, - purchaseTokenAndroid = null, - replacementModeAndroid = null, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, + purchaseToken = null, + replacementMode = null, skus = skus, subscriptionOffers = null ) diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt index 01decbaa..004d8343 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt @@ -56,8 +56,8 @@ internal data class AndroidPurchaseArgs( val obfuscatedAccountId: String?, val obfuscatedProfileId: String?, val offerToken: String?, - val purchaseTokenAndroid: String?, - val replacementModeAndroid: Int?, + val purchaseToken: String?, + val replacementMode: Int?, val subscriptionOffers: List?, val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid?, val type: ProductQueryType, @@ -75,12 +75,12 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { ?: throw IllegalArgumentException("Google purchase parameters are required (use 'google' field)") AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalizedAndroid, - obfuscatedAccountId = params.obfuscatedAccountIdAndroid, - obfuscatedProfileId = params.obfuscatedProfileIdAndroid, - offerToken = params.offerTokenAndroid, - purchaseTokenAndroid = null, - replacementModeAndroid = null, + isOfferPersonalized = params.isOfferPersonalized, + obfuscatedAccountId = params.obfuscatedAccountId, + obfuscatedProfileId = params.obfuscatedProfileId, + offerToken = params.offerToken, + purchaseToken = null, + replacementMode = null, subscriptionOffers = null, subscriptionProductReplacementParams = null, type = type, @@ -93,17 +93,17 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { ?: throw IllegalArgumentException("Google subscription parameters are required (use 'google' field)") // For subscription upgrades/downgrades: - // - purchaseTokenAndroid: Identifies which existing subscription to upgrade/downgrade + // - purchaseToken: Identifies which existing subscription to upgrade/downgrade // - obfuscatedProfileId: Optional user identifier for fraud prevention and attribution // Both can be provided together - they serve different purposes and are not mutually exclusive AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalizedAndroid, - obfuscatedAccountId = params.obfuscatedAccountIdAndroid, - obfuscatedProfileId = params.obfuscatedProfileIdAndroid, + isOfferPersonalized = params.isOfferPersonalized, + obfuscatedAccountId = params.obfuscatedAccountId, + obfuscatedProfileId = params.obfuscatedProfileId, offerToken = null, - purchaseTokenAndroid = params.purchaseTokenAndroid, - replacementModeAndroid = params.replacementModeAndroid, + purchaseToken = params.purchaseToken, + replacementMode = params.replacementMode, subscriptionOffers = params.subscriptionOffers, subscriptionProductReplacementParams = params.subscriptionProductReplacementParams, type = type, 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 5f41cd71..fd99e8ce 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 @@ -3496,26 +3496,26 @@ public data class RequestPurchaseAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag (Android). + * Personalized offer flag. * When true, indicates the price was customized for this user. */ - val isOfferPersonalizedAndroid: Boolean? = null, + val isOfferPersonalized: Boolean? = null, /** * Obfuscated account ID */ - val obfuscatedAccountIdAndroid: String? = null, + val obfuscatedAccountId: String? = null, /** * Obfuscated profile ID */ - val obfuscatedProfileIdAndroid: String? = null, + val obfuscatedProfileId: String? = null, /** - * Offer token for one-time purchase discounts (Android 7.0+). + * Offer token for one-time purchase discounts (7.0+). * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers * to apply a discount offer to the purchase. */ - val offerTokenAndroid: String? = null, + val offerToken: String? = null, /** * List of product SKUs */ @@ -3523,30 +3523,30 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean - val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String - val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String - val offerTokenAndroid = json["offerTokenAndroid"] as? String + 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 return RequestPurchaseAndroidProps( - developerBillingOptionAndroid = developerBillingOptionAndroid, - isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, - obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, - obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, - offerTokenAndroid = offerTokenAndroid, + developerBillingOption = developerBillingOption, + isOfferPersonalized = isOfferPersonalized, + obfuscatedAccountId = obfuscatedAccountId, + obfuscatedProfileId = obfuscatedProfileId, + offerToken = offerToken, skus = skus, ) } } fun toJson(): Map = mapOf( - "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), - "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, - "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, - "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, - "offerTokenAndroid" to offerTokenAndroid, + "developerBillingOption" to developerBillingOption?.toJson(), + "isOfferPersonalized" to isOfferPersonalized, + "obfuscatedAccountId" to obfuscatedAccountId, + "obfuscatedProfileId" to obfuscatedProfileId, + "offerToken" to offerToken, "skus" to skus, ) } @@ -3715,29 +3715,29 @@ public data class RequestSubscriptionAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag (Android). + * Personalized offer flag. * When true, indicates the price was customized for this user. */ - val isOfferPersonalizedAndroid: Boolean? = null, + val isOfferPersonalized: Boolean? = null, /** * Obfuscated account ID */ - val obfuscatedAccountIdAndroid: String? = null, + val obfuscatedAccountId: String? = null, /** * Obfuscated profile ID */ - val obfuscatedProfileIdAndroid: String? = null, + val obfuscatedProfileId: String? = null, /** * Purchase token for upgrades/downgrades */ - val purchaseTokenAndroid: String? = null, + val purchaseToken: String? = null, /** * Replacement mode for subscription changes * @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) */ - val replacementModeAndroid: Int? = null, + val replacementMode: Int? = null, /** * List of subscription SKUs */ @@ -3748,29 +3748,29 @@ public data class RequestSubscriptionAndroidProps( val subscriptionOffers: List? = null, /** * Product-level replacement parameters (8.1.0+) - * Use this instead of replacementModeAndroid for item-level replacement + * Use this instead of replacementMode for item-level replacement */ val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = null ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean - val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String - val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String - val purchaseTokenAndroid = json["purchaseTokenAndroid"] as? String - val replacementModeAndroid = (json["replacementModeAndroid"] as? Number)?.toInt() + 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 purchaseToken = json["purchaseToken"] as? String + val replacementMode = (json["replacementMode"] as? Number)?.toInt() 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 return RequestSubscriptionAndroidProps( - developerBillingOptionAndroid = developerBillingOptionAndroid, - isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, - obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, - obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, - purchaseTokenAndroid = purchaseTokenAndroid, - replacementModeAndroid = replacementModeAndroid, + developerBillingOption = developerBillingOption, + isOfferPersonalized = isOfferPersonalized, + obfuscatedAccountId = obfuscatedAccountId, + obfuscatedProfileId = obfuscatedProfileId, + purchaseToken = purchaseToken, + replacementMode = replacementMode, skus = skus, subscriptionOffers = subscriptionOffers, subscriptionProductReplacementParams = subscriptionProductReplacementParams, @@ -3779,12 +3779,12 @@ public data class RequestSubscriptionAndroidProps( } fun toJson(): Map = mapOf( - "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), - "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, - "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, - "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, - "purchaseTokenAndroid" to purchaseTokenAndroid, - "replacementModeAndroid" to replacementModeAndroid, + "developerBillingOption" to developerBillingOption?.toJson(), + "isOfferPersonalized" to isOfferPersonalized, + "obfuscatedAccountId" to obfuscatedAccountId, + "obfuscatedProfileId" to obfuscatedProfileId, + "purchaseToken" to purchaseToken, + "replacementMode" to replacementMode, "skus" to skus, "subscriptionOffers" to subscriptionOffers?.map { it.toJson() }, "subscriptionProductReplacementParams" to subscriptionProductReplacementParams?.toJson(), 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 e2995868..c0a6eb65 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 @@ -873,7 +873,7 @@ class OpenIapModule( !androidArgs.offerToken.isNullOrEmpty() && androidArgs.skus.size > 1) { OpenIapLog.w( - "offerTokenAndroid requires a single SKU. Provided SKUs: ${androidArgs.skus}", + "offerToken requires a single SKU. Provided SKUs: ${androidArgs.skus}", TAG ) val err = OpenIapError.SkuOfferMismatch @@ -983,25 +983,25 @@ class OpenIapModule( } // For subscription upgrades/downgrades, purchaseToken and obfuscatedProfileId are mutually exclusive - if (androidArgs.type == ProductQueryType.Subs && !androidArgs.purchaseTokenAndroid.isNullOrBlank()) { + if (androidArgs.type == ProductQueryType.Subs && !androidArgs.purchaseToken.isNullOrBlank()) { // This is a subscription upgrade/downgrade - do not set obfuscatedProfileId OpenIapLog.d("=== Subscription Upgrade Flow ===", TAG) - OpenIapLog.d(" - Old Token: ${androidArgs.purchaseTokenAndroid.take(10)}...", TAG) + OpenIapLog.d(" - Old Token: ${androidArgs.purchaseToken.take(10)}...", TAG) OpenIapLog.d(" - Target SKUs: ${androidArgs.skus}", TAG) - OpenIapLog.d(" - Replacement mode: ${androidArgs.replacementModeAndroid}", TAG) + OpenIapLog.d(" - Replacement mode: ${androidArgs.replacementMode}", TAG) OpenIapLog.d(" - Product Details Count: ${paramsList.size}", TAG) for ((index, params) in paramsList.withIndex()) { OpenIapLog.d(" - Product[$index]: SKU=${details[index].productId}, offerToken=...", TAG) } val updateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder() - .setOldPurchaseToken(androidArgs.purchaseTokenAndroid) + .setOldPurchaseToken(androidArgs.purchaseToken) // Set replacement mode - this is critical for upgrades // Note: setSubscriptionReplacementMode() is deprecated in Billing 8.1.0 // in favor of SubscriptionProductReplacementParams for per-product control. // However, for single-product upgrades, the legacy API still works. - val replacementMode = androidArgs.replacementModeAndroid ?: 5 // Default to CHARGE_FULL_PRICE + val replacementMode = androidArgs.replacementMode ?: 5 // Default to CHARGE_FULL_PRICE @Suppress("DEPRECATION") updateParamsBuilder.setSubscriptionReplacementMode(replacementMode) OpenIapLog.d(" - Final replacement mode: $replacementMode", TAG) diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt index d08d59bc..a96c1e18 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapViewModel.kt @@ -46,9 +46,9 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { val props = when (type) { ProductQueryType.InApp -> { val android = RequestPurchaseAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, skus = skus ) RequestPurchaseProps( @@ -60,11 +60,11 @@ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { } ProductQueryType.Subs -> { val android = RequestSubscriptionAndroidProps( - isOfferPersonalizedAndroid = null, - obfuscatedAccountIdAndroid = null, - obfuscatedProfileIdAndroid = null, - purchaseTokenAndroid = null, - replacementModeAndroid = null, + isOfferPersonalized = null, + obfuscatedAccountId = null, + obfuscatedProfileId = null, + purchaseToken = null, + replacementMode = null, skus = skus, subscriptionOffers = null ) diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt index fcd2b9e2..fa83ce23 100644 --- a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt +++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt @@ -119,8 +119,8 @@ internal data class AndroidPurchaseArgs( val obfuscatedAccountId: String?, val obfuscatedProfileId: String?, val offerToken: String?, - val purchaseTokenAndroid: String?, - val replacementModeAndroid: Int?, + val purchaseToken: String?, + val replacementMode: Int?, val subscriptionOffers: List?, val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid?, val developerBillingOption: DeveloperBillingOptionParamsAndroid?, @@ -136,15 +136,15 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { ?: throw IllegalArgumentException("Google purchase parameters are required (use 'google' field)") AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalizedAndroid, - obfuscatedAccountId = params.obfuscatedAccountIdAndroid, - obfuscatedProfileId = params.obfuscatedProfileIdAndroid, - offerToken = params.offerTokenAndroid, - purchaseTokenAndroid = null, - replacementModeAndroid = null, + isOfferPersonalized = params.isOfferPersonalized, + obfuscatedAccountId = params.obfuscatedAccountId, + obfuscatedProfileId = params.obfuscatedProfileId, + offerToken = params.offerToken, + purchaseToken = null, + replacementMode = null, subscriptionOffers = null, subscriptionProductReplacementParams = null, - developerBillingOption = params.developerBillingOptionAndroid, + developerBillingOption = params.developerBillingOption, type = type, useAlternativeBilling = useAlternativeBilling ) @@ -155,20 +155,20 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs { ?: throw IllegalArgumentException("Google subscription parameters are required (use 'google' field)") // For subscription upgrades/downgrades: - // - purchaseTokenAndroid: Identifies which existing subscription to upgrade/downgrade + // - purchaseToken: Identifies which existing subscription to upgrade/downgrade // - obfuscatedProfileId: Optional user identifier for fraud prevention and attribution // Both can be provided together - they serve different purposes and are not mutually exclusive AndroidPurchaseArgs( skus = params.skus, - isOfferPersonalized = params.isOfferPersonalizedAndroid, - obfuscatedAccountId = params.obfuscatedAccountIdAndroid, - obfuscatedProfileId = params.obfuscatedProfileIdAndroid, + isOfferPersonalized = params.isOfferPersonalized, + obfuscatedAccountId = params.obfuscatedAccountId, + obfuscatedProfileId = params.obfuscatedProfileId, offerToken = null, - purchaseTokenAndroid = params.purchaseTokenAndroid, - replacementModeAndroid = params.replacementModeAndroid, + purchaseToken = params.purchaseToken, + replacementMode = params.replacementMode, subscriptionOffers = params.subscriptionOffers, subscriptionProductReplacementParams = params.subscriptionProductReplacementParams, - developerBillingOption = params.developerBillingOptionAndroid, + developerBillingOption = params.developerBillingOption, type = type, useAlternativeBilling = useAlternativeBilling ) diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt index 53108816..c77a5787 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt @@ -405,59 +405,59 @@ class StandardizedOfferTypesTest { assertEquals(PaymentMode.FreeTrial, product.subscriptionOffers.first().paymentMode) } - // MARK: - RequestPurchaseAndroidProps offerTokenAndroid Tests + // MARK: - RequestPurchaseAndroidProps offerToken Tests @Test - fun `RequestPurchaseAndroidProps supports offerTokenAndroid for one-time purchases`() { + fun `RequestPurchaseAndroidProps supports offerToken for one-time purchases`() { val props = RequestPurchaseAndroidProps( skus = listOf("premium_upgrade"), - offerTokenAndroid = "discount_offer_token_abc123" + offerToken = "discount_offer_token_abc123" ) assertEquals(listOf("premium_upgrade"), props.skus) - assertEquals("discount_offer_token_abc123", props.offerTokenAndroid) - assertNull(props.isOfferPersonalizedAndroid) - assertNull(props.obfuscatedAccountIdAndroid) + assertEquals("discount_offer_token_abc123", props.offerToken) + assertNull(props.isOfferPersonalized) + assertNull(props.obfuscatedAccountId) } @Test - fun `RequestPurchaseAndroidProps toJson includes offerTokenAndroid`() { + fun `RequestPurchaseAndroidProps toJson includes offerToken`() { val props = RequestPurchaseAndroidProps( skus = listOf("product_id"), - offerTokenAndroid = "test_offer_token", - isOfferPersonalizedAndroid = true + offerToken = "test_offer_token", + isOfferPersonalized = true ) val json = props.toJson() assertEquals(listOf("product_id"), json["skus"]) - assertEquals("test_offer_token", json["offerTokenAndroid"]) - assertEquals(true, json["isOfferPersonalizedAndroid"]) + assertEquals("test_offer_token", json["offerToken"]) + assertEquals(true, json["isOfferPersonalized"]) } @Test - fun `RequestPurchaseAndroidProps fromJson parses offerTokenAndroid`() { + fun `RequestPurchaseAndroidProps fromJson parses offerToken`() { val json = mapOf( "skus" to listOf("sku_001"), - "offerTokenAndroid" to "parsed_offer_token", - "obfuscatedAccountIdAndroid" to "account_123" + "offerToken" to "parsed_offer_token", + "obfuscatedAccountId" to "account_123" ) val props = RequestPurchaseAndroidProps.fromJson(json) assertEquals(listOf("sku_001"), props?.skus) - assertEquals("parsed_offer_token", props?.offerTokenAndroid) - assertEquals("account_123", props?.obfuscatedAccountIdAndroid) + assertEquals("parsed_offer_token", props?.offerToken) + assertEquals("account_123", props?.obfuscatedAccountId) } @Test - fun `RequestPurchaseAndroidProps allows null offerTokenAndroid`() { + fun `RequestPurchaseAndroidProps allows null offerToken`() { val props = RequestPurchaseAndroidProps( skus = listOf("regular_product") ) - assertNull(props.offerTokenAndroid) + assertNull(props.offerToken) val json = props.toJson() - assertNull(json["offerTokenAndroid"]) + assertNull(json["offerToken"]) } @Test @@ -476,11 +476,11 @@ class StandardizedOfferTypesTest { // Create purchase props using the offer token from the discount offer val purchaseProps = RequestPurchaseAndroidProps( skus = listOf("premium_upgrade"), - offerTokenAndroid = discountOffer.offerTokenAndroid + offerToken = discountOffer.offerTokenAndroid ) - assertEquals("summer_sale_offer_token_xyz", purchaseProps.offerTokenAndroid) - assertEquals(discountOffer.offerTokenAndroid, purchaseProps.offerTokenAndroid) + assertEquals("summer_sale_offer_token_xyz", purchaseProps.offerToken) + assertEquals(discountOffer.offerTokenAndroid, purchaseProps.offerToken) } @Test @@ -517,10 +517,10 @@ class StandardizedOfferTypesTest { // Create purchase request with the offer token val purchaseProps = RequestPurchaseAndroidProps( skus = listOf(product.id), - offerTokenAndroid = offerToken + offerToken = offerToken ) assertEquals("consumable_gems", purchaseProps.skus.first()) - assertEquals("flash_sale_token", purchaseProps.offerTokenAndroid) + assertEquals("flash_sale_token", purchaseProps.offerToken) } } diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 4813d301..cb285f31 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3579,26 +3579,26 @@ public data class RequestPurchaseAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag (Android). + * Personalized offer flag. * When true, indicates the price was customized for this user. */ - val isOfferPersonalizedAndroid: Boolean? = null, + val isOfferPersonalized: Boolean? = null, /** * Obfuscated account ID */ - val obfuscatedAccountIdAndroid: String? = null, + val obfuscatedAccountId: String? = null, /** * Obfuscated profile ID */ - val obfuscatedProfileIdAndroid: String? = null, + val obfuscatedProfileId: String? = null, /** - * Offer token for one-time purchase discounts (Android 7.0+). + * Offer token for one-time purchase discounts (7.0+). * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers * to apply a discount offer to the purchase. */ - val offerTokenAndroid: String? = null, + val offerToken: String? = null, /** * List of product SKUs */ @@ -3606,30 +3606,30 @@ public data class RequestPurchaseAndroidProps( ) { companion object { fun fromJson(json: Map): RequestPurchaseAndroidProps? { - val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean - val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String - val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String - val offerTokenAndroid = json["offerTokenAndroid"] as? String + 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 return RequestPurchaseAndroidProps( - developerBillingOptionAndroid = developerBillingOptionAndroid, - isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, - obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, - obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, - offerTokenAndroid = offerTokenAndroid, + developerBillingOption = developerBillingOption, + isOfferPersonalized = isOfferPersonalized, + obfuscatedAccountId = obfuscatedAccountId, + obfuscatedProfileId = obfuscatedProfileId, + offerToken = offerToken, skus = skus, ) } } fun toJson(): Map = mapOf( - "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), - "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, - "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, - "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, - "offerTokenAndroid" to offerTokenAndroid, + "developerBillingOption" to developerBillingOption?.toJson(), + "isOfferPersonalized" to isOfferPersonalized, + "obfuscatedAccountId" to obfuscatedAccountId, + "obfuscatedProfileId" to obfuscatedProfileId, + "offerToken" to offerToken, "skus" to skus, ) } @@ -3798,29 +3798,29 @@ public data class RequestSubscriptionAndroidProps( * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - val developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = null, + val developerBillingOption: DeveloperBillingOptionParamsAndroid? = null, /** - * Personalized offer flag (Android). + * Personalized offer flag. * When true, indicates the price was customized for this user. */ - val isOfferPersonalizedAndroid: Boolean? = null, + val isOfferPersonalized: Boolean? = null, /** * Obfuscated account ID */ - val obfuscatedAccountIdAndroid: String? = null, + val obfuscatedAccountId: String? = null, /** * Obfuscated profile ID */ - val obfuscatedProfileIdAndroid: String? = null, + val obfuscatedProfileId: String? = null, /** * Purchase token for upgrades/downgrades */ - val purchaseTokenAndroid: String? = null, + val purchaseToken: String? = null, /** * Replacement mode for subscription changes * @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) */ - val replacementModeAndroid: Int? = null, + val replacementMode: Int? = null, /** * List of subscription SKUs */ @@ -3831,29 +3831,29 @@ public data class RequestSubscriptionAndroidProps( val subscriptionOffers: List? = null, /** * Product-level replacement parameters (8.1.0+) - * Use this instead of replacementModeAndroid for item-level replacement + * Use this instead of replacementMode for item-level replacement */ val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = null ) { companion object { fun fromJson(json: Map): RequestSubscriptionAndroidProps? { - val developerBillingOptionAndroid = (json["developerBillingOptionAndroid"] as? Map)?.let { DeveloperBillingOptionParamsAndroid.fromJson(it) } - val isOfferPersonalizedAndroid = json["isOfferPersonalizedAndroid"] as? Boolean - val obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String - val obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String - val purchaseTokenAndroid = json["purchaseTokenAndroid"] as? String - val replacementModeAndroid = (json["replacementModeAndroid"] as? Number)?.toInt() + 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 purchaseToken = json["purchaseToken"] as? String + val replacementMode = (json["replacementMode"] as? Number)?.toInt() 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 return RequestSubscriptionAndroidProps( - developerBillingOptionAndroid = developerBillingOptionAndroid, - isOfferPersonalizedAndroid = isOfferPersonalizedAndroid, - obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid, - obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid, - purchaseTokenAndroid = purchaseTokenAndroid, - replacementModeAndroid = replacementModeAndroid, + developerBillingOption = developerBillingOption, + isOfferPersonalized = isOfferPersonalized, + obfuscatedAccountId = obfuscatedAccountId, + obfuscatedProfileId = obfuscatedProfileId, + purchaseToken = purchaseToken, + replacementMode = replacementMode, skus = skus, subscriptionOffers = subscriptionOffers, subscriptionProductReplacementParams = subscriptionProductReplacementParams, @@ -3862,12 +3862,12 @@ public data class RequestSubscriptionAndroidProps( } fun toJson(): Map = mapOf( - "developerBillingOptionAndroid" to developerBillingOptionAndroid?.toJson(), - "isOfferPersonalizedAndroid" to isOfferPersonalizedAndroid, - "obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid, - "obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid, - "purchaseTokenAndroid" to purchaseTokenAndroid, - "replacementModeAndroid" to replacementModeAndroid, + "developerBillingOption" to developerBillingOption?.toJson(), + "isOfferPersonalized" to isOfferPersonalized, + "obfuscatedAccountId" to obfuscatedAccountId, + "obfuscatedProfileId" to obfuscatedProfileId, + "purchaseToken" to purchaseToken, + "replacementMode" to replacementMode, "skus" to skus, "subscriptionOffers" to subscriptionOffers?.map { it.toJson() }, "subscriptionProductReplacementParams" to subscriptionProductReplacementParams?.toJson(), diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 152e9163..0ce716da 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1382,34 +1382,34 @@ public struct RequestPurchaseAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag (Android). + public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + /// Personalized offer flag. /// When true, indicates the price was customized for this user. - public var isOfferPersonalizedAndroid: Bool? + public var isOfferPersonalized: Bool? /// Obfuscated account ID - public var obfuscatedAccountIdAndroid: String? + public var obfuscatedAccountId: String? /// Obfuscated profile ID - public var obfuscatedProfileIdAndroid: String? - /// Offer token for one-time purchase discounts (Android 7.0+). + public var obfuscatedProfileId: String? + /// Offer token for one-time purchase discounts (7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. - public var offerTokenAndroid: String? + public var offerToken: String? /// List of product SKUs public var skus: [String] public init( - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalizedAndroid: Bool? = nil, - obfuscatedAccountIdAndroid: String? = nil, - obfuscatedProfileIdAndroid: String? = nil, - offerTokenAndroid: String? = nil, + developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + isOfferPersonalized: Bool? = nil, + obfuscatedAccountId: String? = nil, + obfuscatedProfileId: String? = nil, + offerToken: String? = nil, skus: [String] ) { - self.developerBillingOptionAndroid = developerBillingOptionAndroid - self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid - self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid - self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid - self.offerTokenAndroid = offerTokenAndroid + self.developerBillingOption = developerBillingOption + self.isOfferPersonalized = isOfferPersonalized + self.obfuscatedAccountId = obfuscatedAccountId + self.obfuscatedProfileId = obfuscatedProfileId + self.offerToken = offerToken self.skus = skus } } @@ -1552,44 +1552,44 @@ public struct RequestSubscriptionAndroidProps: Codable { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - public var developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? - /// Personalized offer flag (Android). + public var developerBillingOption: DeveloperBillingOptionParamsAndroid? + /// Personalized offer flag. /// When true, indicates the price was customized for this user. - public var isOfferPersonalizedAndroid: Bool? + public var isOfferPersonalized: Bool? /// Obfuscated account ID - public var obfuscatedAccountIdAndroid: String? + public var obfuscatedAccountId: String? /// Obfuscated profile ID - public var obfuscatedProfileIdAndroid: String? + public var obfuscatedProfileId: String? /// Purchase token for upgrades/downgrades - public var purchaseTokenAndroid: String? + public var purchaseToken: String? /// Replacement mode for subscription changes /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) - public var replacementModeAndroid: Int? + public var replacementMode: Int? /// List of subscription SKUs public var skus: [String] /// Subscription offers public var subscriptionOffers: [AndroidSubscriptionOfferInput]? /// Product-level replacement parameters (8.1.0+) - /// Use this instead of replacementModeAndroid for item-level replacement + /// Use this instead of replacementMode for item-level replacement public var subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? public init( - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid? = nil, - isOfferPersonalizedAndroid: Bool? = nil, - obfuscatedAccountIdAndroid: String? = nil, - obfuscatedProfileIdAndroid: String? = nil, - purchaseTokenAndroid: String? = nil, - replacementModeAndroid: Int? = nil, + developerBillingOption: DeveloperBillingOptionParamsAndroid? = nil, + isOfferPersonalized: Bool? = nil, + obfuscatedAccountId: String? = nil, + obfuscatedProfileId: String? = nil, + purchaseToken: String? = nil, + replacementMode: Int? = nil, skus: [String], subscriptionOffers: [AndroidSubscriptionOfferInput]? = nil, subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = nil ) { - self.developerBillingOptionAndroid = developerBillingOptionAndroid - self.isOfferPersonalizedAndroid = isOfferPersonalizedAndroid - self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid - self.obfuscatedProfileIdAndroid = obfuscatedProfileIdAndroid - self.purchaseTokenAndroid = purchaseTokenAndroid - self.replacementModeAndroid = replacementModeAndroid + self.developerBillingOption = developerBillingOption + self.isOfferPersonalized = isOfferPersonalized + self.obfuscatedAccountId = obfuscatedAccountId + self.obfuscatedProfileId = obfuscatedProfileId + self.purchaseToken = purchaseToken + self.replacementMode = replacementMode self.skus = skus self.subscriptionOffers = subscriptionOffers self.subscriptionProductReplacementParams = subscriptionProductReplacementParams diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 452f904f..373737f3 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3600,50 +3600,50 @@ class PurchaseOptions { class RequestPurchaseAndroidProps { const RequestPurchaseAndroidProps({ - this.developerBillingOptionAndroid, - this.isOfferPersonalizedAndroid, - this.obfuscatedAccountIdAndroid, - this.obfuscatedProfileIdAndroid, - this.offerTokenAndroid, + this.developerBillingOption, + this.isOfferPersonalized, + this.obfuscatedAccountId, + this.obfuscatedProfileId, + this.offerToken, required this.skus, }); /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - final DeveloperBillingOptionParamsAndroid? developerBillingOptionAndroid; - /// Personalized offer flag (Android). + final DeveloperBillingOptionParamsAndroid? developerBillingOption; + /// Personalized offer flag. /// When true, indicates the price was customized for this user. - final bool? isOfferPersonalizedAndroid; + final bool? isOfferPersonalized; /// Obfuscated account ID - final String? obfuscatedAccountIdAndroid; + final String? obfuscatedAccountId; /// Obfuscated profile ID - final String? obfuscatedProfileIdAndroid; - /// Offer token for one-time purchase discounts (Android 7.0+). + final String? obfuscatedProfileId; + /// Offer token for one-time purchase discounts (7.0+). /// Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers /// to apply a discount offer to the purchase. - final String? offerTokenAndroid; + final String? offerToken; /// List of product SKUs final List skus; factory RequestPurchaseAndroidProps.fromJson(Map json) { return RequestPurchaseAndroidProps( - developerBillingOptionAndroid: json['developerBillingOptionAndroid'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOptionAndroid'] as Map) : null, - isOfferPersonalizedAndroid: json['isOfferPersonalizedAndroid'] as bool?, - obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, - obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, - offerTokenAndroid: json['offerTokenAndroid'] as String?, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + isOfferPersonalized: json['isOfferPersonalized'] as bool?, + obfuscatedAccountId: json['obfuscatedAccountId'] as String?, + obfuscatedProfileId: json['obfuscatedProfileId'] as String?, + offerToken: json['offerToken'] as String?, skus: (json['skus'] as List).map((e) => e as String).toList(), ); } Map toJson() { return { - 'developerBillingOptionAndroid': developerBillingOptionAndroid?.toJson(), - 'isOfferPersonalizedAndroid': isOfferPersonalizedAndroid, - 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, - 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, - 'offerTokenAndroid': offerTokenAndroid, + 'developerBillingOption': developerBillingOption?.toJson(), + 'isOfferPersonalized': isOfferPersonalized, + 'obfuscatedAccountId': obfuscatedAccountId, + 'obfuscatedProfileId': obfuscatedProfileId, + 'offerToken': offerToken, 'skus': skus, }; } @@ -3803,12 +3803,12 @@ class RequestPurchasePropsByPlatforms { class RequestSubscriptionAndroidProps { const RequestSubscriptionAndroidProps({ - this.developerBillingOptionAndroid, - this.isOfferPersonalizedAndroid, - this.obfuscatedAccountIdAndroid, - this.obfuscatedProfileIdAndroid, - this.purchaseTokenAndroid, - this.replacementModeAndroid, + this.developerBillingOption, + this.isOfferPersonalized, + this.obfuscatedAccountId, + this.obfuscatedProfileId, + this.purchaseToken, + this.replacementMode, required this.skus, this.subscriptionOffers, this.subscriptionProductReplacementParams, @@ -3817,35 +3817,35 @@ class RequestSubscriptionAndroidProps { /// Developer billing option parameters for external payments flow (8.3.0+). /// When provided, the purchase flow will show a side-by-side choice between /// Google Play Billing and the developer's external payment option. - final DeveloperBillingOptionParamsAndroid? developerBillingOptionAndroid; - /// Personalized offer flag (Android). + final DeveloperBillingOptionParamsAndroid? developerBillingOption; + /// Personalized offer flag. /// When true, indicates the price was customized for this user. - final bool? isOfferPersonalizedAndroid; + final bool? isOfferPersonalized; /// Obfuscated account ID - final String? obfuscatedAccountIdAndroid; + final String? obfuscatedAccountId; /// Obfuscated profile ID - final String? obfuscatedProfileIdAndroid; + final String? obfuscatedProfileId; /// Purchase token for upgrades/downgrades - final String? purchaseTokenAndroid; + final String? purchaseToken; /// Replacement mode for subscription changes /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) - final int? replacementModeAndroid; + final int? replacementMode; /// List of subscription SKUs final List skus; /// Subscription offers final List? subscriptionOffers; /// Product-level replacement parameters (8.1.0+) - /// Use this instead of replacementModeAndroid for item-level replacement + /// Use this instead of replacementMode for item-level replacement final SubscriptionProductReplacementParamsAndroid? subscriptionProductReplacementParams; factory RequestSubscriptionAndroidProps.fromJson(Map json) { return RequestSubscriptionAndroidProps( - developerBillingOptionAndroid: json['developerBillingOptionAndroid'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOptionAndroid'] as Map) : null, - isOfferPersonalizedAndroid: json['isOfferPersonalizedAndroid'] as bool?, - obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?, - obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?, - purchaseTokenAndroid: json['purchaseTokenAndroid'] as String?, - replacementModeAndroid: json['replacementModeAndroid'] as int?, + developerBillingOption: json['developerBillingOption'] != null ? DeveloperBillingOptionParamsAndroid.fromJson(json['developerBillingOption'] as Map) : null, + isOfferPersonalized: json['isOfferPersonalized'] as bool?, + obfuscatedAccountId: json['obfuscatedAccountId'] as String?, + obfuscatedProfileId: json['obfuscatedProfileId'] as String?, + purchaseToken: json['purchaseToken'] as String?, + replacementMode: json['replacementMode'] as int?, skus: (json['skus'] as List).map((e) => e as String).toList(), subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => AndroidSubscriptionOfferInput.fromJson(e as Map)).toList(), subscriptionProductReplacementParams: json['subscriptionProductReplacementParams'] != null ? SubscriptionProductReplacementParamsAndroid.fromJson(json['subscriptionProductReplacementParams'] as Map) : null, @@ -3854,12 +3854,12 @@ class RequestSubscriptionAndroidProps { Map toJson() { return { - 'developerBillingOptionAndroid': developerBillingOptionAndroid?.toJson(), - 'isOfferPersonalizedAndroid': isOfferPersonalizedAndroid, - 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, - 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, - 'purchaseTokenAndroid': purchaseTokenAndroid, - 'replacementModeAndroid': replacementModeAndroid, + 'developerBillingOption': developerBillingOption?.toJson(), + 'isOfferPersonalized': isOfferPersonalized, + 'obfuscatedAccountId': obfuscatedAccountId, + 'obfuscatedProfileId': obfuscatedProfileId, + 'purchaseToken': purchaseToken, + 'replacementMode': replacementMode, 'skus': skus, 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(), 'subscriptionProductReplacementParams': subscriptionProductReplacementParams?.toJson(), diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 8a327690..46e41e1e 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -3195,52 +3195,52 @@ class RequestPurchaseAndroidProps: ## List of product SKUs var skus: Array[String] ## Obfuscated account ID - var obfuscated_account_id_android: String + var obfuscated_account_id: String ## Obfuscated profile ID - var obfuscated_profile_id_android: String - ## Personalized offer flag (Android). - var is_offer_personalized_android: bool - ## Offer token for one-time purchase discounts (Android 7.0+). - var offer_token_android: String + var obfuscated_profile_id: String + ## Personalized offer flag. + var is_offer_personalized: bool + ## Offer token for one-time purchase discounts (7.0+). + var offer_token: String ## Developer billing option parameters for external payments flow (8.3.0+). - var developer_billing_option_android: DeveloperBillingOptionParamsAndroid + var developer_billing_option: DeveloperBillingOptionParamsAndroid static func from_dict(data: Dictionary) -> RequestPurchaseAndroidProps: var obj = RequestPurchaseAndroidProps.new() if data.has("skus") and data["skus"] != null: obj.skus = data["skus"] - if data.has("obfuscatedAccountIdAndroid") and data["obfuscatedAccountIdAndroid"] != null: - obj.obfuscated_account_id_android = data["obfuscatedAccountIdAndroid"] - if data.has("obfuscatedProfileIdAndroid") and data["obfuscatedProfileIdAndroid"] != null: - obj.obfuscated_profile_id_android = data["obfuscatedProfileIdAndroid"] - if data.has("isOfferPersonalizedAndroid") and data["isOfferPersonalizedAndroid"] != null: - obj.is_offer_personalized_android = data["isOfferPersonalizedAndroid"] - if data.has("offerTokenAndroid") and data["offerTokenAndroid"] != null: - obj.offer_token_android = data["offerTokenAndroid"] - if data.has("developerBillingOptionAndroid") and data["developerBillingOptionAndroid"] != null: - if data["developerBillingOptionAndroid"] is Dictionary: - obj.developer_billing_option_android = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOptionAndroid"]) + if data.has("obfuscatedAccountId") and data["obfuscatedAccountId"] != null: + obj.obfuscated_account_id = data["obfuscatedAccountId"] + if data.has("obfuscatedProfileId") and data["obfuscatedProfileId"] != null: + obj.obfuscated_profile_id = data["obfuscatedProfileId"] + if data.has("isOfferPersonalized") and data["isOfferPersonalized"] != null: + obj.is_offer_personalized = data["isOfferPersonalized"] + if data.has("offerToken") and data["offerToken"] != null: + 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_android = data["developerBillingOptionAndroid"] + obj.developer_billing_option = data["developerBillingOption"] return obj func to_dict() -> Dictionary: var dict = {} if skus != null: dict["skus"] = skus - if obfuscated_account_id_android != null: - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - if obfuscated_profile_id_android != null: - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - if is_offer_personalized_android != null: - dict["isOfferPersonalizedAndroid"] = is_offer_personalized_android - if offer_token_android != null: - dict["offerTokenAndroid"] = offer_token_android - if developer_billing_option_android != null: - if developer_billing_option_android.has_method("to_dict"): - dict["developerBillingOptionAndroid"] = developer_billing_option_android.to_dict() - else: - dict["developerBillingOptionAndroid"] = developer_billing_option_android + if obfuscated_account_id != null: + dict["obfuscatedAccountId"] = obfuscated_account_id + if obfuscated_profile_id != null: + dict["obfuscatedProfileId"] = obfuscated_profile_id + if is_offer_personalized != null: + dict["isOfferPersonalized"] = is_offer_personalized + if offer_token != null: + dict["offerToken"] = offer_token + if developer_billing_option != null: + if developer_billing_option.has_method("to_dict"): + dict["developerBillingOption"] = developer_billing_option.to_dict() + else: + dict["developerBillingOption"] = developer_billing_option return dict class RequestPurchaseIosProps: @@ -3411,36 +3411,36 @@ class RequestSubscriptionAndroidProps: ## List of subscription SKUs var skus: Array[String] ## Obfuscated account ID - var obfuscated_account_id_android: String + var obfuscated_account_id: String ## Obfuscated profile ID - var obfuscated_profile_id_android: String - ## Personalized offer flag (Android). - var is_offer_personalized_android: bool + var obfuscated_profile_id: String + ## Personalized offer flag. + var is_offer_personalized: bool ## Purchase token for upgrades/downgrades - var purchase_token_android: String + var purchase_token: String ## Replacement mode for subscription changes - var replacement_mode_android: int + var replacement_mode: int ## Subscription offers var subscription_offers: Array[AndroidSubscriptionOfferInput] ## Product-level replacement parameters (8.1.0+) var subscription_product_replacement_params: SubscriptionProductReplacementParamsAndroid ## Developer billing option parameters for external payments flow (8.3.0+). - var developer_billing_option_android: DeveloperBillingOptionParamsAndroid + var developer_billing_option: DeveloperBillingOptionParamsAndroid static func from_dict(data: Dictionary) -> RequestSubscriptionAndroidProps: var obj = RequestSubscriptionAndroidProps.new() if data.has("skus") and data["skus"] != null: obj.skus = data["skus"] - if data.has("obfuscatedAccountIdAndroid") and data["obfuscatedAccountIdAndroid"] != null: - obj.obfuscated_account_id_android = data["obfuscatedAccountIdAndroid"] - if data.has("obfuscatedProfileIdAndroid") and data["obfuscatedProfileIdAndroid"] != null: - obj.obfuscated_profile_id_android = data["obfuscatedProfileIdAndroid"] - if data.has("isOfferPersonalizedAndroid") and data["isOfferPersonalizedAndroid"] != null: - obj.is_offer_personalized_android = data["isOfferPersonalizedAndroid"] - if data.has("purchaseTokenAndroid") and data["purchaseTokenAndroid"] != null: - obj.purchase_token_android = data["purchaseTokenAndroid"] - if data.has("replacementModeAndroid") and data["replacementModeAndroid"] != null: - obj.replacement_mode_android = data["replacementModeAndroid"] + if data.has("obfuscatedAccountId") and data["obfuscatedAccountId"] != null: + obj.obfuscated_account_id = data["obfuscatedAccountId"] + if data.has("obfuscatedProfileId") and data["obfuscatedProfileId"] != null: + obj.obfuscated_profile_id = data["obfuscatedProfileId"] + if data.has("isOfferPersonalized") and data["isOfferPersonalized"] != null: + obj.is_offer_personalized = data["isOfferPersonalized"] + if data.has("purchaseToken") and data["purchaseToken"] != null: + obj.purchase_token = data["purchaseToken"] + if data.has("replacementMode") and data["replacementMode"] != null: + obj.replacement_mode = data["replacementMode"] if data.has("subscriptionOffers") and data["subscriptionOffers"] != null: var arr = [] for item in data["subscriptionOffers"]: @@ -3454,27 +3454,27 @@ class RequestSubscriptionAndroidProps: obj.subscription_product_replacement_params = SubscriptionProductReplacementParamsAndroid.from_dict(data["subscriptionProductReplacementParams"]) else: obj.subscription_product_replacement_params = data["subscriptionProductReplacementParams"] - if data.has("developerBillingOptionAndroid") and data["developerBillingOptionAndroid"] != null: - if data["developerBillingOptionAndroid"] is Dictionary: - obj.developer_billing_option_android = DeveloperBillingOptionParamsAndroid.from_dict(data["developerBillingOptionAndroid"]) + 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_android = data["developerBillingOptionAndroid"] + obj.developer_billing_option = data["developerBillingOption"] return obj func to_dict() -> Dictionary: var dict = {} if skus != null: dict["skus"] = skus - if obfuscated_account_id_android != null: - dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android - if obfuscated_profile_id_android != null: - dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android - if is_offer_personalized_android != null: - dict["isOfferPersonalizedAndroid"] = is_offer_personalized_android - if purchase_token_android != null: - dict["purchaseTokenAndroid"] = purchase_token_android - if replacement_mode_android != null: - dict["replacementModeAndroid"] = replacement_mode_android + if obfuscated_account_id != null: + dict["obfuscatedAccountId"] = obfuscated_account_id + if obfuscated_profile_id != null: + dict["obfuscatedProfileId"] = obfuscated_profile_id + if is_offer_personalized != null: + dict["isOfferPersonalized"] = is_offer_personalized + if purchase_token != null: + dict["purchaseToken"] = purchase_token + if replacement_mode != null: + dict["replacementMode"] = replacement_mode if subscription_offers != null: var arr = [] for item in subscription_offers: @@ -3488,11 +3488,11 @@ class RequestSubscriptionAndroidProps: dict["subscriptionProductReplacementParams"] = subscription_product_replacement_params.to_dict() else: dict["subscriptionProductReplacementParams"] = subscription_product_replacement_params - if developer_billing_option_android != null: - if developer_billing_option_android.has_method("to_dict"): - dict["developerBillingOptionAndroid"] = developer_billing_option_android.to_dict() + if developer_billing_option != null: + if developer_billing_option.has_method("to_dict"): + dict["developerBillingOption"] = developer_billing_option.to_dict() else: - dict["developerBillingOptionAndroid"] = developer_billing_option_android + dict["developerBillingOption"] = developer_billing_option return dict class RequestSubscriptionIosProps: diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index 74f0e4c0..3478d6e1 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1193,22 +1193,22 @@ export interface RequestPurchaseAndroidProps { * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - developerBillingOptionAndroid?: (DeveloperBillingOptionParamsAndroid | null); + developerBillingOption?: (DeveloperBillingOptionParamsAndroid | null); /** - * Personalized offer flag (Android). + * Personalized offer flag. * When true, indicates the price was customized for this user. */ - isOfferPersonalizedAndroid?: (boolean | null); + isOfferPersonalized?: (boolean | null); /** Obfuscated account ID */ - obfuscatedAccountIdAndroid?: (string | null); + obfuscatedAccountId?: (string | null); /** Obfuscated profile ID */ - obfuscatedProfileIdAndroid?: (string | null); + obfuscatedProfileId?: (string | null); /** - * Offer token for one-time purchase discounts (Android 7.0+). + * Offer token for one-time purchase discounts (7.0+). * Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers * to apply a discount offer to the purchase. */ - offerTokenAndroid?: (string | null); + offerToken?: (string | null); /** List of product SKUs */ skus: string[]; } @@ -1279,30 +1279,30 @@ export interface RequestSubscriptionAndroidProps { * When provided, the purchase flow will show a side-by-side choice between * Google Play Billing and the developer's external payment option. */ - developerBillingOptionAndroid?: (DeveloperBillingOptionParamsAndroid | null); + developerBillingOption?: (DeveloperBillingOptionParamsAndroid | null); /** - * Personalized offer flag (Android). + * Personalized offer flag. * When true, indicates the price was customized for this user. */ - isOfferPersonalizedAndroid?: (boolean | null); + isOfferPersonalized?: (boolean | null); /** Obfuscated account ID */ - obfuscatedAccountIdAndroid?: (string | null); + obfuscatedAccountId?: (string | null); /** Obfuscated profile ID */ - obfuscatedProfileIdAndroid?: (string | null); + obfuscatedProfileId?: (string | null); /** Purchase token for upgrades/downgrades */ - purchaseTokenAndroid?: (string | null); + purchaseToken?: (string | null); /** * Replacement mode for subscription changes * @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) */ - replacementModeAndroid?: (number | null); + replacementMode?: (number | null); /** List of subscription SKUs */ skus: string[]; /** Subscription offers */ subscriptionOffers?: (AndroidSubscriptionOfferInput[] | null); /** * Product-level replacement parameters (8.1.0+) - * Use this instead of replacementModeAndroid for item-level replacement + * Use this instead of replacementMode for item-level replacement */ subscriptionProductReplacementParams?: (SubscriptionProductReplacementParamsAndroid | null); } diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql index ad13adc9..01ce9611 100644 --- a/packages/gql/src/type-android.graphql +++ b/packages/gql/src/type-android.graphql @@ -355,28 +355,28 @@ input RequestPurchaseAndroidProps { """ Obfuscated account ID """ - obfuscatedAccountIdAndroid: String + obfuscatedAccountId: String """ Obfuscated profile ID """ - obfuscatedProfileIdAndroid: String + obfuscatedProfileId: String """ - Personalized offer flag (Android). + Personalized offer flag. When true, indicates the price was customized for this user. """ - isOfferPersonalizedAndroid: Boolean + isOfferPersonalized: Boolean """ - Offer token for one-time purchase discounts (Android 7.0+). + Offer token for one-time purchase discounts (7.0+). Pass the offerToken from oneTimePurchaseOfferDetailsAndroid or discountOffers to apply a discount offer to the purchase. """ - offerTokenAndroid: String + offerToken: String """ Developer billing option parameters for external payments flow (8.3.0+). When provided, the purchase flow will show a side-by-side choice between Google Play Billing and the developer's external payment option. """ - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid + developerBillingOption: DeveloperBillingOptionParamsAndroid } input RequestSubscriptionAndroidProps { @@ -387,32 +387,32 @@ input RequestSubscriptionAndroidProps { """ Obfuscated account ID """ - obfuscatedAccountIdAndroid: String + obfuscatedAccountId: String """ Obfuscated profile ID """ - obfuscatedProfileIdAndroid: String + obfuscatedProfileId: String """ - Personalized offer flag (Android). + Personalized offer flag. When true, indicates the price was customized for this user. """ - isOfferPersonalizedAndroid: Boolean + isOfferPersonalized: Boolean """ Purchase token for upgrades/downgrades """ - purchaseTokenAndroid: String + purchaseToken: String """ Replacement mode for subscription changes @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+) """ - replacementModeAndroid: Int + replacementMode: Int """ Subscription offers """ subscriptionOffers: [AndroidSubscriptionOfferInput!] """ Product-level replacement parameters (8.1.0+) - Use this instead of replacementModeAndroid for item-level replacement + Use this instead of replacementMode for item-level replacement """ subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid """ @@ -420,7 +420,7 @@ input RequestSubscriptionAndroidProps { When provided, the purchase flow will show a side-by-side choice between Google Play Billing and the developer's external payment option. """ - developerBillingOptionAndroid: DeveloperBillingOptionParamsAndroid + developerBillingOption: DeveloperBillingOptionParamsAndroid } # Subscription Replacement (Google Play Billing Library 8.1.0+)