Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.
Merged
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
178 changes: 109 additions & 69 deletions android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import dev.hyo.openiap.ExternalLinkLaunchModeAndroid as OpenIapExternalLinkLaunc
import dev.hyo.openiap.ExternalLinkTypeAndroid as OpenIapExternalLinkType
import dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener
import dev.hyo.openiap.store.OpenIapStore
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CompletableDeferred
Expand Down Expand Up @@ -100,19 +101,34 @@ class HybridRnIap : HybridRnIapSpec() {
// CRITICAL: Set Activity BEFORE calling initConnection
// Horizon SDK needs Activity to initialize OVRPlatform with proper returnComponent
// https://github.com/meta-quest/Meta-Spatial-SDK-Samples/issues/82#issuecomment-3452577530
withContext(Dispatchers.Main) {
runCatching { context.currentActivity }
.onSuccess { activity ->
if (activity != null) {
RnIapLog.debug("Activity available: ${activity.javaClass.name}")
openIap.setActivity(activity)
} else {
RnIapLog.warn("Activity is null during initConnection")
try {
withContext(Dispatchers.Main) {
runCatching { context.currentActivity }
.onSuccess { activity ->
if (activity != null) {
RnIapLog.debug("Activity available: ${activity.javaClass.name}")
openIap.setActivity(activity)
} else {
RnIapLog.warn("Activity is null during initConnection")
}
}
}
.onFailure {
RnIapLog.warn("Activity not available during initConnection - OpenIAP will use Context")
}
.onFailure {
RnIapLog.warn("Activity not available during initConnection - OpenIAP will use Context")
}
}
} catch (err: CancellationException) {
throw err
} catch (err: Throwable) {
val error = OpenIAPError.InitConnection
val errorMessage = err.message ?: err.javaClass.name
RnIapLog.failure("initConnection.setActivity", err)
throw OpenIapException(
toErrorJson(
error = error,
debugMessage = errorMessage,
messageOverride = "Failed to set activity: $errorMessage"
)
)
Comment thread
hyochan marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Single-flight: capture or create the shared Deferred atomically
Expand All @@ -128,64 +144,88 @@ class HybridRnIap : HybridRnIapSpec() {
return@async result
}

if (!listenersAttached) {
listenersAttached = true
RnIapLog.payload("listeners.attach", null)
openIap.addPurchaseUpdateListener(OpenIapPurchaseUpdateListener { p ->
runCatching {
RnIapLog.result(
"purchaseUpdatedListener",
mapOf("id" to p.id, "sku" to p.productId)
)
sendPurchaseUpdate(convertToNitroPurchase(p))
}.onFailure { RnIapLog.failure("purchaseUpdatedListener", it) }
})
openIap.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e ->
val code = OpenIAPError.toCode(e)
val message = e.message ?: OpenIAPError.defaultMessage(code)
runCatching {
RnIapLog.result(
"purchaseErrorListener",
mapOf("code" to code, "message" to message)
)
sendPurchaseError(
NitroPurchaseResult(
responseCode = -1.0,
debugMessage = null,
code = code,
message = message,
purchaseToken = null
try {
if (!listenersAttached) {
listenersAttached = true
RnIapLog.payload("listeners.attach", null)
openIap.addPurchaseUpdateListener(OpenIapPurchaseUpdateListener { p ->
runCatching {
RnIapLog.result(
"purchaseUpdatedListener",
mapOf("id" to p.id, "sku" to p.productId)
)
)
}.onFailure { RnIapLog.failure("purchaseErrorListener", it) }
})
openIap.addUserChoiceBillingListener(OpenIapUserChoiceBillingListener { details ->
runCatching {
RnIapLog.result(
"userChoiceBillingListener",
mapOf("products" to details.products, "token" to details.externalTransactionToken)
)
val nitroDetails = UserChoiceBillingDetails(
externalTransactionToken = details.externalTransactionToken,
products = details.products.toTypedArray()
)
sendUserChoiceBilling(nitroDetails)
}.onFailure { RnIapLog.failure("userChoiceBillingListener", it) }
})
// Developer Provided Billing listener (External Payments - 8.3.0+)
openIap.addDeveloperProvidedBillingListener(OpenIapDeveloperProvidedBillingListener { details ->
runCatching {
RnIapLog.result(
"developerProvidedBillingListener",
mapOf("token" to details.externalTransactionToken)
)
val nitroDetails = DeveloperProvidedBillingDetailsAndroid(
externalTransactionToken = details.externalTransactionToken
)
sendDeveloperProvidedBilling(nitroDetails)
}.onFailure { RnIapLog.failure("developerProvidedBillingListener", it) }
})
RnIapLog.result("listeners.attach", "attached")
sendPurchaseUpdate(convertToNitroPurchase(p))
}.onFailure { RnIapLog.failure("purchaseUpdatedListener", it) }
})
openIap.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e ->
val code = OpenIAPError.toCode(e)
val message = e.message ?: OpenIAPError.defaultMessage(code)
runCatching {
RnIapLog.result(
"purchaseErrorListener",
mapOf("code" to code, "message" to message)
)
sendPurchaseError(
NitroPurchaseResult(
responseCode = -1.0,
debugMessage = null,
code = code,
message = message,
purchaseToken = null
)
)
}.onFailure { RnIapLog.failure("purchaseErrorListener", it) }
})
openIap.addUserChoiceBillingListener(OpenIapUserChoiceBillingListener { details ->
runCatching {
RnIapLog.result(
"userChoiceBillingListener",
mapOf("products" to details.products, "token" to details.externalTransactionToken)
)
val nitroDetails = UserChoiceBillingDetails(
externalTransactionToken = details.externalTransactionToken,
products = details.products.toTypedArray()
)
sendUserChoiceBilling(nitroDetails)
}.onFailure { RnIapLog.failure("userChoiceBillingListener", it) }
})
// Developer Provided Billing listener (External Payments - 8.3.0+)
openIap.addDeveloperProvidedBillingListener(OpenIapDeveloperProvidedBillingListener { details ->
runCatching {
RnIapLog.result(
"developerProvidedBillingListener",
mapOf("token" to details.externalTransactionToken)
)
val nitroDetails = DeveloperProvidedBillingDetailsAndroid(
externalTransactionToken = details.externalTransactionToken
)
sendDeveloperProvidedBilling(nitroDetails)
}.onFailure { RnIapLog.failure("developerProvidedBillingListener", it) }
})
RnIapLog.result("listeners.attach", "attached")
}
} catch (err: CancellationException) {
throw err
} catch (err: Throwable) {
listenersAttached = false
val error = OpenIAPError.InitConnection
val errorMessage = err.message ?: err.javaClass.name
RnIapLog.failure("initConnection.listeners", err)
val wrapped = OpenIapException(
toErrorJson(
error = error,
debugMessage = errorMessage,
messageOverride = "Failed to register billing listeners: $errorMessage"
)
)
synchronized(initLock) {
initDeferred?.let { deferred ->
if (!deferred.isCompleted) deferred.completeExceptionally(wrapped)
}
initDeferred = null
}
isInitialized = false
throw wrapped
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// We created it above; reuse the shared instance
Expand Down