1import { NativeModules } from 'react-native'; 2 3import { ProxyNativeModule } from './NativeModulesProxy.types'; 4 5const LegacyNativeProxy = NativeModules.NativeUnimoduleProxy; 6// Fixes `cannot find name 'global'.` in tests 7// @ts-ignore 8const ExpoNativeProxy = global.expo?.modules?.NativeModulesProxy; 9 10const modulesConstantsKey = 'modulesConstants'; 11const exportedMethodsKey = 'exportedMethods'; 12 13const NativeModulesProxy: { [moduleName: string]: ProxyNativeModule } = {}; 14 15if (LegacyNativeProxy) { 16 // use JSI proxy if available, fallback to legacy RN proxy 17 const NativeProxy = ExpoNativeProxy ?? LegacyNativeProxy; 18 19 Object.keys(NativeProxy[exportedMethodsKey]).forEach((moduleName) => { 20 // copy constants 21 NativeModulesProxy[moduleName] = NativeProxy[modulesConstantsKey][moduleName] || {}; 22 23 // copy methods 24 NativeProxy[exportedMethodsKey][moduleName].forEach((methodInfo) => { 25 NativeModulesProxy[moduleName][methodInfo.name] = (...args: unknown[]): Promise<any> => { 26 // Use the new proxy to call methods on legacy modules, if possible. 27 if (ExpoNativeProxy?.callMethod) { 28 return ExpoNativeProxy.callMethod(moduleName, methodInfo.name, args); 29 } 30 31 // Otherwise fall back to the legacy proxy. 32 // This is deprecated and might be removed in SDK47 or later. 33 const { key, argumentsCount } = methodInfo; 34 if (argumentsCount !== args.length) { 35 return Promise.reject( 36 new Error( 37 `Native method ${moduleName}.${methodInfo.name} expects ${argumentsCount} ${ 38 argumentsCount === 1 ? 'argument' : 'arguments' 39 } but received ${args.length}` 40 ) 41 ); 42 } 43 return LegacyNativeProxy.callMethod(moduleName, key, args); 44 }; 45 }); 46 47 // These are called by EventEmitter (which is a wrapper for NativeEventEmitter) 48 // only on iOS and they use iOS-specific native module, EXReactNativeEventEmitter. 49 // 50 // On Android only {start,stop}Observing are called on the native module 51 // and these should be exported as Expo methods. 52 // 53 // Before the RN 65, addListener/removeListeners weren't called on Android. However, it no longer stays true. 54 // See https://github.com/facebook/react-native/commit/f5502fbda9fe271ff6e1d0da773a3a8ee206a453. 55 // That's why, we check if the `EXReactNativeEventEmitter` exists and only if yes, we use it in the listener implementation. 56 // Otherwise, those methods are NOOP. 57 if (NativeModules.EXReactNativeEventEmitter) { 58 NativeModulesProxy[moduleName].addListener = (...args) => 59 NativeModules.EXReactNativeEventEmitter.addProxiedListener(moduleName, ...args); 60 NativeModulesProxy[moduleName].removeListeners = (...args) => 61 NativeModules.EXReactNativeEventEmitter.removeProxiedListeners(moduleName, ...args); 62 } else { 63 // Fixes on Android: 64 // WARN `new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method. 65 // WARN `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method. 66 NativeModulesProxy[moduleName].addListener = () => {}; 67 NativeModulesProxy[moduleName].removeListeners = () => {}; 68 } 69 }); 70} else { 71 console.warn( 72 `The "EXNativeModulesProxy" native module is not exported through NativeModules; verify that expo-modules-core's native code is linked properly` 73 ); 74} 75 76export default NativeModulesProxy; 77