SIGN IN SIGN UP

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