fix(listeners): use singleton native listener to prevent iOS removeAll() bug (#3164)
## Summary - Fix critical bug where iOS `removePurchaseUpdatedListener` called `removeAll()` instead of removing only the specific listener, silently wiping ALL registered listeners - Replace per-listener native registration with singleton native handler + JS-level `Set` fan-out for all 5 listener types - `remove()` now only deletes from the JS Set — other listeners remain intact regardless of iOS native behavior ## Root Cause When multiple `useIAP` hooks were active (e.g., a persistent top-level component + a screen-level component), unmounting one component called `removePurchaseUpdatedListener` on iOS which triggered `purchaseUpdatedListeners.removeAll()`. This wiped **all** listeners including the ones from the still-mounted component, causing `onPurchaseSuccess` to silently stop firing. Android was unaffected because it correctly used `.remove(listener)` instead of `.removeAll()`. ### Before (broken) ``` Component A: useIAP → registers listener A Component B: useIAP → registers listener B Component B unmounts → iOS removeAll() → listener A also gone ❌ User purchases → no listener to receive → purchase lost ``` ### After (fixed) ``` Component A: useIAP → adds cbA to JS Set Component B: useIAP → adds cbB to JS Set (native singleton already attached, no duplicate registration) Component B unmounts → Set.delete(cbB) → cbA still in Set ✅ User purchases → native singleton → fans out to cbA ✅ ``` ## Changes ### `src/index.ts` - Replace `WeakMap` per-listener tracking with module-level `Set` per event type - Register a single native handler that fans out to all JS listeners in the Set - `remove()` only deletes from JS Set (never calls native remove) - Add `resetListenerState()` called in `endConnection()` for clean re-registration - Applied to all 5 listener types: `purchaseUpdated`, `purchaseError`, `promotedProduct`, `userChoiceBilling`, `developerProvidedBilling` ### `src/__tests__/index.test.ts` - Update tests to verify singleton native registration (1 call, not N) - Verify JS-level removal: listener stops receiving after `remove()` - Verify independent removal: removing one listener doesn't affect others ## Test plan - [x] `yarn typecheck` passes - [x] `yarn lint` passes - [x] `yarn jest` — 251 tests pass Closes #3150 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a public way to fully clear and reset event-listener state to ensure clean reconnections. * **Refactor** * Centralized event dispatch so a single native callback fans out to multiple JS listeners for consistent broadcasting and error handling. * **Tests** * Updated tests to validate single-handler broadcasting, JS-level removal behavior, reconnection/re-registration, and normalized error forwarding. * **Documentation** * Clarified PR review workflow to require explicit POST when replying to individual review comments. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
H
Hyo committed
4d9f74347429043c76a7f1f331eac03ef9d2efab
Parent: e2acf36
Committed by GitHub <noreply@github.com>
on 3/10/2026, 6:55:00 PM