// Copyright © 2024 650 Industries. /// 'use client'; import { type Component, type ComponentType, createRef, PureComponent } from 'react'; import { type ReactNativeElement, findNodeHandle, type HostComponent } from 'react-native'; import { get as componentRegistryGet } from 'react-native/Libraries/NativeComponent/NativeComponentRegistry'; import { SharedObject } from './SharedObject'; import { requireNativeModule } from './requireNativeModule'; // To make the transition from React Native's `requireNativeComponent` to Expo's // `requireNativeViewManager` as easy as possible, `requireNativeViewManager` is a drop-in // replacement for `requireNativeComponent`. // // For each view manager, we create a wrapper component that accepts all the props available to // the author of the universal module. This wrapper component splits the props into two sets: props // passed to React Native's View (ex: style, testID) and custom view props, which are passed to the // adapter view component in a prop called `proxiedProperties`. /** * A map that caches registered native components. */ const nativeComponentsCache = new Map>(); // TODO(@kitten): Optimally, this is defined on ExpoGlobal, but we treat `__expo_app_identifier__` as internal declare namespace globalThis { const expo: | undefined | { __expo_app_identifier__?: string; getViewConfig( moduleName: string, viewName?: string ): { validAttributes: Record; directEventTypes: Record; } | null; }; } /** * Requires a React Native component using the static view config from an Expo module. */ function requireNativeComponent( moduleName: string, viewName?: string ): HostComponent { const appIdentifier = globalThis.expo?.['__expo_app_identifier__'] ?? ''; const viewNameSuffix = appIdentifier ? `_${appIdentifier}` : ''; const nativeViewName = viewName ? `ViewManagerAdapter_${moduleName}_${viewName}${viewNameSuffix}` : `ViewManagerAdapter_${moduleName}${viewNameSuffix}`; return componentRegistryGet(nativeViewName, () => { const expoViewConfig = globalThis.expo?.getViewConfig(moduleName, viewName); if (!expoViewConfig) { console.warn( 'Unable to get the view config for %s from module &s', viewName ?? 'default view', moduleName ); return { uiViewClassName: nativeViewName }; } return { uiViewClassName: nativeViewName, directEventTypes: expoViewConfig.directEventTypes, validAttributes: addAttributeProcessing(expoViewConfig.validAttributes), }; }); } /** * Unwraps a shared object to its id so that native receives the registry id * instead of the JS wrapper. Other values are passed through unchanged. */ function processPropValue(value: unknown): unknown { if (value != null && typeof value === 'object' && value instanceof SharedObject) { // `__expo_shared_object_id__` is a hidden property installed by native and planned for // removal; the `typeof` check guards against returning `undefined` once native stops // setting the property. // @ts-expect-error const sharedObjectId = value.__expo_shared_object_id__; if (typeof sharedObjectId === 'number') { return sharedObjectId; } } return value; } /** * Wraps each attribute in the descriptor shape React Native expects and attaches `process` * to unwrap shared objects to their registry id. `diff` is intentionally left unset so * React Native falls back to its `deepDiffer` default, which does structural comparison * for object/array props. */ function addAttributeProcessing(validAttributes: Record): Record { const descriptor = { process: processPropValue }; const attributes: Record = {}; for (const key of Object.keys(validAttributes)) { attributes[key] = descriptor; } return attributes; } /** * Requires a React Native component from cache if possible. This prevents * "Tried to register two views with the same name" errors on fast refresh, but * also when there are multiple versions of the same package with native component. */ function requireCachedNativeComponent( moduleName: string, viewName?: string ): HostComponent { const cacheKey = `${moduleName}_${viewName}`; const cachedNativeComponent = nativeComponentsCache.get(cacheKey); if (!cachedNativeComponent) { const nativeComponent = requireNativeComponent(moduleName, viewName); nativeComponentsCache.set(cacheKey, nativeComponent); return nativeComponent; } return cachedNativeComponent; } /** * A drop-in replacement for `requireNativeComponent`. */ export function requireNativeViewManager

( moduleName: string, viewName?: string ): ComponentType

{ const ReactNativeComponent = requireCachedNativeComponent(moduleName, viewName); class NativeComponent extends PureComponent

{ static displayName = viewName ? viewName : moduleName; nativeRef = createRef(); // This will be accessed from native when the prototype functions are called, // in order to find the associated native view. nativeTag: number | null = null; componentDidMount(): void { this.nativeTag = findNodeHandle(this.nativeRef.current); } render() { return ; } } try { const nativeModule = requireNativeModule(moduleName); const nativeViewPrototype = nativeModule.ViewPrototypes[viewName ? `${moduleName}_${viewName}` : moduleName]; if (nativeViewPrototype) { // Assign native view functions to the component prototype, so they can be accessed from the ref. Object.assign(NativeComponent.prototype, nativeViewPrototype); } } catch { // `requireNativeModule` may throw an error when the native module cannot be found. // In some tests we don't mock the entire modules, but we do want to mock native views. For now, // until we still have to support the legacy modules proxy and don't have better ways to mock, // let's just gracefully skip assigning the prototype functions. // See: https://github.com/expo/expo/blob/main/packages/expo-modules-core/src/__tests__/NativeViewManagerAdapter-test.native.tsx } return NativeComponent; }