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