From 4d14f4c269604449def687f0f97c599e6d1654d0 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 12 Dec 2025 18:16:54 +0900 Subject: [PATCH 1/9] refactor: update verifyPurchase to support platform-specific options - Add VerifyPurchaseAppleOptions (jws) for iOS App Store verification - Add VerifyPurchaseGoogleOptions (packageName, purchaseToken, accessToken, isSub) for Play Store - Add VerifyPurchaseHorizonOptions (sku, userId, accessToken) for Meta Quest S2S API - Update VerifyPurchaseProps to accept apple, google, horizon fields - Deprecate androidOptions in favor of google field - Implement verifyPurchaseWithHorizon() for Meta S2S verification - Add platform-specific documentation comments Note: Pending version updates for release: - openiap-gql: 1.3.3 - openiap-google: 1.3.13 - openiap-apple: 1.3.1 --- packages/apple/Sources/Models/Types.swift | 100 +++++++++- packages/apple/Sources/OpenIapModule.swift | 27 ++- .../java/dev/hyo/openiap/OpenIapModule.kt | 31 +++- .../src/main/java/dev/hyo/openiap/Types.kt | 156 +++++++++++++++- .../utils/PurchaseVerificationValidator.kt | 166 +++++++++++++---- .../PurchaseVerificationValidatorTest.kt | 108 ++++++++++- packages/gql/src/generated/Types.kt | 156 +++++++++++++++- packages/gql/src/generated/Types.swift | 100 +++++++++- packages/gql/src/generated/types.dart | 173 +++++++++++++++++- packages/gql/src/generated/types.ts | 80 +++++++- packages/gql/src/type-android.graphql | 46 +++++ packages/gql/src/type-ios.graphql | 11 ++ packages/gql/src/type.graphql | 48 ++++- 13 files changed, 1130 insertions(+), 72 deletions(-) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index e4681362..7338537d 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1142,6 +1142,12 @@ public struct RequestPurchaseProps: Codable { } } +/// Platform-specific purchase request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// (determined at build time, not runtime) public struct RequestPurchasePropsByPlatforms: Codable { /// @deprecated Use google instead public var android: RequestPurchaseAndroidProps? @@ -1228,6 +1234,12 @@ public struct RequestSubscriptionIosProps: Codable { } } +/// Platform-specific subscription request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// (determined at build time, not runtime) public struct RequestSubscriptionPropsByPlatforms: Codable { /// @deprecated Use google instead public var android: RequestSubscriptionAndroidProps? @@ -1273,12 +1285,16 @@ public struct RequestVerifyPurchaseWithIapkitGoogleProps: Codable { } } +/// Platform-specific verification parameters for IAPKit. +/// +/// - apple: Verifies via App Store (JWS token) +/// - google: Verifies via Play Store (purchase token) public struct RequestVerifyPurchaseWithIapkitProps: Codable { /// API key used for the Authorization header (Bearer {apiKey}). public var apiKey: String? - /// Apple verification parameters. + /// Apple App Store verification parameters. public var apple: RequestVerifyPurchaseWithIapkitAppleProps? - /// Google verification parameters. + /// Google Play Store verification parameters. public var google: RequestVerifyPurchaseWithIapkitGoogleProps? public init( @@ -1310,6 +1326,7 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable { } } +/// @deprecated Use VerifyPurchaseGoogleOptions instead public struct VerifyPurchaseAndroidOptions: Codable { public var accessToken: String public var isSub: Bool? @@ -1329,17 +1346,94 @@ public struct VerifyPurchaseAndroidOptions: Codable { } } +/// Apple App Store verification parameters. +/// Used for server-side receipt validation via App Store Server API. +public struct VerifyPurchaseAppleOptions: Codable { + /// The JWS (JSON Web Signature) representation of the transaction. + public var jws: String + + public init( + jws: String + ) { + self.jws = jws + } +} + +/// Google Play Store verification parameters. +/// Used for server-side receipt validation via Google Play Developer API. +public struct VerifyPurchaseGoogleOptions: Codable { + /// Google OAuth2 access token for API authentication + public var accessToken: String + /// Whether this is a subscription purchase (affects API endpoint used) + public var isSub: Bool? + /// Android package name (e.g., com.example.app) + public var packageName: String + /// Purchase token from the purchase response + public var purchaseToken: String + + public init( + accessToken: String, + isSub: Bool? = nil, + packageName: String, + purchaseToken: String + ) { + self.accessToken = accessToken + self.isSub = isSub + self.packageName = packageName + self.purchaseToken = purchaseToken + } +} + +/// Meta Horizon (Quest) verification parameters. +/// Used for server-side entitlement verification via Meta's S2S API. +/// POST https://graph.oculus.com/$APP_ID/verify_entitlement +public struct VerifyPurchaseHorizonOptions: Codable { + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + public var accessToken: String + /// The SKU for the add-on item, defined in Meta Developer Dashboard + public var sku: String + /// The user ID of the user whose purchase you want to verify + public var userId: String + + public init( + accessToken: String, + sku: String, + userId: String + ) { + self.accessToken = accessToken + self.sku = sku + self.userId = userId + } +} + +/// Platform-specific purchase verification parameters. +/// +/// - apple: Verifies via App Store Server API +/// - google: Verifies via Google Play Developer API +/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) public struct VerifyPurchaseProps: Codable { - /// Android-specific validation options + /// @deprecated Use google instead public var androidOptions: VerifyPurchaseAndroidOptions? + /// Apple App Store verification parameters. + public var apple: VerifyPurchaseAppleOptions? + /// Google Play Store verification parameters. + public var google: VerifyPurchaseGoogleOptions? + /// Meta Horizon (Quest) verification parameters. + public var horizon: VerifyPurchaseHorizonOptions? /// Product SKU to validate public var sku: String public init( androidOptions: VerifyPurchaseAndroidOptions? = nil, + apple: VerifyPurchaseAppleOptions? = nil, + google: VerifyPurchaseGoogleOptions? = nil, + horizon: VerifyPurchaseHorizonOptions? = nil, sku: String ) { self.androidOptions = androidOptions + self.apple = apple + self.google = google + self.horizon = horizon self.sku = sku } } diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 8c30fff6..281696e5 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -598,16 +598,25 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { var jws: String = "" var isValid = false - do { - let product = try await storeProduct(for: props.sku) - if let result = await product.latestTransaction { - jws = result.jwsRepresentation - let transaction = try checkVerified(result) - latestPurchase = .purchaseIos(await StoreKitTypesBridge.purchaseIOS(from: transaction, jwsRepresentation: result.jwsRepresentation)) - isValid = true + // If apple options with JWS are provided, use that directly + // Otherwise, fetch the latest transaction from StoreKit + if let appleOptions = props.apple, !appleOptions.jws.isEmpty { + jws = appleOptions.jws + // When JWS is provided externally, we trust it's valid + // The caller should verify the JWS on their server + isValid = true + } else { + do { + let product = try await storeProduct(for: props.sku) + if let result = await product.latestTransaction { + jws = result.jwsRepresentation + let transaction = try checkVerified(result) + latestPurchase = .purchaseIos(await StoreKitTypesBridge.purchaseIOS(from: transaction, jwsRepresentation: result.jwsRepresentation)) + isValid = true + } + } catch { + isValid = false } - } catch { - isValid = false } return VerifyPurchaseResultIOS( 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 13e1d488..ea253ae9 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 @@ -34,6 +34,7 @@ import dev.hyo.openiap.utils.HorizonBillingConverters.toPurchase import dev.hyo.openiap.utils.HorizonBillingConverters.toSubscriptionProduct import dev.hyo.openiap.utils.toProduct import dev.hyo.openiap.utils.verifyPurchaseWithGooglePlay +import dev.hyo.openiap.utils.verifyPurchaseWithHorizon import dev.hyo.openiap.MutationVerifyPurchaseHandler import dev.hyo.openiap.MutationValidateReceiptHandler import dev.hyo.openiap.MutationVerifyPurchaseWithProviderHandler @@ -654,7 +655,35 @@ class OpenIapModule( } override val verifyPurchase: MutationVerifyPurchaseHandler = { props -> - verifyPurchaseWithGooglePlay(props, TAG) + // Use Horizon API if horizon options provided, otherwise fallback to Google Play + if (props.horizon != null) { + val horizonAppId = appId ?: throw OpenIapError.DeveloperError + verifyPurchaseWithHorizon(props, horizonAppId, TAG) + // Return a VerifyPurchaseResult - for now using Android result type + // TODO: Add VerifyPurchaseResultHorizon to GraphQL schema + VerifyPurchaseResultAndroid( + autoRenewing = false, + betaProduct = false, + cancelDate = null, + cancelReason = null, + deferredDate = null, + deferredSku = null, + freeTrialEndDate = 0.0, + gracePeriodEndDate = 0.0, + parentProductId = "", + productId = props.sku, + productType = "inapp", + purchaseDate = 0.0, + quantity = 1, + receiptId = "", + renewalDate = 0.0, + term = "", + termSku = "", + testTransaction = false + ) + } else { + verifyPurchaseWithGooglePlay(props, TAG) + } } override val verifyPurchaseWithProvider: MutationVerifyPurchaseWithProviderHandler = { props -> 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 f193f38e..4c004f0e 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 @@ -2767,6 +2767,14 @@ public data class RequestPurchaseProps( } } +/** + * Platform-specific purchase request parameters. + * + * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. + * - apple: Always targets App Store + * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * (determined at build time, not runtime) + */ public data class RequestPurchasePropsByPlatforms( /** * @deprecated Use google instead @@ -2895,6 +2903,14 @@ public data class RequestSubscriptionIosProps( ) } +/** + * Platform-specific subscription request parameters. + * + * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. + * - apple: Always targets App Store + * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * (determined at build time, not runtime) + */ public data class RequestSubscriptionPropsByPlatforms( /** * @deprecated Use google instead @@ -2970,17 +2986,23 @@ public data class RequestVerifyPurchaseWithIapkitGoogleProps( ) } +/** + * Platform-specific verification parameters for IAPKit. + * + * - apple: Verifies via App Store (JWS token) + * - google: Verifies via Play Store (purchase token) + */ public data class RequestVerifyPurchaseWithIapkitProps( /** * API key used for the Authorization header (Bearer {apiKey}). */ val apiKey: String? = null, /** - * Apple verification parameters. + * Apple App Store verification parameters. */ val apple: RequestVerifyPurchaseWithIapkitAppleProps? = null, /** - * Google verification parameters. + * Google Play Store verification parameters. */ val google: RequestVerifyPurchaseWithIapkitGoogleProps? = null ) { @@ -3031,6 +3053,9 @@ public data class SubscriptionProductReplacementParamsAndroid( ) } +/** + * @deprecated Use VerifyPurchaseGoogleOptions instead + */ public data class VerifyPurchaseAndroidOptions( val accessToken: String, val isSub: Boolean? = null, @@ -3056,11 +3081,130 @@ public data class VerifyPurchaseAndroidOptions( ) } +/** + * Apple App Store verification parameters. + * Used for server-side receipt validation via App Store Server API. + */ +public data class VerifyPurchaseAppleOptions( + /** + * The JWS (JSON Web Signature) representation of the transaction. + */ + val jws: String +) { + companion object { + fun fromJson(json: Map): VerifyPurchaseAppleOptions { + return VerifyPurchaseAppleOptions( + jws = json["jws"] as String, + ) + } + } + + fun toJson(): Map = mapOf( + "jws" to jws, + ) +} + +/** + * Google Play Store verification parameters. + * Used for server-side receipt validation via Google Play Developer API. + */ +public data class VerifyPurchaseGoogleOptions( + /** + * Google OAuth2 access token for API authentication + */ + val accessToken: String, + /** + * Whether this is a subscription purchase (affects API endpoint used) + */ + val isSub: Boolean? = null, + /** + * Android package name (e.g., com.example.app) + */ + val packageName: String, + /** + * Purchase token from the purchase response + */ + val purchaseToken: String +) { + companion object { + fun fromJson(json: Map): VerifyPurchaseGoogleOptions { + return VerifyPurchaseGoogleOptions( + accessToken = json["accessToken"] as String, + isSub = json["isSub"] as Boolean?, + packageName = json["packageName"] as String, + purchaseToken = json["purchaseToken"] as String, + ) + } + } + + fun toJson(): Map = mapOf( + "accessToken" to accessToken, + "isSub" to isSub, + "packageName" to packageName, + "purchaseToken" to purchaseToken, + ) +} + +/** + * Meta Horizon (Quest) verification parameters. + * Used for server-side entitlement verification via Meta's S2S API. + * POST https://graph.oculus.com/$APP_ID/verify_entitlement + */ +public data class VerifyPurchaseHorizonOptions( + /** + * Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + */ + val accessToken: String, + /** + * The SKU for the add-on item, defined in Meta Developer Dashboard + */ + val sku: String, + /** + * The user ID of the user whose purchase you want to verify + */ + val userId: String +) { + companion object { + fun fromJson(json: Map): VerifyPurchaseHorizonOptions { + return VerifyPurchaseHorizonOptions( + accessToken = json["accessToken"] as String, + sku = json["sku"] as String, + userId = json["userId"] as String, + ) + } + } + + fun toJson(): Map = mapOf( + "accessToken" to accessToken, + "sku" to sku, + "userId" to userId, + ) +} + +/** + * Platform-specific purchase verification parameters. + * + * - apple: Verifies via App Store Server API + * - google: Verifies via Google Play Developer API + * - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) + */ public data class VerifyPurchaseProps( /** - * Android-specific validation options + * @deprecated Use google instead */ val androidOptions: VerifyPurchaseAndroidOptions? = null, + /** + * Apple App Store verification parameters. + */ + val apple: VerifyPurchaseAppleOptions? = null, + /** + * Google Play Store verification parameters. + */ + val google: VerifyPurchaseGoogleOptions? = null, + /** + * Meta Horizon (Quest) verification parameters. + */ + val horizon: VerifyPurchaseHorizonOptions? = null, /** * Product SKU to validate */ @@ -3070,6 +3214,9 @@ public data class VerifyPurchaseProps( fun fromJson(json: Map): VerifyPurchaseProps { return VerifyPurchaseProps( androidOptions = (json["androidOptions"] as Map?)?.let { VerifyPurchaseAndroidOptions.fromJson(it) }, + apple = (json["apple"] as Map?)?.let { VerifyPurchaseAppleOptions.fromJson(it) }, + google = (json["google"] as Map?)?.let { VerifyPurchaseGoogleOptions.fromJson(it) }, + horizon = (json["horizon"] as Map?)?.let { VerifyPurchaseHorizonOptions.fromJson(it) }, sku = json["sku"] as String, ) } @@ -3077,6 +3224,9 @@ public data class VerifyPurchaseProps( fun toJson(): Map = mapOf( "androidOptions" to androidOptions?.toJson(), + "apple" to apple?.toJson(), + "google" to google?.toJson(), + "horizon" to horizon?.toJson(), "sku" to sku, ) } diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index b275c262..b5e9d9d0 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -28,29 +28,49 @@ suspend fun verifyPurchaseWithGooglePlay( tag: String, connectionFactory: (String) -> HttpURLConnection = ::openConnection ): VerifyPurchaseResultAndroid = withContext(Dispatchers.IO) { - val options = props.androidOptions - ?: throw IllegalArgumentException( - "Android validation requires packageName, productToken, and accessToken" - ) + // Support both new google field and deprecated androidOptions + val googleOptions = props.google + val legacyOptions = props.androidOptions + + val packageName: String + val purchaseToken: String + val accessToken: String + val isSub: Boolean? + + when { + googleOptions != null -> { + packageName = googleOptions.packageName + purchaseToken = googleOptions.purchaseToken + accessToken = googleOptions.accessToken + isSub = googleOptions.isSub + } + legacyOptions != null -> { + packageName = legacyOptions.packageName + purchaseToken = legacyOptions.productToken + accessToken = legacyOptions.accessToken + isSub = legacyOptions.isSub + } + else -> { + throw IllegalArgumentException( + "Google Play validation requires google options (packageName, purchaseToken, accessToken)" + ) + } + } - if ( - options.packageName.isBlank() || - options.productToken.isBlank() || - options.accessToken.isBlank() - ) { + if (packageName.isBlank() || purchaseToken.isBlank() || accessToken.isBlank()) { throw IllegalArgumentException( - "Android validation requires packageName, productToken, and accessToken" + "Google Play validation requires packageName, purchaseToken, and accessToken" ) } - val typeSegment = if (options.isSub == true) "subscriptions" else "products" + val typeSegment = if (isSub == true) "subscriptions" else "products" val baseUrl = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications" - val url = "$baseUrl/${options.packageName}/purchases/$typeSegment/${props.sku}/tokens/${options.productToken}" + val url = "$baseUrl/$packageName/purchases/$typeSegment/${props.sku}/tokens/$purchaseToken" val connection = connectionFactory(url).apply { requestMethod = "GET" setRequestProperty("Content-Type", "application/json") - setRequestProperty("Authorization", "Bearer ${options.accessToken}") + setRequestProperty("Authorization", "Bearer $accessToken") } try { @@ -80,6 +100,89 @@ suspend fun verifyPurchaseWithGooglePlay( } } +/** + * Verify purchase with Meta Horizon S2S API. + * POST https://graph.oculus.com/$APP_ID/verify_entitlement + */ +suspend fun verifyPurchaseWithHorizon( + props: VerifyPurchaseProps, + appId: String, + tag: String, + connectionFactory: (String) -> HttpURLConnection = ::openConnection +): VerifyPurchaseResultHorizon = withContext(Dispatchers.IO) { + val horizonOptions = props.horizon + ?: throw IllegalArgumentException( + "Horizon validation requires horizon options (sku, userId, accessToken)" + ) + + if (horizonOptions.sku.isBlank() || horizonOptions.userId.isBlank() || horizonOptions.accessToken.isBlank()) { + throw IllegalArgumentException( + "Horizon validation requires sku, userId, and accessToken" + ) + } + + val url = "https://graph.oculus.com/$appId/verify_entitlement" + + val connection = connectionFactory(url).apply { + requestMethod = "POST" + doOutput = true + setRequestProperty("Content-Type", "application/x-www-form-urlencoded") + } + + try { + // Build form data + val formData = buildString { + append("access_token=${java.net.URLEncoder.encode(horizonOptions.accessToken, "UTF-8")}") + append("&user_id=${java.net.URLEncoder.encode(horizonOptions.userId, "UTF-8")}") + append("&sku=${java.net.URLEncoder.encode(horizonOptions.sku, "UTF-8")}") + } + + connection.outputStream.use { stream -> + stream.write(formData.toByteArray()) + } + + val statusCode = connection.responseCode + val responseBody = (if (statusCode in 200..299) connection.inputStream else connection.errorStream) + ?.bufferedReader() + ?.use { it.readText() } + .orElse("") + + if (statusCode !in 200..299) { + OpenIapLog.warn("Horizon verifyPurchase failed (HTTP $statusCode): $responseBody", tag) + throw OpenIapError.InvalidPurchaseVerification + } + + try { + // Response: {"success":true,"grant_time":1744148687} + val mapType = object : TypeToken>() {}.type + val parsed = gson.fromJson>(responseBody, mapType) + val success = parsed["success"] as? Boolean ?: false + val grantTime = (parsed["grant_time"] as? Number)?.toLong() + + VerifyPurchaseResultHorizon( + success = success, + grantTime = grantTime + ) + } catch (jsonError: JsonSyntaxException) { + OpenIapLog.warn("Failed to parse Horizon verification response: ${jsonError.message}", tag) + throw OpenIapError.InvalidPurchaseVerification + } + } catch (io: IOException) { + OpenIapLog.warn("Network error during Horizon verification: ${io.message}", tag) + throw OpenIapError.NetworkError + } finally { + connection.disconnect() + } +} + +/** + * Result from Meta Horizon verify_entitlement API. + */ +data class VerifyPurchaseResultHorizon( + val success: Boolean, + val grantTime: Long? +) + suspend fun verifyPurchaseWithIapkit( props: RequestVerifyPurchaseWithIapkitProps, tag: String, @@ -87,13 +190,14 @@ suspend fun verifyPurchaseWithIapkit( ): RequestVerifyPurchaseWithIapkitResult = withContext(Dispatchers.IO) { val endpoint = DEFAULT_IAPKIT_ENDPOINT - // On Android, only Google verification is supported + // On Android, only Google verification is supported via IAPKit + // Note: Horizon verification requires direct S2S API calls to Meta (not yet supported) if (props.google == null) { throw IllegalArgumentException("IAPKit verification on Android requires google payload") } val store = IapStore.Google - val payload = buildPayload(props, store) + val payload = buildGooglePayload(props) val connection = connectionFactory(endpoint).apply { requestMethod = "POST" @@ -166,28 +270,22 @@ suspend fun verifyPurchaseWithIapkit( } } -private fun buildPayload( - props: RequestVerifyPurchaseWithIapkitProps, - store: IapStore -): Map { - return when (store) { - IapStore.Google, IapStore.Horizon -> { - val google = props.google - ?: throw IllegalArgumentException("IAPKit Google verification requires google options") - if (google.purchaseToken.isBlank()) { - throw IllegalArgumentException( - "IAPKit Google verification requires purchaseToken" - ) - } - mutableMapOf( - "store" to store.rawValue, - "purchaseToken" to google.purchaseToken - ) - } - else -> throw IllegalArgumentException("IAPKit verification on Android supports Google payloads only") +/** + * Build payload for Google Play Store verification via IAPKit. + */ +private fun buildGooglePayload(props: RequestVerifyPurchaseWithIapkitProps): Map { + val google = props.google + ?: throw IllegalArgumentException("IAPKit Google verification requires google options") + if (google.purchaseToken.isBlank()) { + throw IllegalArgumentException("IAPKit Google verification requires purchaseToken") } + return mutableMapOf( + "store" to IapStore.Google.rawValue, + "purchaseToken" to google.purchaseToken + ) } + private fun String?.orElse(fallback: String): String = this ?: fallback /** diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt index 9d53b45e..ed07c1e9 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt @@ -2,12 +2,15 @@ package dev.hyo.openiap import com.google.gson.Gson import dev.hyo.openiap.utils.verifyPurchaseWithGooglePlay +import dev.hyo.openiap.utils.verifyPurchaseWithHorizon import dev.hyo.openiap.utils.verifyPurchaseWithIapkit import dev.hyo.openiap.IapStore import dev.hyo.openiap.IapkitPurchaseState import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitGoogleProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps import dev.hyo.openiap.VerifyPurchaseAndroidOptions +import dev.hyo.openiap.VerifyPurchaseGoogleOptions +import dev.hyo.openiap.VerifyPurchaseHorizonOptions import dev.hyo.openiap.VerifyPurchaseProps import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -23,19 +26,61 @@ import org.junit.Test class PurchaseVerificationValidatorTest { @Test - fun `verifyPurchaseWithGooglePlay throws without androidOptions`() = runTest { - val props = VerifyPurchaseProps(androidOptions = null, sku = "product.sku") + fun `verifyPurchaseWithGooglePlay throws without google or androidOptions`() = runTest { + val props = VerifyPurchaseProps(sku = "product.sku") try { verifyPurchaseWithGooglePlay(props, "TEST_TAG") { _ -> throw AssertionError("Connection should not be created when options are missing") } - throw AssertionError("Expected IllegalArgumentException for missing androidOptions") + throw AssertionError("Expected IllegalArgumentException for missing google options") } catch (expected: IllegalArgumentException) { // Expected path } } + @Test + fun `verifyPurchaseWithGooglePlay works with new google field`() = runTest { + val googleOptions = VerifyPurchaseGoogleOptions( + accessToken = "token", + isSub = true, + packageName = "dev.hyo.app", + purchaseToken = "purchaseToken" + ) + val props = VerifyPurchaseProps(google = googleOptions, sku = "premium_monthly") + val body = """ + { + "autoRenewing": true, + "betaProduct": false, + "cancelDate": null, + "cancelReason": null, + "deferredDate": null, + "deferredSku": null, + "freeTrialEndDate": 1.0, + "gracePeriodEndDate": 2.0, + "parentProductId": "parent", + "productId": "premium_monthly", + "productType": "subs", + "purchaseDate": 3.0, + "quantity": 1, + "receiptId": "rid-123", + "renewalDate": 4.0, + "term": "P1M", + "termSku": "plan_monthly", + "testTransaction": false + } + """.trimIndent() + + val result = verifyPurchaseWithGooglePlay( + props, + "TEST_TAG" + ) { _ -> FakeHttpURLConnection(200, body) } + + assertEquals("premium_monthly", result.productId) + assertEquals("plan_monthly", result.termSku) + assertEquals(true, result.autoRenewing) + } + @Test fun `verifyPurchaseWithGooglePlay parses successful response`() = runTest { val options = VerifyPurchaseAndroidOptions( @@ -219,6 +264,63 @@ class PurchaseVerificationValidatorTest { assertTrue(true) } } + + // ===== Horizon verification tests ===== + + @Test + fun `verifyPurchaseWithHorizon throws without horizon options`() = runTest { + val props = VerifyPurchaseProps(sku = "50_gems") + + try { + verifyPurchaseWithHorizon(props, "test-app-id", "TEST") { _ -> + throw AssertionError("Connection should not be created when horizon options are missing") + } + throw AssertionError("Expected IllegalArgumentException for missing horizon options") + } catch (expected: IllegalArgumentException) { + // Expected path + } + } + + @Test + fun `verifyPurchaseWithHorizon parses successful response`() = runTest { + val horizonOptions = VerifyPurchaseHorizonOptions( + sku = "50_gems", + userId = "123456789", + accessToken = "OC|app_id|app_secret" + ) + val props = VerifyPurchaseProps(horizon = horizonOptions, sku = "50_gems") + + val result = verifyPurchaseWithHorizon( + props, + "test-app-id", + "TEST" + ) { _ -> FakeHttpURLConnection(200, """{"success":true,"grant_time":1744148687}""") } + + assertEquals(true, result.success) + assertEquals(1744148687L, result.grantTime) + } + + @Test + fun `verifyPurchaseWithHorizon throws on failure response`() = runTest { + val horizonOptions = VerifyPurchaseHorizonOptions( + sku = "50_gems", + userId = "123456789", + accessToken = "OC|app_id|app_secret" + ) + val props = VerifyPurchaseProps(horizon = horizonOptions, sku = "50_gems") + + try { + verifyPurchaseWithHorizon( + props, + "test-app-id", + "TEST" + ) { _ -> FakeHttpURLConnection(400, """{"error":"invalid_user"}""") } + throw AssertionError("Expected InvalidPurchaseVerification for non-2xx response") + } catch (error: OpenIapError.InvalidPurchaseVerification) { + assertTrue(true) + } + } + } private class FakeHttpURLConnection( diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index cd1b9968..62afd1b2 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -2839,6 +2839,14 @@ public data class RequestPurchaseProps( } } +/** + * Platform-specific purchase request parameters. + * + * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. + * - apple: Always targets App Store + * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * (determined at build time, not runtime) + */ public data class RequestPurchasePropsByPlatforms( /** * @deprecated Use google instead @@ -2967,6 +2975,14 @@ public data class RequestSubscriptionIosProps( ) } +/** + * Platform-specific subscription request parameters. + * + * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. + * - apple: Always targets App Store + * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * (determined at build time, not runtime) + */ public data class RequestSubscriptionPropsByPlatforms( /** * @deprecated Use google instead @@ -3042,17 +3058,23 @@ public data class RequestVerifyPurchaseWithIapkitGoogleProps( ) } +/** + * Platform-specific verification parameters for IAPKit. + * + * - apple: Verifies via App Store (JWS token) + * - google: Verifies via Play Store (purchase token) + */ public data class RequestVerifyPurchaseWithIapkitProps( /** * API key used for the Authorization header (Bearer {apiKey}). */ val apiKey: String? = null, /** - * Apple verification parameters. + * Apple App Store verification parameters. */ val apple: RequestVerifyPurchaseWithIapkitAppleProps? = null, /** - * Google verification parameters. + * Google Play Store verification parameters. */ val google: RequestVerifyPurchaseWithIapkitGoogleProps? = null ) { @@ -3103,6 +3125,9 @@ public data class SubscriptionProductReplacementParamsAndroid( ) } +/** + * @deprecated Use VerifyPurchaseGoogleOptions instead + */ public data class VerifyPurchaseAndroidOptions( val accessToken: String, val isSub: Boolean? = null, @@ -3128,11 +3153,130 @@ public data class VerifyPurchaseAndroidOptions( ) } +/** + * Apple App Store verification parameters. + * Used for server-side receipt validation via App Store Server API. + */ +public data class VerifyPurchaseAppleOptions( + /** + * The JWS (JSON Web Signature) representation of the transaction. + */ + val jws: String +) { + companion object { + fun fromJson(json: Map): VerifyPurchaseAppleOptions { + return VerifyPurchaseAppleOptions( + jws = json["jws"] as String, + ) + } + } + + fun toJson(): Map = mapOf( + "jws" to jws, + ) +} + +/** + * Google Play Store verification parameters. + * Used for server-side receipt validation via Google Play Developer API. + */ +public data class VerifyPurchaseGoogleOptions( + /** + * Google OAuth2 access token for API authentication + */ + val accessToken: String, + /** + * Whether this is a subscription purchase (affects API endpoint used) + */ + val isSub: Boolean? = null, + /** + * Android package name (e.g., com.example.app) + */ + val packageName: String, + /** + * Purchase token from the purchase response + */ + val purchaseToken: String +) { + companion object { + fun fromJson(json: Map): VerifyPurchaseGoogleOptions { + return VerifyPurchaseGoogleOptions( + accessToken = json["accessToken"] as String, + isSub = json["isSub"] as Boolean?, + packageName = json["packageName"] as String, + purchaseToken = json["purchaseToken"] as String, + ) + } + } + + fun toJson(): Map = mapOf( + "accessToken" to accessToken, + "isSub" to isSub, + "packageName" to packageName, + "purchaseToken" to purchaseToken, + ) +} + +/** + * Meta Horizon (Quest) verification parameters. + * Used for server-side entitlement verification via Meta's S2S API. + * POST https://graph.oculus.com/$APP_ID/verify_entitlement + */ +public data class VerifyPurchaseHorizonOptions( + /** + * Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + */ + val accessToken: String, + /** + * The SKU for the add-on item, defined in Meta Developer Dashboard + */ + val sku: String, + /** + * The user ID of the user whose purchase you want to verify + */ + val userId: String +) { + companion object { + fun fromJson(json: Map): VerifyPurchaseHorizonOptions { + return VerifyPurchaseHorizonOptions( + accessToken = json["accessToken"] as String, + sku = json["sku"] as String, + userId = json["userId"] as String, + ) + } + } + + fun toJson(): Map = mapOf( + "accessToken" to accessToken, + "sku" to sku, + "userId" to userId, + ) +} + +/** + * Platform-specific purchase verification parameters. + * + * - apple: Verifies via App Store Server API + * - google: Verifies via Google Play Developer API + * - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) + */ public data class VerifyPurchaseProps( /** - * Android-specific validation options + * @deprecated Use google instead */ val androidOptions: VerifyPurchaseAndroidOptions? = null, + /** + * Apple App Store verification parameters. + */ + val apple: VerifyPurchaseAppleOptions? = null, + /** + * Google Play Store verification parameters. + */ + val google: VerifyPurchaseGoogleOptions? = null, + /** + * Meta Horizon (Quest) verification parameters. + */ + val horizon: VerifyPurchaseHorizonOptions? = null, /** * Product SKU to validate */ @@ -3142,6 +3286,9 @@ public data class VerifyPurchaseProps( fun fromJson(json: Map): VerifyPurchaseProps { return VerifyPurchaseProps( androidOptions = (json["androidOptions"] as Map?)?.let { VerifyPurchaseAndroidOptions.fromJson(it) }, + apple = (json["apple"] as Map?)?.let { VerifyPurchaseAppleOptions.fromJson(it) }, + google = (json["google"] as Map?)?.let { VerifyPurchaseGoogleOptions.fromJson(it) }, + horizon = (json["horizon"] as Map?)?.let { VerifyPurchaseHorizonOptions.fromJson(it) }, sku = json["sku"] as String, ) } @@ -3149,6 +3296,9 @@ public data class VerifyPurchaseProps( fun toJson(): Map = mapOf( "androidOptions" to androidOptions?.toJson(), + "apple" to apple?.toJson(), + "google" to google?.toJson(), + "horizon" to horizon?.toJson(), "sku" to sku, ) } diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index e4681362..7338537d 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1142,6 +1142,12 @@ public struct RequestPurchaseProps: Codable { } } +/// Platform-specific purchase request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// (determined at build time, not runtime) public struct RequestPurchasePropsByPlatforms: Codable { /// @deprecated Use google instead public var android: RequestPurchaseAndroidProps? @@ -1228,6 +1234,12 @@ public struct RequestSubscriptionIosProps: Codable { } } +/// Platform-specific subscription request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// (determined at build time, not runtime) public struct RequestSubscriptionPropsByPlatforms: Codable { /// @deprecated Use google instead public var android: RequestSubscriptionAndroidProps? @@ -1273,12 +1285,16 @@ public struct RequestVerifyPurchaseWithIapkitGoogleProps: Codable { } } +/// Platform-specific verification parameters for IAPKit. +/// +/// - apple: Verifies via App Store (JWS token) +/// - google: Verifies via Play Store (purchase token) public struct RequestVerifyPurchaseWithIapkitProps: Codable { /// API key used for the Authorization header (Bearer {apiKey}). public var apiKey: String? - /// Apple verification parameters. + /// Apple App Store verification parameters. public var apple: RequestVerifyPurchaseWithIapkitAppleProps? - /// Google verification parameters. + /// Google Play Store verification parameters. public var google: RequestVerifyPurchaseWithIapkitGoogleProps? public init( @@ -1310,6 +1326,7 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable { } } +/// @deprecated Use VerifyPurchaseGoogleOptions instead public struct VerifyPurchaseAndroidOptions: Codable { public var accessToken: String public var isSub: Bool? @@ -1329,17 +1346,94 @@ public struct VerifyPurchaseAndroidOptions: Codable { } } +/// Apple App Store verification parameters. +/// Used for server-side receipt validation via App Store Server API. +public struct VerifyPurchaseAppleOptions: Codable { + /// The JWS (JSON Web Signature) representation of the transaction. + public var jws: String + + public init( + jws: String + ) { + self.jws = jws + } +} + +/// Google Play Store verification parameters. +/// Used for server-side receipt validation via Google Play Developer API. +public struct VerifyPurchaseGoogleOptions: Codable { + /// Google OAuth2 access token for API authentication + public var accessToken: String + /// Whether this is a subscription purchase (affects API endpoint used) + public var isSub: Bool? + /// Android package name (e.g., com.example.app) + public var packageName: String + /// Purchase token from the purchase response + public var purchaseToken: String + + public init( + accessToken: String, + isSub: Bool? = nil, + packageName: String, + purchaseToken: String + ) { + self.accessToken = accessToken + self.isSub = isSub + self.packageName = packageName + self.purchaseToken = purchaseToken + } +} + +/// Meta Horizon (Quest) verification parameters. +/// Used for server-side entitlement verification via Meta's S2S API. +/// POST https://graph.oculus.com/$APP_ID/verify_entitlement +public struct VerifyPurchaseHorizonOptions: Codable { + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + public var accessToken: String + /// The SKU for the add-on item, defined in Meta Developer Dashboard + public var sku: String + /// The user ID of the user whose purchase you want to verify + public var userId: String + + public init( + accessToken: String, + sku: String, + userId: String + ) { + self.accessToken = accessToken + self.sku = sku + self.userId = userId + } +} + +/// Platform-specific purchase verification parameters. +/// +/// - apple: Verifies via App Store Server API +/// - google: Verifies via Google Play Developer API +/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) public struct VerifyPurchaseProps: Codable { - /// Android-specific validation options + /// @deprecated Use google instead public var androidOptions: VerifyPurchaseAndroidOptions? + /// Apple App Store verification parameters. + public var apple: VerifyPurchaseAppleOptions? + /// Google Play Store verification parameters. + public var google: VerifyPurchaseGoogleOptions? + /// Meta Horizon (Quest) verification parameters. + public var horizon: VerifyPurchaseHorizonOptions? /// Product SKU to validate public var sku: String public init( androidOptions: VerifyPurchaseAndroidOptions? = nil, + apple: VerifyPurchaseAppleOptions? = nil, + google: VerifyPurchaseGoogleOptions? = nil, + horizon: VerifyPurchaseHorizonOptions? = nil, sku: String ) { self.androidOptions = androidOptions + self.apple = apple + self.google = google + self.horizon = horizon self.sku = sku } } diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index b79e0451..38b53b05 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3281,6 +3281,12 @@ class _SubsPurchase extends RequestPurchaseProps { } } +/// Platform-specific purchase request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// (determined at build time, not runtime) class RequestPurchasePropsByPlatforms { const RequestPurchasePropsByPlatforms({ /// @deprecated Use google instead @@ -3425,6 +3431,12 @@ class RequestSubscriptionIosProps { } } +/// Platform-specific subscription request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, or Horizon when built with horizon flavor +/// (determined at build time, not runtime) class RequestSubscriptionPropsByPlatforms { const RequestSubscriptionPropsByPlatforms({ /// @deprecated Use google instead @@ -3509,21 +3521,25 @@ class RequestVerifyPurchaseWithIapkitGoogleProps { } } +/// Platform-specific verification parameters for IAPKit. +/// +/// - apple: Verifies via App Store (JWS token) +/// - google: Verifies via Play Store (purchase token) class RequestVerifyPurchaseWithIapkitProps { const RequestVerifyPurchaseWithIapkitProps({ /// API key used for the Authorization header (Bearer {apiKey}). this.apiKey, - /// Apple verification parameters. + /// Apple App Store verification parameters. this.apple, - /// Google verification parameters. + /// Google Play Store verification parameters. this.google, }); /// API key used for the Authorization header (Bearer {apiKey}). final String? apiKey; - /// Apple verification parameters. + /// Apple App Store verification parameters. final RequestVerifyPurchaseWithIapkitAppleProps? apple; - /// Google verification parameters. + /// Google Play Store verification parameters. final RequestVerifyPurchaseWithIapkitGoogleProps? google; factory RequestVerifyPurchaseWithIapkitProps.fromJson(Map json) { @@ -3574,6 +3590,7 @@ class SubscriptionProductReplacementParamsAndroid { } } +/// @deprecated Use VerifyPurchaseGoogleOptions instead class VerifyPurchaseAndroidOptions { const VerifyPurchaseAndroidOptions({ required this.accessToken, @@ -3606,22 +3623,145 @@ class VerifyPurchaseAndroidOptions { } } +/// Apple App Store verification parameters. +/// Used for server-side receipt validation via App Store Server API. +class VerifyPurchaseAppleOptions { + const VerifyPurchaseAppleOptions({ + /// The JWS (JSON Web Signature) representation of the transaction. + required this.jws, + }); + + /// The JWS (JSON Web Signature) representation of the transaction. + final String jws; + + factory VerifyPurchaseAppleOptions.fromJson(Map json) { + return VerifyPurchaseAppleOptions( + jws: json['jws'] as String, + ); + } + + Map toJson() { + return { + 'jws': jws, + }; + } +} + +/// Google Play Store verification parameters. +/// Used for server-side receipt validation via Google Play Developer API. +class VerifyPurchaseGoogleOptions { + const VerifyPurchaseGoogleOptions({ + /// Google OAuth2 access token for API authentication + required this.accessToken, + /// Whether this is a subscription purchase (affects API endpoint used) + this.isSub, + /// Android package name (e.g., com.example.app) + required this.packageName, + /// Purchase token from the purchase response + required this.purchaseToken, + }); + + /// Google OAuth2 access token for API authentication + final String accessToken; + /// Whether this is a subscription purchase (affects API endpoint used) + final bool? isSub; + /// Android package name (e.g., com.example.app) + final String packageName; + /// Purchase token from the purchase response + final String purchaseToken; + + factory VerifyPurchaseGoogleOptions.fromJson(Map json) { + return VerifyPurchaseGoogleOptions( + accessToken: json['accessToken'] as String, + isSub: json['isSub'] as bool?, + packageName: json['packageName'] as String, + purchaseToken: json['purchaseToken'] as String, + ); + } + + Map toJson() { + return { + 'accessToken': accessToken, + 'isSub': isSub, + 'packageName': packageName, + 'purchaseToken': purchaseToken, + }; + } +} + +/// Meta Horizon (Quest) verification parameters. +/// Used for server-side entitlement verification via Meta's S2S API. +/// POST https://graph.oculus.com/$APP_ID/verify_entitlement +class VerifyPurchaseHorizonOptions { + const VerifyPurchaseHorizonOptions({ + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + required this.accessToken, + /// The SKU for the add-on item, defined in Meta Developer Dashboard + required this.sku, + /// The user ID of the user whose purchase you want to verify + required this.userId, + }); + + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + final String accessToken; + /// The SKU for the add-on item, defined in Meta Developer Dashboard + final String sku; + /// The user ID of the user whose purchase you want to verify + final String userId; + + factory VerifyPurchaseHorizonOptions.fromJson(Map json) { + return VerifyPurchaseHorizonOptions( + accessToken: json['accessToken'] as String, + sku: json['sku'] as String, + userId: json['userId'] as String, + ); + } + + Map toJson() { + return { + 'accessToken': accessToken, + 'sku': sku, + 'userId': userId, + }; + } +} + +/// Platform-specific purchase verification parameters. +/// +/// - apple: Verifies via App Store Server API +/// - google: Verifies via Google Play Developer API +/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) class VerifyPurchaseProps { const VerifyPurchaseProps({ - /// Android-specific validation options + /// @deprecated Use google instead this.androidOptions, + /// Apple App Store verification parameters. + this.apple, + /// Google Play Store verification parameters. + this.google, + /// Meta Horizon (Quest) verification parameters. + this.horizon, /// Product SKU to validate required this.sku, }); - /// Android-specific validation options + /// @deprecated Use google instead final VerifyPurchaseAndroidOptions? androidOptions; + /// Apple App Store verification parameters. + final VerifyPurchaseAppleOptions? apple; + /// Google Play Store verification parameters. + final VerifyPurchaseGoogleOptions? google; + /// Meta Horizon (Quest) verification parameters. + final VerifyPurchaseHorizonOptions? horizon; /// Product SKU to validate final String sku; factory VerifyPurchaseProps.fromJson(Map json) { return VerifyPurchaseProps( androidOptions: json['androidOptions'] != null ? VerifyPurchaseAndroidOptions.fromJson(json['androidOptions'] as Map) : null, + apple: json['apple'] != null ? VerifyPurchaseAppleOptions.fromJson(json['apple'] as Map) : null, + google: json['google'] != null ? VerifyPurchaseGoogleOptions.fromJson(json['google'] as Map) : null, + horizon: json['horizon'] != null ? VerifyPurchaseHorizonOptions.fromJson(json['horizon'] as Map) : null, sku: json['sku'] as String, ); } @@ -3629,6 +3769,9 @@ class VerifyPurchaseProps { Map toJson() { return { 'androidOptions': androidOptions?.toJson(), + 'apple': apple?.toJson(), + 'google': google?.toJson(), + 'horizon': horizon?.toJson(), 'sku': sku, }; } @@ -3904,11 +4047,17 @@ abstract class MutationResolver { /// Validate purchase receipts with the configured providers Future validateReceipt({ VerifyPurchaseAndroidOptions? androidOptions, + VerifyPurchaseAppleOptions? apple, + VerifyPurchaseGoogleOptions? google, + VerifyPurchaseHorizonOptions? horizon, required String sku, }); /// Verify purchases with the configured providers Future verifyPurchase({ VerifyPurchaseAndroidOptions? androidOptions, + VerifyPurchaseAppleOptions? apple, + VerifyPurchaseGoogleOptions? google, + VerifyPurchaseHorizonOptions? horizon, required String sku, }); /// Verify purchases with a specific provider (e.g., IAPKit) @@ -3963,6 +4112,9 @@ abstract class QueryResolver { /// Validate a receipt for a specific product Future validateReceiptIOS({ VerifyPurchaseAndroidOptions? androidOptions, + VerifyPurchaseAppleOptions? apple, + VerifyPurchaseGoogleOptions? google, + VerifyPurchaseHorizonOptions? horizon, required String sku, }); } @@ -4013,10 +4165,16 @@ typedef MutationShowManageSubscriptionsIOSHandler = Future> Fu typedef MutationSyncIOSHandler = Future Function(); typedef MutationValidateReceiptHandler = Future Function({ VerifyPurchaseAndroidOptions? androidOptions, + VerifyPurchaseAppleOptions? apple, + VerifyPurchaseGoogleOptions? google, + VerifyPurchaseHorizonOptions? horizon, required String sku, }); typedef MutationVerifyPurchaseHandler = Future Function({ VerifyPurchaseAndroidOptions? androidOptions, + VerifyPurchaseAppleOptions? apple, + VerifyPurchaseGoogleOptions? google, + VerifyPurchaseHorizonOptions? horizon, required String sku, }); typedef MutationVerifyPurchaseWithProviderHandler = Future Function({ @@ -4101,6 +4259,9 @@ typedef QueryLatestTransactionIOSHandler = Future Function(String typedef QuerySubscriptionStatusIOSHandler = Future> Function(String sku); typedef QueryValidateReceiptIOSHandler = Future Function({ VerifyPurchaseAndroidOptions? androidOptions, + VerifyPurchaseAppleOptions? apple, + VerifyPurchaseGoogleOptions? google, + VerifyPurchaseHorizonOptions? horizon, required String sku, }); diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index bdd4ac65..7c88b0d8 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -917,6 +917,14 @@ export type RequestPurchaseProps = useAlternativeBilling?: boolean | null; }; +/** + * Platform-specific purchase request parameters. + * + * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. + * - apple: Always targets App Store + * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * (determined at build time, not runtime) + */ export interface RequestPurchasePropsByPlatforms { /** @deprecated Use google instead */ android?: (RequestPurchaseAndroidProps | null); @@ -963,6 +971,14 @@ export interface RequestSubscriptionIosProps { withOffer?: (DiscountOfferInputIOS | null); } +/** + * Platform-specific subscription request parameters. + * + * Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. + * - apple: Always targets App Store + * - google: Targets Play Store by default, or Horizon when built with horizon flavor + * (determined at build time, not runtime) + */ export interface RequestSubscriptionPropsByPlatforms { /** @deprecated Use google instead */ android?: (RequestSubscriptionAndroidProps | null); @@ -984,12 +1000,18 @@ export interface RequestVerifyPurchaseWithIapkitGoogleProps { purchaseToken: string; } +/** + * Platform-specific verification parameters for IAPKit. + * + * - apple: Verifies via App Store (JWS token) + * - google: Verifies via Play Store (purchase token) + */ export interface RequestVerifyPurchaseWithIapkitProps { /** API key used for the Authorization header (Bearer {apiKey}). */ apiKey?: (string | null); - /** Apple verification parameters. */ + /** Apple App Store verification parameters. */ apple?: (RequestVerifyPurchaseWithIapkitAppleProps | null); - /** Google verification parameters. */ + /** Google Play Store verification parameters. */ google?: (RequestVerifyPurchaseWithIapkitGoogleProps | null); } @@ -1088,6 +1110,7 @@ export interface ValidTimeWindowAndroid { startTimeMillis: string; } +/** @deprecated Use VerifyPurchaseGoogleOptions instead */ export interface VerifyPurchaseAndroidOptions { accessToken: string; isSub?: (boolean | null); @@ -1095,9 +1118,60 @@ export interface VerifyPurchaseAndroidOptions { productToken: string; } +/** + * Apple App Store verification parameters. + * Used for server-side receipt validation via App Store Server API. + */ +export interface VerifyPurchaseAppleOptions { + /** The JWS (JSON Web Signature) representation of the transaction. */ + jws: string; +} + +/** + * Google Play Store verification parameters. + * Used for server-side receipt validation via Google Play Developer API. + */ +export interface VerifyPurchaseGoogleOptions { + /** Google OAuth2 access token for API authentication */ + accessToken: string; + /** Whether this is a subscription purchase (affects API endpoint used) */ + isSub?: (boolean | null); + /** Android package name (e.g., com.example.app) */ + packageName: string; + /** Purchase token from the purchase response */ + purchaseToken: string; +} + +/** + * Meta Horizon (Quest) verification parameters. + * Used for server-side entitlement verification via Meta's S2S API. + * POST https://graph.oculus.com/$APP_ID/verify_entitlement + */ +export interface VerifyPurchaseHorizonOptions { + /** Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) */ + accessToken: string; + /** The SKU for the add-on item, defined in Meta Developer Dashboard */ + sku: string; + /** The user ID of the user whose purchase you want to verify */ + userId: string; +} + +/** + * Platform-specific purchase verification parameters. + * + * - apple: Verifies via App Store Server API + * - google: Verifies via Google Play Developer API + * - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) + */ export interface VerifyPurchaseProps { - /** Android-specific validation options */ + /** @deprecated Use google instead */ androidOptions?: (VerifyPurchaseAndroidOptions | null); + /** Apple App Store verification parameters. */ + apple?: (VerifyPurchaseAppleOptions | null); + /** Google Play Store verification parameters. */ + google?: (VerifyPurchaseGoogleOptions | null); + /** Meta Horizon (Quest) verification parameters. */ + horizon?: (VerifyPurchaseHorizonOptions | null); /** Product SKU to validate */ sku: string; } diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql index 7503c0a3..93b12357 100644 --- a/packages/gql/src/type-android.graphql +++ b/packages/gql/src/type-android.graphql @@ -370,6 +370,52 @@ input AndroidSubscriptionOfferInput { offerToken: String! } +""" +Google Play Store verification parameters. +Used for server-side receipt validation via Google Play Developer API. +""" +input VerifyPurchaseGoogleOptions { + """ + Android package name (e.g., com.example.app) + """ + packageName: String! + """ + Purchase token from the purchase response + """ + purchaseToken: String! + """ + Google OAuth2 access token for API authentication + """ + accessToken: String! + """ + Whether this is a subscription purchase (affects API endpoint used) + """ + isSub: Boolean +} + +""" +Meta Horizon (Quest) verification parameters. +Used for server-side entitlement verification via Meta's S2S API. +POST https://graph.oculus.com/$APP_ID/verify_entitlement +""" +input VerifyPurchaseHorizonOptions { + """ + The SKU for the add-on item, defined in Meta Developer Dashboard + """ + sku: String! + """ + The user ID of the user whose purchase you want to verify + """ + userId: String! + """ + Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + """ + accessToken: String! +} + +""" +@deprecated Use VerifyPurchaseGoogleOptions instead +""" input VerifyPurchaseAndroidOptions { packageName: String! productToken: String! diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql index daed31ce..a44b285b 100644 --- a/packages/gql/src/type-ios.graphql +++ b/packages/gql/src/type-ios.graphql @@ -227,6 +227,17 @@ input DiscountOfferInputIOS { timestamp: Float! } +""" +Apple App Store verification parameters. +Used for server-side receipt validation via App Store Server API. +""" +input VerifyPurchaseAppleOptions { + """ + The JWS (JSON Web Signature) representation of the transaction. + """ + jws: String! +} + type VerifyPurchaseResultIOS { """ Whether the receipt is valid diff --git a/packages/gql/src/type.graphql b/packages/gql/src/type.graphql index 65ce1f20..a15c4b97 100644 --- a/packages/gql/src/type.graphql +++ b/packages/gql/src/type.graphql @@ -187,7 +187,14 @@ input DeepLinkOptions { packageNameAndroid: String } -# Request props (platform-specific containers) +""" +Platform-specific purchase request parameters. + +Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +- apple: Always targets App Store +- google: Targets Play Store by default, or Horizon when built with horizon flavor + (determined at build time, not runtime) +""" input RequestPurchasePropsByPlatforms { """ Apple-specific purchase parameters @@ -207,6 +214,14 @@ input RequestPurchasePropsByPlatforms { android: RequestPurchaseAndroidProps } +""" +Platform-specific subscription request parameters. + +Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +- apple: Always targets App Store +- google: Targets Play Store by default, or Horizon when built with horizon flavor + (determined at build time, not runtime) +""" input RequestSubscriptionPropsByPlatforms { """ Apple-specific subscription parameters @@ -227,13 +242,32 @@ input RequestSubscriptionPropsByPlatforms { } # Receipt validation inputs and results +""" +Platform-specific purchase verification parameters. + +- apple: Verifies via App Store Server API +- google: Verifies via Google Play Developer API +- horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) +""" input VerifyPurchaseProps { """ Product SKU to validate """ sku: String! """ - Android-specific validation options + Apple App Store verification parameters. + """ + apple: VerifyPurchaseAppleOptions + """ + Google Play Store verification parameters. + """ + google: VerifyPurchaseGoogleOptions + """ + Meta Horizon (Quest) verification parameters. + """ + horizon: VerifyPurchaseHorizonOptions + """ + @deprecated Use google instead """ androidOptions: VerifyPurchaseAndroidOptions } @@ -256,17 +290,23 @@ input RequestVerifyPurchaseWithIapkitGoogleProps { purchaseToken: String! } +""" +Platform-specific verification parameters for IAPKit. + +- apple: Verifies via App Store (JWS token) +- google: Verifies via Play Store (purchase token) +""" input RequestVerifyPurchaseWithIapkitProps { """ API key used for the Authorization header (Bearer {apiKey}). """ apiKey: String """ - Apple verification parameters. + Apple App Store verification parameters. """ apple: RequestVerifyPurchaseWithIapkitAppleProps """ - Google verification parameters. + Google Play Store verification parameters. """ google: RequestVerifyPurchaseWithIapkitGoogleProps } From 0959e8f7ce753e6adbaab2d280c34936e8ebf7be Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 12 Dec 2025 18:17:49 +0900 Subject: [PATCH 2/9] docs: add release notes for verifyPurchase platform-specific options - openiap-gql v1.3.3 - openiap-google v1.3.13 - openiap-apple v1.3.1 New VerifyPurchaseProps structure with apple, google, horizon fields --- .../docs/src/pages/docs/updates/notes.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/packages/docs/src/pages/docs/updates/notes.tsx b/packages/docs/src/pages/docs/updates/notes.tsx index 810c8ae5..fa7c0f9e 100644 --- a/packages/docs/src/pages/docs/updates/notes.tsx +++ b/packages/docs/src/pages/docs/updates/notes.tsx @@ -19,6 +19,91 @@ function Notes() {

