14.7.13 - Thread Safety & Error Handling
This release fixes two critical reliability issues: thread safety for purchase event listeners across both platforms, and unhandled JNI exceptions during Android initialization.
Thread Safety for Listener Operations (#3152, #3157)
Problem
When purchase events fired concurrently (e.g., during high-volume purchases or rapid listener add/remove), listener arrays could be modified while being iterated — causing ConcurrentModificationException on Android and potential crashes on iOS. This resulted in purchase events being silently lost.
Fix
Both platforms now use a synchronized + snapshot pattern: listener arrays are protected by a lock, and a snapshot copy is taken before iteration. This ensures safe concurrent access without blocking event delivery.
iOS
- Added
NSLockto protect all listener arrays (purchaseUpdatedListeners,purchaseErrorListeners,promotedProductListeners) sendPurchaseUpdateandsendPurchaseErroriterate over snapshot copies- Error dedup state and delivery tracking collections are also protected
cleanupExistingStateclears all state atomically within the lock
Android
- Applied snapshot pattern to
userChoiceBillinganddeveloperProvidedBillinglisteners for consistency - Added
synchronizedfor clearing these listener arrays inendConnection
Before & After
// Before: Concurrent modification could crash or silently drop events
listeners.forEach { it(event) } // ❌ Another thread modifies the list
// After: Snapshot ensures safe iteration
val snapshot = lock.withLock { ArrayList(listeners) }
snapshot.forEach { it(event) } // ✅ Isolated from modifications
Android JNI Exception Handling (#3158)
Problem
Some users reported a cryptic error during initConnection:
Unknown N8facebook3jni12JniExceptionE error.
This occurred because the openIap object is lazy-initialized, and its first access happens during setActivity() or listener registration — both of which were outside any try-catch block. On devices without Google Play Services or with billing client issues, the raw JNI exception propagated unhandled, producing the mangled C++ class name as the error message.
Fix
- Wrapped
setActivityand listener registration in try-catch blocks - Converts exceptions to structured
OpenIapExceptionwith error codeinit-connectionand descriptive messages - Added
CancellationExceptionpassthrough to preserve coroutine cancellation semantics - On listener registration failure,
initDeferredis completed exceptionally to prevent concurrent callers from deadlocking
Before & After
// Before: Cryptic error with no actionable information
catch (error) {
// error.message = "Unknown N8facebook3jni12JniExceptionE error."
// error.code = undefined ❌
}
// After: Structured error with clear context
catch (error) {
// error.code = "init-connection" ✅
// error.message = "Failed to register billing listeners: <details>" ✅
}
Installation
yarn add react-native-iap@14.7.13
