Fix NativeEventEmitter crash on React Native 0.79+#1595
Conversation
….79+ Fixes #1298 React Native 0.79 introduced breaking changes to NativeEventEmitter that made the constructor stricter. When the native module doesn't have proper event emitter methods, construction throws an error causing the app to crash with "undefined is not a function in eventEmitter.addListener". This change wraps the NativeEventEmitter construction in a try-catch block to gracefully handle failures. When construction fails: - A clear warning is logged explaining the impact - eventEmitter is set to null - Core SDK functionality (purchases, customer info) continues to work - Only event listeners (customer info updates, promo purchases) are affected - Existing optional chaining (eventEmitter?.addListener) handles null safely All 189 existing tests pass with this change.
React Native 0.79+ requires native modules to implement addListener and removeListeners methods for NativeEventEmitter to work properly. Android already had these methods, but iOS was missing them. This completes the fix for #1298 by addressing both sides: - JavaScript: Try-catch wrapper for graceful degradation (previous commit) - Native iOS: Required event emitter methods (this commit) - Native Android: Already implemented ✓
…nted The try-catch was defensive programming, but now that we've added the required addListener/removeListeners methods to both iOS and Android native modules, it's no longer needed. Event listeners are critical functionality (customer info updates, promo purchases, logging, analytics). Better to fail fast if there's a real problem than silently degrade. Added documentation comments explaining the RN 0.79+ requirements and linking to the GitHub issue and React Native breaking changes.
Changes: - Call [super] to preserve RCTEventEmitter's lifecycle management - Change NSInteger to double for removeListeners parameter (matches RN base class) - Update comments to note this has been required since RN 0.65, not 0.79 - Add link to React Native source code for reference This preserves the base class's listener counting and startObserving/ stopObserving lifecycle callbacks while maintaining full RN 0.79+ compatibility. Sources: - https://github.com/facebook/react-native/blob/main/packages/react-native/React/Modules/RCTEventEmitter.m - react-native-device-info/react-native-device-info@3917f33
Added inline documentation explaining what [super addListener] and [super removeListeners] actually do: - Validates eventName against supportedEvents (debug only) - Tracks _listenerCount - Calls startObserving/stopObserving lifecycle methods Includes direct GitHub blob URL with line numbers to React Native's RCTEventEmitter.m implementation (lines 101-125). This helps future developers understand exactly what's happening when these methods are called.
Changed focus from describing what [super addListener] does to explaining WHY we call it: - We must export these methods for RN 0.79+ compatibility - We call [super] to preserve RCTEventEmitter's listener counting and lifecycle - Without [super], we'd lose startObserving/stopObserving functionality This makes it clearer for future developers why we can't just have empty stubs.
The key insight: RCT_EXPORT_METHOD declarations are NOT inherited in React Native. Even though RCTEventEmitter (our parent) has addListener and removeListeners implemented, the React Native bridge only sees methods that are explicitly exported in RNPurchases itself. We must re-export them here to make them visible to JavaScript, then call [super] to delegate to the parent's implementation. This explains the 'why' - not implementation details.
Fixed confusion in documentation: - NativeEventEmitter = JavaScript class that wraps native modules - RCTEventEmitter = Objective-C parent class of RNPurchases The JavaScript NativeEventEmitter constructor checks if the native module has addListener/removeListeners methods. We inherit from RCTEventEmitter (native), but must re-export its methods because RCT_EXPORT_METHOD doesn't inherit automatically.
rickvdl
left a comment
There was a problem hiding this comment.
The changes make sense imo, thanks for fixing! I do think the PR description is outdated though 😅
tonidero
left a comment
There was a problem hiding this comment.
Just a question. Can't say I'm fully familiar with the issue, but the explanation makes sense 👍
| // See: https://github.com/RevenueCat/react-native-purchases/issues/1298 | ||
| // See: https://github.com/facebook/react-native/blob/main/packages/react-native/React/Modules/RCTEventEmitter.m#L101-L125 | ||
| RCT_EXPORT_METHOD(addListener:(NSString *)eventName) { | ||
| [super addListener:eventName]; |
There was a problem hiding this comment.
Do we need to implement similar methods for the react-native-purchases-ui SDK?
There was a problem hiding this comment.
Pull request overview
Fixes the React Native 0.79+ NativeEventEmitter initialization crash by ensuring the iOS native module explicitly exports the required addListener/removeListeners methods, and by documenting the RN 0.79 requirement in the JS entrypoint.
Changes:
- Exported
addListenerandremoveListenersfrom the iOSRNPurchasesmodule (forwarding toRCTEventEmitter). - Added inline documentation in
src/purchases.tsexplaining the RN 0.79+NativeEventEmitterrequirement and linking the related issue.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/purchases.ts |
Adds documentation about RN 0.79+ NativeEventEmitter requirements near emitter initialization. |
ios/RNPurchases.m |
Exports addListener/removeListeners so new NativeEventEmitter(RNPurchases) is valid on RN 0.79+. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Only create event emitter if native module is available to avoid crash on import | ||
| // | ||
| // React Native 0.79+ requires native modules to implement addListener() and removeListeners() | ||
| // methods for NativeEventEmitter to work. Both iOS and Android native modules now have these. | ||
| // See: https://github.com/RevenueCat/react-native-purchases/issues/1298 | ||
| // See: https://reactnative.dev/blog/2025/04/08/react-native-0.79 (Breaking Changes section) | ||
| const eventEmitter = !usingBrowserMode && RNPurchases ? new NativeEventEmitter(RNPurchases) : null; |
| // Required for RN 0.65+ NativeEventEmitter (JavaScript class) support | ||
| // | ||
| // In JavaScript: new NativeEventEmitter(RNPurchases) | ||
| // NativeEventEmitter checks if the native module has addListener/removeListeners methods. | ||
| // Without these exported methods, construction throws in RN 0.79+. | ||
| // |
Description
Fixes #1298
This PR resolves a crash that occurs when initializing the React Native Purchases SDK on React Native 0.79.4+. The crash happens due to breaking changes in
NativeEventEmitterthat made the constructor stricter.Problem
React Native 0.79 introduced breaking changes to
NativeEventEmitter. When the native module doesn't have proper event emitter methods, the constructor throws an error causing:This affects 100% of devices running on React Native 0.79.4+ and has been reported by multiple users in production.
Solution
Wrapped the
NativeEventEmitterconstruction in a try-catch block to gracefully handle failures:eventEmitteris set tonulleventEmitter?.addListener) safely handles nullTesting
Checklist
Related Issues