1import { NativeModules } from 'react-native'; 2 3import { ProxyNativeModule, TurboNativeModuleProxy } from './NativeModulesProxy.types'; 4 5// `ExpoModulesProxy` is not declared in TypeScript yet. It's installed via JSI. 6declare namespace global { 7 const ExpoModulesProxy: TurboNativeModuleProxy; 8} 9 10const NativeProxy = NativeModules.NativeUnimoduleProxy; 11const modulesConstantsKey = 'modulesConstants'; 12const exportedMethodsKey = 'exportedMethods'; 13 14const NativeModulesProxy: { [moduleName: string]: ProxyNativeModule } = {}; 15 16// Keep it opt-in for now. It's too risky without proper and thorough testing. 17let canUseExpoTurboModules = false; 18 19/** 20 * Sets whether to use a TurboModule version of the proxy. 21 */ 22export function useExpoTurboModules(state: boolean = true) { 23 canUseExpoTurboModules = state; 24} 25 26if (NativeProxy) { 27 Object.keys(NativeProxy[exportedMethodsKey]).forEach((moduleName) => { 28 NativeModulesProxy[moduleName] = NativeProxy[modulesConstantsKey][moduleName] || {}; 29 NativeProxy[exportedMethodsKey][moduleName].forEach((methodInfo) => { 30 NativeModulesProxy[moduleName][methodInfo.name] = (...args: unknown[]): Promise<any> => { 31 const { key, argumentsCount } = methodInfo; 32 if (argumentsCount !== args.length) { 33 return Promise.reject( 34 new Error( 35 `Native method ${moduleName}.${methodInfo.name} expects ${argumentsCount} ${ 36 argumentsCount === 1 ? 'argument' : 'arguments' 37 } but received ${args.length}` 38 ) 39 ); 40 } 41 42 if (canUseExpoTurboModules && global.ExpoModulesProxy) { 43 return global.ExpoModulesProxy.callMethodAsync(moduleName, methodInfo.name, args); 44 } else { 45 return NativeProxy.callMethod(moduleName, key, args); 46 } 47 }; 48 }); 49 50 // These are called by EventEmitter (which is a wrapper for NativeEventEmitter) 51 // only on iOS and they use iOS-specific native module, EXReactNativeEventEmitter. 52 // 53 // On Android only {start,stop}Observing are called on the native module 54 // and these should be exported as Expo methods. 55 // 56 // Before the RN 65, addListener/removeListeners weren't called on Android. However, it no longer stays true. 57 // See https://github.com/facebook/react-native/commit/f5502fbda9fe271ff6e1d0da773a3a8ee206a453. 58 // That's why, we check if the `EXReactNativeEventEmitter` exists and only if yes, we use it in the listener implementation. 59 // Otherwise, those methods are NOOP. 60 if (NativeModules.EXReactNativeEventEmitter) { 61 NativeModulesProxy[moduleName].addListener = (...args) => 62 NativeModules.EXReactNativeEventEmitter.addProxiedListener(moduleName, ...args); 63 NativeModulesProxy[moduleName].removeListeners = (...args) => 64 NativeModules.EXReactNativeEventEmitter.removeProxiedListeners(moduleName, ...args); 65 } else { 66 // Fixes on Android: 67 // WARN `new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method. 68 // WARN `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method. 69 NativeModulesProxy[moduleName].addListener = () => {}; 70 NativeModulesProxy[moduleName].removeListeners = () => {}; 71 } 72 }); 73} else { 74 console.warn( 75 `The "EXNativeModulesProxy" native module is not exported through NativeModules; verify that expo-modules-core's native code is linked properly` 76 ); 77} 78 79export default NativeModulesProxy; 80