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