📝 API & Terminology Changes

+
+

+ 📅 openiap-gql v1.3.3 / openiap-google v1.3.13 / openiap-apple v1.3.1 + - Platform-Specific Verification Options +

+

+ verifyPurchase API Refactored: +

+

+ The verifyPurchase API now supports platform-specific + options for Apple, Google, and Meta Horizon stores. +

+
    +
  • + + VerifyPurchaseAppleOptions + {' '} + - Apple App Store verification with JWS token +
  • +
  • + + VerifyPurchaseGoogleOptions + {' '} + - Google Play verification with packageName, purchaseToken, and + accessToken +
  • +
  • + + VerifyPurchaseHorizonOptions + {' '} + - Meta Horizon (Quest) verification via S2S API with sku, userId, + and accessToken +
  • +
+

+ New VerifyPurchaseProps Structure: +

+ + {`// Platform-specific verification (recommended) +verifyPurchase({ + sku: 'premium_monthly', + apple: { jws: 'eyJ...' }, // iOS App Store + google: { // Google Play + packageName: 'com.example.app', + purchaseToken: 'token...', + accessToken: 'oauth_token...', + isSub: true + }, + horizon: { // Meta Quest + sku: '50_gems', + userId: '123456789', + accessToken: 'OC|app_id|app_secret' + } +}) + +// Legacy format still supported (deprecated) +verifyPurchase({ + sku: 'premium_monthly', + androidOptions: { ... } // @deprecated - use google instead +})`} + +

