Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
android:supportsRtl="true">

<!-- Horizon OS (Meta Quest) Configuration -->
<!-- <meta-data
<meta-data
android:name="com.meta.horizon.platform.HORIZON_APP_ID"
android:value="31705015229097839" /> -->
android:value="31705015229097839" />

<activity
android:name=".MainActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,16 @@ internal object HorizonBillingConverters {
val token = purchaseToken
val productsList = products ?: emptyList()
val state = PurchaseState.fromHorizonState(getPurchaseState())

return PurchaseAndroid(
autoRenewingAndroid = isAutoRenewing(),
currentPlanId = basePlanId,
dataAndroid = originalJson,
developerPayloadAndroid = developerPayload,
id = orderId ?: token,
// Horizon reports a present-but-blank orderId (the module already
// logs orderIdPresent via isNullOrBlank), so a null-only fallback
// produced an empty id/transactionId that the SDKs' strict
// purchase decoders reject, failing the whole batch on device.
id = orderId?.takeIf { it.isNotBlank() } ?: token,
ids = productsList,
isAcknowledgedAndroid = isAcknowledged(),
isAutoRenewing = isAutoRenewing(),
Expand All @@ -145,7 +148,7 @@ internal object HorizonBillingConverters {
signatureAndroid = signature,
store = IapStore.Horizon,
transactionDate = (purchaseTime ?: 0L).toDouble(),
transactionId = orderId ?: token
transactionId = orderId?.takeIf { it.isNotBlank() } ?: token
)
}

Expand All @@ -158,7 +161,8 @@ internal object HorizonBillingConverters {
purchaseToken = purchaseToken,
purchaseTokenAndroid = purchaseToken,
transactionDate = (purchaseTime ?: 0L).toDouble(),
transactionId = orderId ?: purchaseToken
// Same blank-orderId fallback as toPurchase above.
transactionId = orderId?.takeIf { it.isNotBlank() } ?: purchaseToken
)

fun PurchaseAndroid.toActiveSubscription(): ActiveSubscription = ActiveSubscription(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package dev.hyo.openiap

import com.meta.horizon.billingclient.api.Purchase as HorizonPurchase
import dev.hyo.openiap.utils.HorizonBillingConverters
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/**
* The Horizon billing-compatibility `Purchase.orderId` is a non-null String
* and arrives blank on device, so a null-only `orderId ?: token` fallback
* produced an empty `id`/`transactionId`. The SDKs' strict purchase decoders
* reject an empty id, which failed the whole available-purchases batch on a
* Quest 3 with real store data. These tests pin the blank-aware fallback.
*/
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [29])
class HorizonBlankOrderIdTest {

private fun horizonPurchase(orderId: String): HorizonPurchase = HorizonPurchase(
5L,
"purchase-token",
listOf("product-id"),
"dev.hyo.openiap.test",
"",
orderId,
"""{"purchaseState":0,"acknowledged":false,"autoRenewing":false}""",
1,
"signature",
)

@Test
fun `blank orderId falls back to the purchase token for id and transactionId`() {
val purchase = with(HorizonBillingConverters) {
horizonPurchase(orderId = "").toPurchase()
}

assertEquals("purchase-token", purchase.id)
assertEquals("purchase-token", purchase.transactionId)
}

@Test
fun `present orderId is preserved for id and transactionId`() {
val purchase = with(HorizonBillingConverters) {
horizonPurchase(orderId = "order-1").toPurchase()
}

assertEquals("order-1", purchase.id)
assertEquals("order-1", purchase.transactionId)
}

@Test
fun `blank orderId falls back to the purchase token for active subscriptions`() {
val subscription = with(HorizonBillingConverters) {
horizonPurchase(orderId = "").toActiveSubscription()
}

assertEquals("purchase-token", subscription.transactionId)
}
}
3 changes: 2 additions & 1 deletion scripts/audit-purchase-payload-parity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1835,7 +1835,8 @@ function checkGooglePurchasePayloadContracts() {
productId: /^productsList\.firstOrNull\(\)\.orEmpty\(\)$/,
purchaseToken: /^token$/,
signatureAndroid: /^signature$/,
transactionId: /^orderId \?: token$/,
transactionId:
/^orderId\?\.takeIf \{ it\.isNotBlank\(\) \} \?: token$/,
},
intentionallyDefaultedFields: [
"isSuspendedAndroid",
Expand Down