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