+ Deprecations: +

+
    +
  • + androidOptions in VerifyPurchaseProps → Use{' '} + google instead +
  • +
+

+ See:{' '} + verifyPurchase API,{' '} + VerifyPurchaseProps +

+
+
Date: Sat, 13 Dec 2025 03:07:49 +0900 Subject: [PATCH 3/9] refactor!: remove deprecated androidOptions and fix Horizon verification BREAKING CHANGE: androidOptions is removed, use google options instead - Remove androidOptions/legacyOptions from verifyPurchaseWithGooglePlay - Capture and check Horizon verification result, throw error if failed - Populate VerifyPurchaseResultAndroid with actual Horizon data --- .../java/dev/hyo/openiap/OpenIapModule.kt | 11 +++--- .../utils/PurchaseVerificationValidator.kt | 35 +++++-------------- 2 files changed, 15 insertions(+), 31 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 ea253ae9..f9d8c6bb 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 @@ -658,7 +658,10 @@ class OpenIapModule( // Use Horizon API if horizon options provided, otherwise fallback to Google Play if (props.horizon != null) { val horizonAppId = appId ?: throw OpenIapError.DeveloperError - verifyPurchaseWithHorizon(props, horizonAppId, TAG) + val horizonResult = verifyPurchaseWithHorizon(props, horizonAppId, TAG) + if (!horizonResult.success) { + throw OpenIapError.InvalidPurchaseVerification + } // Return a VerifyPurchaseResult - for now using Android result type // TODO: Add VerifyPurchaseResultHorizon to GraphQL schema VerifyPurchaseResultAndroid( @@ -671,11 +674,11 @@ class OpenIapModule( freeTrialEndDate = 0.0, gracePeriodEndDate = 0.0, parentProductId = "", - productId = props.sku, + productId = props.horizon?.sku ?: props.sku, productType = "inapp", - purchaseDate = 0.0, + purchaseDate = ((horizonResult.grantTime ?: 0L) * 1000L).toDouble(), quantity = 1, - receiptId = "", + receiptId = "horizon:${props.horizon?.userId.orEmpty()}:${props.horizon?.sku.orEmpty()}", renewalDate = 0.0, term = "", termSku = "", diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index b5e9d9d0..1eafce36 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -28,34 +28,15 @@ suspend fun verifyPurchaseWithGooglePlay( tag: String, connectionFactory: (String) -> HttpURLConnection = ::openConnection ): VerifyPurchaseResultAndroid = withContext(Dispatchers.IO) { - // Support both new google field and deprecated androidOptions val googleOptions = props.google - val legacyOptions = props.androidOptions - - val packageName: String - val purchaseToken: String - val accessToken: String - val isSub: Boolean? - - when { - googleOptions != null -> { - packageName = googleOptions.packageName - purchaseToken = googleOptions.purchaseToken - accessToken = googleOptions.accessToken - isSub = googleOptions.isSub - } - legacyOptions != null -> { - packageName = legacyOptions.packageName - purchaseToken = legacyOptions.productToken - accessToken = legacyOptions.accessToken - isSub = legacyOptions.isSub - } - else -> { - throw IllegalArgumentException( - "Google Play validation requires google options (packageName, purchaseToken, accessToken)" - ) - } - } + ?: throw IllegalArgumentException( + "Google Play validation requires google options (packageName, purchaseToken, accessToken)" + ) + + val packageName = googleOptions.packageName + val purchaseToken = googleOptions.purchaseToken + val accessToken = googleOptions.accessToken + val isSub = googleOptions.isSub if (packageName.isBlank() || purchaseToken.isBlank() || accessToken.isBlank()) { throw IllegalArgumentException( From 9438e287a4d5151d0b4fbc0a4208768c325a71f0 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 13 Dec 2025 03:16:46 +0900 Subject: [PATCH 4/9] refactor!: add VerifyPurchaseResultHorizon to schema and remove androidOptions BREAKING CHANGE: VerifyPurchaseAndroidOptions is removed from schema - Add VerifyPurchaseResultHorizon type to GraphQL schema - Add VerifyPurchaseResultHorizon to VerifyPurchaseResult union - Remove deprecated VerifyPurchaseAndroidOptions from schema - Remove deprecated androidOptions from VerifyPurchaseProps - Update verifyPurchase to return VerifyPurchaseResultHorizon directly - Fix tests to use google options instead of androidOptions - Regenerate types for all platforms --- packages/apple/Sources/Models/Types.swift | 34 +++----- .../java/dev/hyo/openiap/OpenIapModule.kt | 23 +----- .../src/main/java/dev/hyo/openiap/Types.kt | 66 ++++++++-------- .../utils/PurchaseVerificationValidator.kt | 15 +--- .../PurchaseVerificationValidatorTest.kt | 15 ++-- packages/gql/src/generated/Types.kt | 66 ++++++++-------- packages/gql/src/generated/Types.swift | 34 +++----- packages/gql/src/generated/types.dart | 79 ++++++++----------- packages/gql/src/generated/types.ts | 23 +++--- packages/gql/src/type-android.graphql | 17 ++-- packages/gql/src/type.graphql | 5 +- 11 files changed, 154 insertions(+), 223 deletions(-) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 7338537d..9c00671a 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -844,6 +844,15 @@ public struct VerifyPurchaseResultAndroid: Codable { public var testTransaction: Bool } +/// Result from Meta Horizon verify_entitlement API. +/// Returns verification status and grant time for the entitlement. +public struct VerifyPurchaseResultHorizon: Codable { + /// Unix timestamp (seconds) when the entitlement was granted. + public var grantTime: Double? + /// Whether the entitlement verification succeeded. + public var success: Bool +} + public struct VerifyPurchaseResultIOS: Codable { /// Whether the receipt is valid public var isValid: Bool @@ -1326,26 +1335,6 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable { } } -/// @deprecated Use VerifyPurchaseGoogleOptions instead -public struct VerifyPurchaseAndroidOptions: Codable { - public var accessToken: String - public var isSub: Bool? - public var packageName: String - public var productToken: String - - public init( - accessToken: String, - isSub: Bool? = nil, - packageName: String, - productToken: String - ) { - self.accessToken = accessToken - self.isSub = isSub - self.packageName = packageName - self.productToken = productToken - } -} - /// Apple App Store verification parameters. /// Used for server-side receipt validation via App Store Server API. public struct VerifyPurchaseAppleOptions: Codable { @@ -1412,8 +1401,6 @@ public struct VerifyPurchaseHorizonOptions: Codable { /// - google: Verifies via Google Play Developer API /// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) public struct VerifyPurchaseProps: Codable { - /// @deprecated Use google instead - public var androidOptions: VerifyPurchaseAndroidOptions? /// Apple App Store verification parameters. public var apple: VerifyPurchaseAppleOptions? /// Google Play Store verification parameters. @@ -1424,13 +1411,11 @@ public struct VerifyPurchaseProps: Codable { public var sku: String public init( - androidOptions: VerifyPurchaseAndroidOptions? = nil, apple: VerifyPurchaseAppleOptions? = nil, google: VerifyPurchaseGoogleOptions? = nil, horizon: VerifyPurchaseHorizonOptions? = nil, sku: String ) { - self.androidOptions = androidOptions self.apple = apple self.google = google self.horizon = horizon @@ -1761,6 +1746,7 @@ public enum Purchase: Codable, PurchaseCommon { public enum VerifyPurchaseResult: Codable { case verifyPurchaseResultAndroid(VerifyPurchaseResultAndroid) case verifyPurchaseResultIos(VerifyPurchaseResultIOS) + case verifyPurchaseResultHorizon(VerifyPurchaseResultHorizon) } // MARK: - Root Operations 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 f9d8c6bb..d5d3d4dc 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 @@ -662,28 +662,7 @@ class OpenIapModule( if (!horizonResult.success) { throw OpenIapError.InvalidPurchaseVerification } - // Return a VerifyPurchaseResult - for now using Android result type - // TODO: Add VerifyPurchaseResultHorizon to GraphQL schema - VerifyPurchaseResultAndroid( - autoRenewing = false, - betaProduct = false, - cancelDate = null, - cancelReason = null, - deferredDate = null, - deferredSku = null, - freeTrialEndDate = 0.0, - gracePeriodEndDate = 0.0, - parentProductId = "", - productId = props.horizon?.sku ?: props.sku, - productType = "inapp", - purchaseDate = ((horizonResult.grantTime ?: 0L) * 1000L).toDouble(), - quantity = 1, - receiptId = "horizon:${props.horizon?.userId.orEmpty()}:${props.horizon?.sku.orEmpty()}", - renewalDate = 0.0, - term = "", - termSku = "", - testTransaction = false - ) + horizonResult } else { verifyPurchaseWithGooglePlay(props, TAG) } 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 4c004f0e..6681fc65 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 @@ -2336,6 +2336,37 @@ public data class VerifyPurchaseResultAndroid( ) } +/** + * Result from Meta Horizon verify_entitlement API. + * Returns verification status and grant time for the entitlement. + */ +public data class VerifyPurchaseResultHorizon( + /** + * Unix timestamp (seconds) when the entitlement was granted. + */ + val grantTime: Double? = null, + /** + * Whether the entitlement verification succeeded. + */ + val success: Boolean +) : VerifyPurchaseResult { + + companion object { + fun fromJson(json: Map): VerifyPurchaseResultHorizon { + return VerifyPurchaseResultHorizon( + grantTime = (json["grantTime"] as Number?)?.toDouble(), + success = json["success"] as Boolean, + ) + } + } + + override fun toJson(): Map = mapOf( + "__typename" to "VerifyPurchaseResultHorizon", + "grantTime" to grantTime, + "success" to success, + ) +} + public data class VerifyPurchaseResultIOS( /** * Whether the receipt is valid @@ -3053,34 +3084,6 @@ public data class SubscriptionProductReplacementParamsAndroid( ) } -/** - * @deprecated Use VerifyPurchaseGoogleOptions instead - */ -public data class VerifyPurchaseAndroidOptions( - val accessToken: String, - val isSub: Boolean? = null, - val packageName: String, - val productToken: String -) { - companion object { - fun fromJson(json: Map): VerifyPurchaseAndroidOptions { - return VerifyPurchaseAndroidOptions( - accessToken = json["accessToken"] as String, - isSub = json["isSub"] as Boolean?, - packageName = json["packageName"] as String, - productToken = json["productToken"] as String, - ) - } - } - - fun toJson(): Map = mapOf( - "accessToken" to accessToken, - "isSub" to isSub, - "packageName" to packageName, - "productToken" to productToken, - ) -} - /** * Apple App Store verification parameters. * Used for server-side receipt validation via App Store Server API. @@ -3189,10 +3192,6 @@ public data class VerifyPurchaseHorizonOptions( * - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) */ public data class VerifyPurchaseProps( - /** - * @deprecated Use google instead - */ - val androidOptions: VerifyPurchaseAndroidOptions? = null, /** * Apple App Store verification parameters. */ @@ -3213,7 +3212,6 @@ public data class VerifyPurchaseProps( companion object { fun fromJson(json: Map): VerifyPurchaseProps { return VerifyPurchaseProps( - androidOptions = (json["androidOptions"] as Map?)?.let { VerifyPurchaseAndroidOptions.fromJson(it) }, apple = (json["apple"] as Map?)?.let { VerifyPurchaseAppleOptions.fromJson(it) }, google = (json["google"] as Map?)?.let { VerifyPurchaseGoogleOptions.fromJson(it) }, horizon = (json["horizon"] as Map?)?.let { VerifyPurchaseHorizonOptions.fromJson(it) }, @@ -3223,7 +3221,6 @@ public data class VerifyPurchaseProps( } fun toJson(): Map = mapOf( - "androidOptions" to androidOptions?.toJson(), "apple" to apple?.toJson(), "google" to google?.toJson(), "horizon" to horizon?.toJson(), @@ -3325,6 +3322,7 @@ public sealed interface VerifyPurchaseResult { fun fromJson(json: Map): VerifyPurchaseResult { return when (json["__typename"] as String?) { "VerifyPurchaseResultAndroid" -> VerifyPurchaseResultAndroid.fromJson(json) + "VerifyPurchaseResultHorizon" -> VerifyPurchaseResultHorizon.fromJson(json) "VerifyPurchaseResultIOS" -> VerifyPurchaseResultIOS.fromJson(json) else -> throw IllegalArgumentException("Unknown __typename for VerifyPurchaseResult: ${json["__typename"]}") } diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index 1eafce36..20e1065a 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -10,6 +10,7 @@ import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitResult import dev.hyo.openiap.VerifyPurchaseProps import dev.hyo.openiap.VerifyPurchaseResultAndroid +import dev.hyo.openiap.VerifyPurchaseResultHorizon import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.IOException @@ -138,11 +139,11 @@ suspend fun verifyPurchaseWithHorizon( val mapType = object : TypeToken>() {}.type val parsed = gson.fromJson>(responseBody, mapType) val success = parsed["success"] as? Boolean ?: false - val grantTime = (parsed["grant_time"] as? Number)?.toLong() + val grantTime = (parsed["grant_time"] as? Number)?.toDouble() VerifyPurchaseResultHorizon( - success = success, - grantTime = grantTime + grantTime = grantTime, + success = success ) } catch (jsonError: JsonSyntaxException) { OpenIapLog.warn("Failed to parse Horizon verification response: ${jsonError.message}", tag) @@ -156,14 +157,6 @@ suspend fun verifyPurchaseWithHorizon( } } -/** - * Result from Meta Horizon verify_entitlement API. - */ -data class VerifyPurchaseResultHorizon( - val success: Boolean, - val grantTime: Long? -) - suspend fun verifyPurchaseWithIapkit( props: RequestVerifyPurchaseWithIapkitProps, tag: String, diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt index ed07c1e9..df347ef8 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt @@ -8,7 +8,6 @@ import dev.hyo.openiap.IapStore import dev.hyo.openiap.IapkitPurchaseState import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitGoogleProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps -import dev.hyo.openiap.VerifyPurchaseAndroidOptions import dev.hyo.openiap.VerifyPurchaseGoogleOptions import dev.hyo.openiap.VerifyPurchaseHorizonOptions import dev.hyo.openiap.VerifyPurchaseProps @@ -83,13 +82,13 @@ class PurchaseVerificationValidatorTest { @Test fun `verifyPurchaseWithGooglePlay parses successful response`() = runTest { - val options = VerifyPurchaseAndroidOptions( + val googleOptions = VerifyPurchaseGoogleOptions( accessToken = "token", isSub = true, packageName = "dev.hyo.app", - productToken = "purchaseToken" + purchaseToken = "purchaseToken" ) - val props = VerifyPurchaseProps(androidOptions = options, sku = "premium_monthly") + val props = VerifyPurchaseProps(google = googleOptions, sku = "premium_monthly") val body = """ { "autoRenewing": true, @@ -127,13 +126,13 @@ class PurchaseVerificationValidatorTest { @Test fun `verifyPurchaseWithGooglePlay wraps non-2xx as InvalidPurchaseVerification`() = runTest { - val options = VerifyPurchaseAndroidOptions( + val googleOptions = VerifyPurchaseGoogleOptions( accessToken = "token", isSub = false, packageName = "dev.hyo.app", - productToken = "purchaseToken" + purchaseToken = "purchaseToken" ) - val props = VerifyPurchaseProps(androidOptions = options, sku = "premium_monthly") + val props = VerifyPurchaseProps(google = googleOptions, sku = "premium_monthly") try { verifyPurchaseWithGooglePlay( @@ -297,7 +296,7 @@ class PurchaseVerificationValidatorTest { ) { _ -> FakeHttpURLConnection(200, """{"success":true,"grant_time":1744148687}""") } assertEquals(true, result.success) - assertEquals(1744148687L, result.grantTime) + assertEquals(1744148687.0, result.grantTime) } @Test diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 62afd1b2..762009d7 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -2408,6 +2408,37 @@ public data class VerifyPurchaseResultAndroid( ) } +/** + * Result from Meta Horizon verify_entitlement API. + * Returns verification status and grant time for the entitlement. + */ +public data class VerifyPurchaseResultHorizon( + /** + * Unix timestamp (seconds) when the entitlement was granted. + */ + val grantTime: Double? = null, + /** + * Whether the entitlement verification succeeded. + */ + val success: Boolean +) : VerifyPurchaseResult { + + companion object { + fun fromJson(json: Map): VerifyPurchaseResultHorizon { + return VerifyPurchaseResultHorizon( + grantTime = (json["grantTime"] as Number?)?.toDouble(), + success = json["success"] as Boolean, + ) + } + } + + override fun toJson(): Map = mapOf( + "__typename" to "VerifyPurchaseResultHorizon", + "grantTime" to grantTime, + "success" to success, + ) +} + public data class VerifyPurchaseResultIOS( /** * Whether the receipt is valid @@ -3125,34 +3156,6 @@ public data class SubscriptionProductReplacementParamsAndroid( ) } -/** - * @deprecated Use VerifyPurchaseGoogleOptions instead - */ -public data class VerifyPurchaseAndroidOptions( - val accessToken: String, - val isSub: Boolean? = null, - val packageName: String, - val productToken: String -) { - companion object { - fun fromJson(json: Map): VerifyPurchaseAndroidOptions { - return VerifyPurchaseAndroidOptions( - accessToken = json["accessToken"] as String, - isSub = json["isSub"] as Boolean?, - packageName = json["packageName"] as String, - productToken = json["productToken"] as String, - ) - } - } - - fun toJson(): Map = mapOf( - "accessToken" to accessToken, - "isSub" to isSub, - "packageName" to packageName, - "productToken" to productToken, - ) -} - /** * Apple App Store verification parameters. * Used for server-side receipt validation via App Store Server API. @@ -3261,10 +3264,6 @@ public data class VerifyPurchaseHorizonOptions( * - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) */ public data class VerifyPurchaseProps( - /** - * @deprecated Use google instead - */ - val androidOptions: VerifyPurchaseAndroidOptions? = null, /** * Apple App Store verification parameters. */ @@ -3285,7 +3284,6 @@ public data class VerifyPurchaseProps( companion object { fun fromJson(json: Map): VerifyPurchaseProps { return VerifyPurchaseProps( - androidOptions = (json["androidOptions"] as Map?)?.let { VerifyPurchaseAndroidOptions.fromJson(it) }, apple = (json["apple"] as Map?)?.let { VerifyPurchaseAppleOptions.fromJson(it) }, google = (json["google"] as Map?)?.let { VerifyPurchaseGoogleOptions.fromJson(it) }, horizon = (json["horizon"] as Map?)?.let { VerifyPurchaseHorizonOptions.fromJson(it) }, @@ -3295,7 +3293,6 @@ public data class VerifyPurchaseProps( } fun toJson(): Map = mapOf( - "androidOptions" to androidOptions?.toJson(), "apple" to apple?.toJson(), "google" to google?.toJson(), "horizon" to horizon?.toJson(), @@ -3397,6 +3394,7 @@ public sealed interface VerifyPurchaseResult { fun fromJson(json: Map): VerifyPurchaseResult { return when (json["__typename"] as String?) { "VerifyPurchaseResultAndroid" -> VerifyPurchaseResultAndroid.fromJson(json) + "VerifyPurchaseResultHorizon" -> VerifyPurchaseResultHorizon.fromJson(json) "VerifyPurchaseResultIOS" -> VerifyPurchaseResultIOS.fromJson(json) else -> throw IllegalArgumentException("Unknown __typename for VerifyPurchaseResult: ${json["__typename"]}") } diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 7338537d..9c00671a 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -844,6 +844,15 @@ public struct VerifyPurchaseResultAndroid: Codable { public var testTransaction: Bool } +/// Result from Meta Horizon verify_entitlement API. +/// Returns verification status and grant time for the entitlement. +public struct VerifyPurchaseResultHorizon: Codable { + /// Unix timestamp (seconds) when the entitlement was granted. + public var grantTime: Double? + /// Whether the entitlement verification succeeded. + public var success: Bool +} + public struct VerifyPurchaseResultIOS: Codable { /// Whether the receipt is valid public var isValid: Bool @@ -1326,26 +1335,6 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable { } } -/// @deprecated Use VerifyPurchaseGoogleOptions instead -public struct VerifyPurchaseAndroidOptions: Codable { - public var accessToken: String - public var isSub: Bool? - public var packageName: String - public var productToken: String - - public init( - accessToken: String, - isSub: Bool? = nil, - packageName: String, - productToken: String - ) { - self.accessToken = accessToken - self.isSub = isSub - self.packageName = packageName - self.productToken = productToken - } -} - /// Apple App Store verification parameters. /// Used for server-side receipt validation via App Store Server API. public struct VerifyPurchaseAppleOptions: Codable { @@ -1412,8 +1401,6 @@ public struct VerifyPurchaseHorizonOptions: Codable { /// - google: Verifies via Google Play Developer API /// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) public struct VerifyPurchaseProps: Codable { - /// @deprecated Use google instead - public var androidOptions: VerifyPurchaseAndroidOptions? /// Apple App Store verification parameters. public var apple: VerifyPurchaseAppleOptions? /// Google Play Store verification parameters. @@ -1424,13 +1411,11 @@ public struct VerifyPurchaseProps: Codable { public var sku: String public init( - androidOptions: VerifyPurchaseAndroidOptions? = nil, apple: VerifyPurchaseAppleOptions? = nil, google: VerifyPurchaseGoogleOptions? = nil, horizon: VerifyPurchaseHorizonOptions? = nil, sku: String ) { - self.androidOptions = androidOptions self.apple = apple self.google = google self.horizon = horizon @@ -1761,6 +1746,7 @@ public enum Purchase: Codable, PurchaseCommon { public enum VerifyPurchaseResult: Codable { case verifyPurchaseResultAndroid(VerifyPurchaseResultAndroid) case verifyPurchaseResultIos(VerifyPurchaseResultIOS) + case verifyPurchaseResultHorizon(VerifyPurchaseResultHorizon) } // MARK: - Root Operations diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 38b53b05..51da9ede 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -2807,6 +2807,38 @@ class VerifyPurchaseResultAndroid extends VerifyPurchaseResult { } } +/// Result from Meta Horizon verify_entitlement API. +/// Returns verification status and grant time for the entitlement. +class VerifyPurchaseResultHorizon extends VerifyPurchaseResult { + const VerifyPurchaseResultHorizon({ + /// Unix timestamp (seconds) when the entitlement was granted. + this.grantTime, + /// Whether the entitlement verification succeeded. + required this.success, + }); + + /// Unix timestamp (seconds) when the entitlement was granted. + final double? grantTime; + /// Whether the entitlement verification succeeded. + final bool success; + + factory VerifyPurchaseResultHorizon.fromJson(Map json) { + return VerifyPurchaseResultHorizon( + grantTime: (json['grantTime'] as num?)?.toDouble(), + success: json['success'] as bool, + ); + } + + @override + Map toJson() { + return { + '__typename': 'VerifyPurchaseResultHorizon', + 'grantTime': grantTime, + 'success': success, + }; + } +} + class VerifyPurchaseResultIOS extends VerifyPurchaseResult { const VerifyPurchaseResultIOS({ /// Whether the receipt is valid @@ -3590,39 +3622,6 @@ class SubscriptionProductReplacementParamsAndroid { } } -/// @deprecated Use VerifyPurchaseGoogleOptions instead -class VerifyPurchaseAndroidOptions { - const VerifyPurchaseAndroidOptions({ - required this.accessToken, - this.isSub, - required this.packageName, - required this.productToken, - }); - - final String accessToken; - final bool? isSub; - final String packageName; - final String productToken; - - factory VerifyPurchaseAndroidOptions.fromJson(Map json) { - return VerifyPurchaseAndroidOptions( - accessToken: json['accessToken'] as String, - isSub: json['isSub'] as bool?, - packageName: json['packageName'] as String, - productToken: json['productToken'] as String, - ); - } - - Map toJson() { - return { - 'accessToken': accessToken, - 'isSub': isSub, - 'packageName': packageName, - 'productToken': productToken, - }; - } -} - /// Apple App Store verification parameters. /// Used for server-side receipt validation via App Store Server API. class VerifyPurchaseAppleOptions { @@ -3733,8 +3732,6 @@ class VerifyPurchaseHorizonOptions { /// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) class VerifyPurchaseProps { const VerifyPurchaseProps({ - /// @deprecated Use google instead - this.androidOptions, /// Apple App Store verification parameters. this.apple, /// Google Play Store verification parameters. @@ -3745,8 +3742,6 @@ class VerifyPurchaseProps { required this.sku, }); - /// @deprecated Use google instead - final VerifyPurchaseAndroidOptions? androidOptions; /// Apple App Store verification parameters. final VerifyPurchaseAppleOptions? apple; /// Google Play Store verification parameters. @@ -3758,7 +3753,6 @@ class VerifyPurchaseProps { factory VerifyPurchaseProps.fromJson(Map json) { return VerifyPurchaseProps( - androidOptions: json['androidOptions'] != null ? VerifyPurchaseAndroidOptions.fromJson(json['androidOptions'] as Map) : null, apple: json['apple'] != null ? VerifyPurchaseAppleOptions.fromJson(json['apple'] as Map) : null, google: json['google'] != null ? VerifyPurchaseGoogleOptions.fromJson(json['google'] as Map) : null, horizon: json['horizon'] != null ? VerifyPurchaseHorizonOptions.fromJson(json['horizon'] as Map) : null, @@ -3768,7 +3762,6 @@ class VerifyPurchaseProps { Map toJson() { return { - 'androidOptions': androidOptions?.toJson(), 'apple': apple?.toJson(), 'google': google?.toJson(), 'horizon': horizon?.toJson(), @@ -3970,6 +3963,8 @@ sealed class VerifyPurchaseResult { switch (typeName) { case 'VerifyPurchaseResultAndroid': return VerifyPurchaseResultAndroid.fromJson(json); + case 'VerifyPurchaseResultHorizon': + return VerifyPurchaseResultHorizon.fromJson(json); case 'VerifyPurchaseResultIOS': return VerifyPurchaseResultIOS.fromJson(json); } @@ -4046,7 +4041,6 @@ abstract class MutationResolver { Future syncIOS(); /// Validate purchase receipts with the configured providers Future validateReceipt({ - VerifyPurchaseAndroidOptions? androidOptions, VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, @@ -4054,7 +4048,6 @@ abstract class MutationResolver { }); /// Verify purchases with the configured providers Future verifyPurchase({ - VerifyPurchaseAndroidOptions? androidOptions, VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, @@ -4111,7 +4104,6 @@ abstract class QueryResolver { Future> subscriptionStatusIOS(String sku); /// Validate a receipt for a specific product Future validateReceiptIOS({ - VerifyPurchaseAndroidOptions? androidOptions, VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, @@ -4164,14 +4156,12 @@ typedef MutationShowAlternativeBillingDialogAndroidHandler = Future Functi typedef MutationShowManageSubscriptionsIOSHandler = Future> Function(); typedef MutationSyncIOSHandler = Future Function(); typedef MutationValidateReceiptHandler = Future Function({ - VerifyPurchaseAndroidOptions? androidOptions, VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, required String sku, }); typedef MutationVerifyPurchaseHandler = Future Function({ - VerifyPurchaseAndroidOptions? androidOptions, VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, @@ -4258,7 +4248,6 @@ typedef QueryIsTransactionVerifiedIOSHandler = Future Function(String sku) typedef QueryLatestTransactionIOSHandler = Future Function(String sku); typedef QuerySubscriptionStatusIOSHandler = Future> Function(String sku); typedef QueryValidateReceiptIOSHandler = Future Function({ - VerifyPurchaseAndroidOptions? androidOptions, VerifyPurchaseAppleOptions? apple, VerifyPurchaseGoogleOptions? google, VerifyPurchaseHorizonOptions? horizon, diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index 7c88b0d8..f69cdbca 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1110,14 +1110,6 @@ export interface ValidTimeWindowAndroid { startTimeMillis: string; } -/** @deprecated Use VerifyPurchaseGoogleOptions instead */ -export interface VerifyPurchaseAndroidOptions { - accessToken: string; - isSub?: (boolean | null); - packageName: string; - productToken: string; -} - /** * Apple App Store verification parameters. * Used for server-side receipt validation via App Store Server API. @@ -1164,8 +1156,6 @@ export interface VerifyPurchaseHorizonOptions { * - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) */ export interface VerifyPurchaseProps { - /** @deprecated Use google instead */ - androidOptions?: (VerifyPurchaseAndroidOptions | null); /** Apple App Store verification parameters. */ apple?: (VerifyPurchaseAppleOptions | null); /** Google Play Store verification parameters. */ @@ -1176,7 +1166,7 @@ export interface VerifyPurchaseProps { sku: string; } -export type VerifyPurchaseResult = VerifyPurchaseResultAndroid | VerifyPurchaseResultIOS; +export type VerifyPurchaseResult = VerifyPurchaseResultAndroid | VerifyPurchaseResultHorizon | VerifyPurchaseResultIOS; export interface VerifyPurchaseResultAndroid { autoRenewing: boolean; @@ -1199,6 +1189,17 @@ export interface VerifyPurchaseResultAndroid { testTransaction: boolean; } +/** + * Result from Meta Horizon verify_entitlement API. + * Returns verification status and grant time for the entitlement. + */ +export interface VerifyPurchaseResultHorizon { + /** Unix timestamp (seconds) when the entitlement was granted. */ + grantTime?: (number | null); + /** Whether the entitlement verification succeeded. */ + success: boolean; +} + export interface VerifyPurchaseResultIOS { /** Whether the receipt is valid */ isValid: boolean; diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql index 93b12357..ec0884b1 100644 --- a/packages/gql/src/type-android.graphql +++ b/packages/gql/src/type-android.graphql @@ -414,13 +414,18 @@ input VerifyPurchaseHorizonOptions { } """ -@deprecated Use VerifyPurchaseGoogleOptions instead +Result from Meta Horizon verify_entitlement API. +Returns verification status and grant time for the entitlement. """ -input VerifyPurchaseAndroidOptions { - packageName: String! - productToken: String! - accessToken: String! - isSub: Boolean +type VerifyPurchaseResultHorizon { + """ + Whether the entitlement verification succeeded. + """ + success: Boolean! + """ + Unix timestamp (seconds) when the entitlement was granted. + """ + grantTime: Float } type VerifyPurchaseResultAndroid { diff --git a/packages/gql/src/type.graphql b/packages/gql/src/type.graphql index a15c4b97..08e57323 100644 --- a/packages/gql/src/type.graphql +++ b/packages/gql/src/type.graphql @@ -266,15 +266,12 @@ input VerifyPurchaseProps { Meta Horizon (Quest) verification parameters. """ horizon: VerifyPurchaseHorizonOptions - """ - @deprecated Use google instead - """ - androidOptions: VerifyPurchaseAndroidOptions } union VerifyPurchaseResult = VerifyPurchaseResultAndroid | VerifyPurchaseResultIOS + | VerifyPurchaseResultHorizon input RequestVerifyPurchaseWithIapkitAppleProps { """ From 8704c07a22015b1f4842080ba57652f1199c47ab Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 13 Dec 2025 03:24:22 +0900 Subject: [PATCH 5/9] fix: remove sensitive token logging and update test name - Remove IAPKit request/response body logging to prevent token leaks - Remove responseBody from error logs - Rename test to reflect removal of androidOptions --- .../hyo/openiap/utils/PurchaseVerificationValidator.kt | 9 +-------- .../dev/hyo/openiap/PurchaseVerificationValidatorTest.kt | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index 20e1065a..8389575b 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -185,10 +185,6 @@ suspend fun verifyPurchaseWithIapkit( try { val body = gson.toJson(payload) - // Log request details for debugging - OpenIapLog.debug("IAPKit request URL: $endpoint", tag) - OpenIapLog.debug("IAPKit request body: $body", tag) - connection.outputStream.use { stream -> stream.write(body.toByteArray()) } @@ -199,10 +195,8 @@ suspend fun verifyPurchaseWithIapkit( ?.use { it.readText() } .orElse("") - OpenIapLog.debug("IAPKit response (HTTP $statusCode): $responseBody", tag) - if (statusCode !in 200..299) { - OpenIapLog.warn("verifyPurchaseWithProvider failed (HTTP $statusCode) [$store]: $responseBody", tag) + OpenIapLog.warn("verifyPurchaseWithProvider failed (HTTP $statusCode) [$store]", tag) // Extract concise error message from IAPKit response // IAPKit returns nested error format - extract the deepest originalError val errorMessage = try { @@ -230,7 +224,6 @@ suspend fun verifyPurchaseWithIapkit( this["store"] = store.toJson() } } - OpenIapLog.debug("IAPKit normalized response: $normalizedParsed", tag) RequestVerifyPurchaseWithIapkitResult.fromJson(normalizedParsed) } catch (jsonError: Exception) { OpenIapLog.warn("Failed to parse IAPKit verification response: ${jsonError.message}", tag) diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt index df347ef8..940265d4 100644 --- a/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt @@ -25,7 +25,7 @@ import org.junit.Test class PurchaseVerificationValidatorTest { @Test - fun `verifyPurchaseWithGooglePlay throws without google or androidOptions`() = runTest { + fun `verifyPurchaseWithGooglePlay throws without google options`() = runTest { val props = VerifyPurchaseProps(sku = "product.sku") try { From b14bf1885841899b1bd93ea063678708d31d218d Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 13 Dec 2025 03:28:15 +0900 Subject: [PATCH 6/9] docs: add security warnings for sensitive tokens in verification options - Add SECURITY warnings to VerifyPurchaseAppleOptions (jws) - Add SECURITY warnings to VerifyPurchaseGoogleOptions (accessToken, purchaseToken) - Add SECURITY warnings to VerifyPurchaseHorizonOptions (accessToken) - Regenerate types for all platforms with security documentation --- packages/apple/Sources/Models/Types.swift | 16 +++++++++--- .../src/main/java/dev/hyo/openiap/Types.kt | 16 +++++++++--- packages/gql/src/generated/Types.kt | 16 +++++++++--- packages/gql/src/generated/Types.swift | 16 +++++++++--- packages/gql/src/generated/types.dart | 26 ++++++++++++++----- packages/gql/src/generated/types.ts | 26 ++++++++++++++++--- packages/gql/src/type-android.graphql | 13 +++++++--- packages/gql/src/type-ios.graphql | 3 +++ 8 files changed, 107 insertions(+), 25 deletions(-) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 9c00671a..42365f34 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1337,8 +1337,11 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable { /// Apple App Store verification parameters. /// Used for server-side receipt validation via App Store Server API. +/// +/// ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data. public struct VerifyPurchaseAppleOptions: Codable { /// The JWS (JSON Web Signature) representation of the transaction. + /// ⚠️ Sensitive: Do not log this value. public var jws: String public init( @@ -1350,14 +1353,18 @@ public struct VerifyPurchaseAppleOptions: Codable { /// Google Play Store verification parameters. /// Used for server-side receipt validation via Google Play Developer API. +/// +/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. public struct VerifyPurchaseGoogleOptions: Codable { - /// Google OAuth2 access token for API authentication + /// Google OAuth2 access token for API authentication. + /// ⚠️ Sensitive: Do not log this value. public var accessToken: String /// Whether this is a subscription purchase (affects API endpoint used) public var isSub: Bool? /// Android package name (e.g., com.example.app) public var packageName: String - /// Purchase token from the purchase response + /// Purchase token from the purchase response. + /// ⚠️ Sensitive: Do not log this value. public var purchaseToken: String public init( @@ -1376,8 +1383,11 @@ public struct VerifyPurchaseGoogleOptions: Codable { /// Meta Horizon (Quest) verification parameters. /// Used for server-side entitlement verification via Meta's S2S API. /// POST https://graph.oculus.com/$APP_ID/verify_entitlement +/// +/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. public struct VerifyPurchaseHorizonOptions: Codable { - /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + /// ⚠️ Sensitive: Do not log this value. public var accessToken: String /// The SKU for the add-on item, defined in Meta Developer Dashboard public var sku: String 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 6681fc65..b4ae4233 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 @@ -3087,10 +3087,13 @@ public data class SubscriptionProductReplacementParamsAndroid( /** * Apple App Store verification parameters. * Used for server-side receipt validation via App Store Server API. + * + * ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data. */ public data class VerifyPurchaseAppleOptions( /** * The JWS (JSON Web Signature) representation of the transaction. + * ⚠️ Sensitive: Do not log this value. */ val jws: String ) { @@ -3110,10 +3113,13 @@ public data class VerifyPurchaseAppleOptions( /** * Google Play Store verification parameters. * Used for server-side receipt validation via Google Play Developer API. + * + * ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. */ public data class VerifyPurchaseGoogleOptions( /** - * Google OAuth2 access token for API authentication + * Google OAuth2 access token for API authentication. + * ⚠️ Sensitive: Do not log this value. */ val accessToken: String, /** @@ -3125,7 +3131,8 @@ public data class VerifyPurchaseGoogleOptions( */ val packageName: String, /** - * Purchase token from the purchase response + * Purchase token from the purchase response. + * ⚠️ Sensitive: Do not log this value. */ val purchaseToken: String ) { @@ -3152,10 +3159,13 @@ public data class VerifyPurchaseGoogleOptions( * Meta Horizon (Quest) verification parameters. * Used for server-side entitlement verification via Meta's S2S API. * POST https://graph.oculus.com/$APP_ID/verify_entitlement + * + * ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. */ public data class VerifyPurchaseHorizonOptions( /** - * Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + * Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + * ⚠️ Sensitive: Do not log this value. */ val accessToken: String, /** diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 762009d7..a060f318 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3159,10 +3159,13 @@ public data class SubscriptionProductReplacementParamsAndroid( /** * Apple App Store verification parameters. * Used for server-side receipt validation via App Store Server API. + * + * ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data. */ public data class VerifyPurchaseAppleOptions( /** * The JWS (JSON Web Signature) representation of the transaction. + * ⚠️ Sensitive: Do not log this value. */ val jws: String ) { @@ -3182,10 +3185,13 @@ public data class VerifyPurchaseAppleOptions( /** * Google Play Store verification parameters. * Used for server-side receipt validation via Google Play Developer API. + * + * ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. */ public data class VerifyPurchaseGoogleOptions( /** - * Google OAuth2 access token for API authentication + * Google OAuth2 access token for API authentication. + * ⚠️ Sensitive: Do not log this value. */ val accessToken: String, /** @@ -3197,7 +3203,8 @@ public data class VerifyPurchaseGoogleOptions( */ val packageName: String, /** - * Purchase token from the purchase response + * Purchase token from the purchase response. + * ⚠️ Sensitive: Do not log this value. */ val purchaseToken: String ) { @@ -3224,10 +3231,13 @@ public data class VerifyPurchaseGoogleOptions( * Meta Horizon (Quest) verification parameters. * Used for server-side entitlement verification via Meta's S2S API. * POST https://graph.oculus.com/$APP_ID/verify_entitlement + * + * ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. */ public data class VerifyPurchaseHorizonOptions( /** - * Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + * Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + * ⚠️ Sensitive: Do not log this value. */ val accessToken: String, /** diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 9c00671a..42365f34 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1337,8 +1337,11 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable { /// Apple App Store verification parameters. /// Used for server-side receipt validation via App Store Server API. +/// +/// ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data. public struct VerifyPurchaseAppleOptions: Codable { /// The JWS (JSON Web Signature) representation of the transaction. + /// ⚠️ Sensitive: Do not log this value. public var jws: String public init( @@ -1350,14 +1353,18 @@ public struct VerifyPurchaseAppleOptions: Codable { /// Google Play Store verification parameters. /// Used for server-side receipt validation via Google Play Developer API. +/// +/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. public struct VerifyPurchaseGoogleOptions: Codable { - /// Google OAuth2 access token for API authentication + /// Google OAuth2 access token for API authentication. + /// ⚠️ Sensitive: Do not log this value. public var accessToken: String /// Whether this is a subscription purchase (affects API endpoint used) public var isSub: Bool? /// Android package name (e.g., com.example.app) public var packageName: String - /// Purchase token from the purchase response + /// Purchase token from the purchase response. + /// ⚠️ Sensitive: Do not log this value. public var purchaseToken: String public init( @@ -1376,8 +1383,11 @@ public struct VerifyPurchaseGoogleOptions: Codable { /// Meta Horizon (Quest) verification parameters. /// Used for server-side entitlement verification via Meta's S2S API. /// POST https://graph.oculus.com/$APP_ID/verify_entitlement +/// +/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. public struct VerifyPurchaseHorizonOptions: Codable { - /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + /// ⚠️ Sensitive: Do not log this value. public var accessToken: String /// The SKU for the add-on item, defined in Meta Developer Dashboard public var sku: String diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 51da9ede..ecde6bfe 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3624,13 +3624,17 @@ class SubscriptionProductReplacementParamsAndroid { /// Apple App Store verification parameters. /// Used for server-side receipt validation via App Store Server API. +/// +/// ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data. class VerifyPurchaseAppleOptions { const VerifyPurchaseAppleOptions({ /// The JWS (JSON Web Signature) representation of the transaction. + /// ⚠️ Sensitive: Do not log this value. required this.jws, }); /// The JWS (JSON Web Signature) representation of the transaction. + /// ⚠️ Sensitive: Do not log this value. final String jws; factory VerifyPurchaseAppleOptions.fromJson(Map json) { @@ -3648,25 +3652,31 @@ class VerifyPurchaseAppleOptions { /// Google Play Store verification parameters. /// Used for server-side receipt validation via Google Play Developer API. +/// +/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. class VerifyPurchaseGoogleOptions { const VerifyPurchaseGoogleOptions({ - /// Google OAuth2 access token for API authentication + /// Google OAuth2 access token for API authentication. + /// ⚠️ Sensitive: Do not log this value. required this.accessToken, /// Whether this is a subscription purchase (affects API endpoint used) this.isSub, /// Android package name (e.g., com.example.app) required this.packageName, - /// Purchase token from the purchase response + /// Purchase token from the purchase response. + /// ⚠️ Sensitive: Do not log this value. required this.purchaseToken, }); - /// Google OAuth2 access token for API authentication + /// Google OAuth2 access token for API authentication. + /// ⚠️ Sensitive: Do not log this value. final String accessToken; /// Whether this is a subscription purchase (affects API endpoint used) final bool? isSub; /// Android package name (e.g., com.example.app) final String packageName; - /// Purchase token from the purchase response + /// Purchase token from the purchase response. + /// ⚠️ Sensitive: Do not log this value. final String purchaseToken; factory VerifyPurchaseGoogleOptions.fromJson(Map json) { @@ -3691,9 +3701,12 @@ class VerifyPurchaseGoogleOptions { /// Meta Horizon (Quest) verification parameters. /// Used for server-side entitlement verification via Meta's S2S API. /// POST https://graph.oculus.com/$APP_ID/verify_entitlement +/// +/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. class VerifyPurchaseHorizonOptions { const VerifyPurchaseHorizonOptions({ - /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + /// ⚠️ Sensitive: Do not log this value. required this.accessToken, /// The SKU for the add-on item, defined in Meta Developer Dashboard required this.sku, @@ -3701,7 +3714,8 @@ class VerifyPurchaseHorizonOptions { required this.userId, }); - /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + /// ⚠️ Sensitive: Do not log this value. final String accessToken; /// The SKU for the add-on item, defined in Meta Developer Dashboard final String sku; diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index f69cdbca..2f7ebf1a 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1113,24 +1113,37 @@ export interface ValidTimeWindowAndroid { /** * Apple App Store verification parameters. * Used for server-side receipt validation via App Store Server API. + * + * ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data. */ export interface VerifyPurchaseAppleOptions { - /** The JWS (JSON Web Signature) representation of the transaction. */ + /** + * The JWS (JSON Web Signature) representation of the transaction. + * ⚠️ Sensitive: Do not log this value. + */ jws: string; } /** * Google Play Store verification parameters. * Used for server-side receipt validation via Google Play Developer API. + * + * ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. */ export interface VerifyPurchaseGoogleOptions { - /** Google OAuth2 access token for API authentication */ + /** + * Google OAuth2 access token for API authentication. + * ⚠️ Sensitive: Do not log this value. + */ accessToken: string; /** Whether this is a subscription purchase (affects API endpoint used) */ isSub?: (boolean | null); /** Android package name (e.g., com.example.app) */ packageName: string; - /** Purchase token from the purchase response */ + /** + * Purchase token from the purchase response. + * ⚠️ Sensitive: Do not log this value. + */ purchaseToken: string; } @@ -1138,9 +1151,14 @@ export interface VerifyPurchaseGoogleOptions { * Meta Horizon (Quest) verification parameters. * Used for server-side entitlement verification via Meta's S2S API. * POST https://graph.oculus.com/$APP_ID/verify_entitlement + * + * ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. */ export interface VerifyPurchaseHorizonOptions { - /** Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) */ + /** + * Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + * ⚠️ Sensitive: Do not log this value. + */ accessToken: string; /** The SKU for the add-on item, defined in Meta Developer Dashboard */ sku: string; diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql index ec0884b1..57a9175b 100644 --- a/packages/gql/src/type-android.graphql +++ b/packages/gql/src/type-android.graphql @@ -373,6 +373,8 @@ input AndroidSubscriptionOfferInput { """ Google Play Store verification parameters. Used for server-side receipt validation via Google Play Developer API. + +⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. """ input VerifyPurchaseGoogleOptions { """ @@ -380,11 +382,13 @@ input VerifyPurchaseGoogleOptions { """ packageName: String! """ - Purchase token from the purchase response + Purchase token from the purchase response. + ⚠️ Sensitive: Do not log this value. """ purchaseToken: String! """ - Google OAuth2 access token for API authentication + Google OAuth2 access token for API authentication. + ⚠️ Sensitive: Do not log this value. """ accessToken: String! """ @@ -397,6 +401,8 @@ input VerifyPurchaseGoogleOptions { Meta Horizon (Quest) verification parameters. Used for server-side entitlement verification via Meta's S2S API. POST https://graph.oculus.com/$APP_ID/verify_entitlement + +⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. """ input VerifyPurchaseHorizonOptions { """ @@ -408,7 +414,8 @@ input VerifyPurchaseHorizonOptions { """ userId: String! """ - Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token) + Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + ⚠️ Sensitive: Do not log this value. """ accessToken: String! } diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql index a44b285b..d4b036a1 100644 --- a/packages/gql/src/type-ios.graphql +++ b/packages/gql/src/type-ios.graphql @@ -230,10 +230,13 @@ input DiscountOfferInputIOS { """ Apple App Store verification parameters. Used for server-side receipt validation via App Store Server API. + +⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data. """ input VerifyPurchaseAppleOptions { """ The JWS (JSON Web Signature) representation of the transaction. + ⚠️ Sensitive: Do not log this value. """ jws: String! } From a60bcb7ef59b65903a7841281343633751df20c5 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 13 Dec 2025 03:32:23 +0900 Subject: [PATCH 7/9] fix: URL-encode path segments and use explicit UTF-8 charset - Add encodePathSegment helper for URL path encoding - URL-encode packageName, sku, purchaseToken in Google Play API URL - Use explicit Charsets.UTF_8 for Horizon form data encoding --- .../hyo/openiap/utils/PurchaseVerificationValidator.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index 8389575b..ba03179f 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.withContext import java.io.IOException import java.net.HttpURLConnection import java.net.URL +import java.net.URLEncoder private const val DEFAULT_IAPKIT_ENDPOINT = "https://api.iapkit.com/v1/purchase/verify" private val gson = Gson() @@ -24,6 +25,9 @@ private fun openConnection(url: String): HttpURLConnection { return URL(url).openConnection() as HttpURLConnection } +private fun encodePathSegment(value: String): String = + URLEncoder.encode(value, Charsets.UTF_8.name()).replace("+", "%20") + suspend fun verifyPurchaseWithGooglePlay( props: VerifyPurchaseProps, tag: String, @@ -47,7 +51,8 @@ suspend fun verifyPurchaseWithGooglePlay( val typeSegment = if (isSub == true) "subscriptions" else "products" val baseUrl = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications" - val url = "$baseUrl/$packageName/purchases/$typeSegment/${props.sku}/tokens/$purchaseToken" + val url = "$baseUrl/${encodePathSegment(packageName)}/purchases/$typeSegment/" + + "${encodePathSegment(props.sku)}/tokens/${encodePathSegment(purchaseToken)}" val connection = connectionFactory(url).apply { requestMethod = "GET" @@ -120,7 +125,7 @@ suspend fun verifyPurchaseWithHorizon( } connection.outputStream.use { stream -> - stream.write(formData.toByteArray()) + stream.write(formData.toByteArray(Charsets.UTF_8)) } val statusCode = connection.responseCode From 0b800ce6234efbf7d7b612f5ca04463f8098445b Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 13 Dec 2025 03:38:04 +0900 Subject: [PATCH 8/9] fix: use explicit UTF-8 charset for IAPKit request body --- .../java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index ba03179f..1f71393e 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -191,7 +191,7 @@ suspend fun verifyPurchaseWithIapkit( val body = gson.toJson(payload) connection.outputStream.use { stream -> - stream.write(body.toByteArray()) + stream.write(body.toByteArray(Charsets.UTF_8)) } val statusCode = connection.responseCode From aeb2c38eecc5f8ab10ee3d69ac68c9ee84348460 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 13 Dec 2025 03:45:36 +0900 Subject: [PATCH 9/9] fix: remove sensitive responseBody from Horizon error log --- .../java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt index 1f71393e..b3e97875 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt @@ -135,7 +135,7 @@ suspend fun verifyPurchaseWithHorizon( .orElse("") if (statusCode !in 200..299) { - OpenIapLog.warn("Horizon verifyPurchase failed (HTTP $statusCode): $responseBody", tag) + OpenIapLog.warn("Horizon verifyPurchase failed (HTTP $statusCode)", tag) throw OpenIapError.InvalidPurchaseVerification }