Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.
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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:

validate-ios:
runs-on: macos-15
timeout-minutes: 30
timeout-minutes: 60
env:
XCODE_VERSION: 16.4
steps:
Expand Down
40 changes: 22 additions & 18 deletions android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ class HybridRnIap : HybridRnIapSpec() {
productTypeBySku.clear()
isInitialized = false
listenersAttached = false
purchaseUpdatedListeners.clear()
purchaseErrorListeners.clear()
synchronized(purchaseUpdatedListeners) { purchaseUpdatedListeners.clear() }
synchronized(purchaseErrorListeners) { purchaseErrorListeners.clear() }
promotedProductListenersIOS.clear()
userChoiceBillingListenersAndroid.clear()
developerProvidedBillingListenersAndroid.clear()
Expand Down Expand Up @@ -731,21 +731,27 @@ class HybridRnIap : HybridRnIapSpec() {

// Event listener methods
override fun addPurchaseUpdatedListener(listener: (purchase: NitroPurchase) -> Unit) {
purchaseUpdatedListeners.add(listener)
synchronized(purchaseUpdatedListeners) {
purchaseUpdatedListeners.add(listener)
}
}

override fun addPurchaseErrorListener(listener: (error: NitroPurchaseResult) -> Unit) {
purchaseErrorListeners.add(listener)
synchronized(purchaseErrorListeners) {
purchaseErrorListeners.add(listener)
}
}

override fun removePurchaseUpdatedListener(listener: (purchase: NitroPurchase) -> Unit) {
// Note: Kotlin doesn't have easy closure comparison, so we'll clear all listeners
purchaseUpdatedListeners.clear()
synchronized(purchaseUpdatedListeners) {
purchaseUpdatedListeners.remove(listener)
}
}

override fun removePurchaseErrorListener(listener: (error: NitroPurchaseResult) -> Unit) {
// Note: Kotlin doesn't have easy closure comparison, so we'll clear all listeners
purchaseErrorListeners.clear()
synchronized(purchaseErrorListeners) {
purchaseErrorListeners.remove(listener)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

override fun addPromotedProductListenerIOS(listener: (product: NitroProduct) -> Unit) {
Expand Down Expand Up @@ -773,11 +779,10 @@ class HybridRnIap : HybridRnIapSpec() {
"sendPurchaseUpdate",
mapOf("productId" to purchase.productId, "platform" to purchase.platform)
)
for (listener in purchaseUpdatedListeners) {
listener(purchase)
}
val snapshot = synchronized(purchaseUpdatedListeners) { ArrayList(purchaseUpdatedListeners) }
snapshot.forEach { it(purchase) }
}

/**
* Send purchase error event to listeners
*/
Expand All @@ -786,9 +791,8 @@ class HybridRnIap : HybridRnIapSpec() {
"sendPurchaseError",
mapOf("code" to error.code, "message" to error.message)
)
for (listener in purchaseErrorListeners) {
listener(error)
}
val snapshot = synchronized(purchaseErrorListeners) { ArrayList(purchaseErrorListeners) }
snapshot.forEach { it(error) }
}

/**
Expand Down
15 changes: 12 additions & 3 deletions ios/HybridRnIap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,10 @@ class HybridRnIap: HybridRnIapSpec {
if purchaseUpdatedSub == nil {
RnIapLog.payload("purchaseUpdatedListener.register", nil)
purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
guard let self else { return }
guard let self else {
RnIapLog.warn("purchaseUpdatedListener: HybridRnIap deallocated, purchase event dropped")
return
}
Task { @MainActor in
let rawPayload = OpenIapSerialization.purchase(openIapPurchase)
let payload = RnIapHelper.sanitizeDictionary(rawPayload)
Expand All @@ -917,7 +920,10 @@ class HybridRnIap: HybridRnIapSpec {
if purchaseErrorSub == nil {
RnIapLog.payload("purchaseErrorListener.register", nil)
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
guard let self else { return }
guard let self else {
RnIapLog.warn("purchaseErrorListener: HybridRnIap deallocated, error event dropped")
return
}
Task { @MainActor in
let payload = RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode(error))
RnIapLog.result("purchaseErrorListener", payload)
Expand All @@ -935,7 +941,10 @@ class HybridRnIap: HybridRnIapSpec {
if promotedProductSub == nil {
RnIapLog.payload("promotedProductListenerIOS.register", nil)
promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
guard let self else { return }
guard let self else {
RnIapLog.warn("promotedProductListenerIOS: HybridRnIap deallocated, promoted product event dropped")
return
}
Task {
RnIapLog.payload("promotedProductListenerIOS", ["productId": productId])
do {
Expand Down