import type { Bridge, BridgeStore, KeyOfOrString, Parser, ParserSchema, Primitive, } from "@webview-bridge/types"; import { createEvents, createRandomId } from "@webview-bridge/utils"; import type React from "react"; import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, } from "react"; import type { WebViewMessageEvent, WebViewProps } from "react-native-webview"; import WebView from "react-native-webview"; import { INJECT_BRIDGE_METHODS, INJECT_BRIDGE_STATE, SAFE_NATIVE_EMITTER_EMIT, handleBridge, } from "./integrations/bridge"; import { INJECT_DEBUG, type LogType, handleLog } from "./integrations/console"; import { handleRegisterWebMethod } from "./integrations/handleRegisterWebMethod"; import type { BridgeWebView } from "./types/webview"; export type CreateWebViewArgs< BridgeObject extends Bridge, PostMessageSchema extends ParserSchema, > = { /** * The bridge object to be used in the WebView. * @example import { createWebView, bridge } from "@webview-bridge/react-native"; import InAppBrowser from "react-native-inappbrowser-reborn"; // Register functions in the bridge object in your React Native code export const appBridge = bridge({ async getMessage() { return "Hello, I'm native"; }, async sum(a: number, b: number) { return a + b; }, async openInAppBrowser(url: string) { if (await InAppBrowser.isAvailable()) { await InAppBrowser.open(url); } }, // ... Add more functions as needed }); export const { WebView } = createWebView({ bridge: appBridge, debug: true, // Enable console.log visibility in the native WebView }); */ bridge: BridgeStore; /** * If `true`, the console.log visibility in the WebView is enabled. * @default false */ debug?: boolean; /** * Set the timeout in milliseconds for the response from the web method. * @default 2000 */ responseTimeout?: number; /** * The schema for the postMessage method. * @link https://gronxb.github.io/webview-bridge/using-a-post-message.html */ postMessageSchema?: PostMessageSchema; /** * Callback function when a method that is not defined in the bridge is called. * @link https://gronxb.github.io/webview-bridge/backward-compatibility/new-method.html#react-native-part */ fallback?: (method: keyof BridgeObject) => void; }; export type WebMethod = T & { isReady: boolean; }; /** * Create a WebView component that can communicate with the bridge. * @link https://gronxb.github.io/webview-bridge/getting-started.html * @example import { createWebView, bridge } from "@webview-bridge/react-native"; import InAppBrowser from "react-native-inappbrowser-reborn"; // Register functions in the bridge object in your React Native code export const appBridge = bridge({ async getMessage() { return "Hello, I'm native"; }, async sum(a: number, b: number) { return a + b; }, async openInAppBrowser(url: string) { if (await InAppBrowser.isAvailable()) { await InAppBrowser.open(url); } }, // ... Add more functions as needed }); export const { WebView } = createWebView({ bridge: appBridge, debug: true, // Enable console.log visibility in the native WebView }); */ export const createWebView = < BridgeObject extends Bridge, PostMessageSchema extends ParserSchema, >({ bridge, debug, responseTimeout = 2000, postMessageSchema, fallback, }: CreateWebViewArgs) => { const WebMethod = { current: { isReady: false, }, }; const webviewRefList: React.RefObject[] = []; const emitter = createEvents(); const uniqueId = createRandomId(); bridge.subscribe((state) => { for (const ref of webviewRefList) { ref?.current?.injectJavaScript( SAFE_NATIVE_EMITTER_EMIT("bridgeStateChange", state), ); } }); return { /** * Sends an event from React Native to the Web. * @link https://gronxb.github.io/webview-bridge/using-a-post-message.html */ postMessage: < EventName extends KeyOfOrString, Args extends Parser, >( eventName: EventName, args: Args, options: { /** * If `true`, the message will be broadcasted to all webviews. * @default false */ broadcast: boolean; } = { broadcast: false, }, ) => { let _args: any = args; if (postMessageSchema) { _args = postMessageSchema[eventName].validate(args); } if (options.broadcast) { for (const ref of webviewRefList) { ref?.current?.injectJavaScript( SAFE_NATIVE_EMITTER_EMIT(`postMessage/${String(eventName)}`, _args), ); } return; } const lastRef = webviewRefList[webviewRefList.length - 1]; lastRef?.current?.injectJavaScript( SAFE_NATIVE_EMITTER_EMIT(`postMessage/${String(eventName)}`, _args), ); }, WebView: forwardRef((props, ref) => { const webviewRef = useRef(null); useLayoutEffect(() => { webviewRefList.push(webviewRef); return () => { webviewRefList.pop(); }; }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: const initData = useMemo(() => { const bridgeMethods = Object.entries(bridge.getState() ?? {}) .filter(([_, bridge]) => typeof bridge === "function") .map(([name]) => name); const initialState = Object.fromEntries( Object.entries(bridge.getState() ?? {}).filter( ([_, value]) => typeof value !== "function", ), ) as Record; return { bridgeMethods, initialState }; }, []); useEffect(() => { // lazy initialize the bridgeId webviewRef.current?.injectJavaScript( SAFE_NATIVE_EMITTER_EMIT("hydrate", initData), ); }, [initData]); useImperativeHandle(ref, () => webviewRef.current as BridgeWebView, []); const handleMessage = async (event: WebViewMessageEvent) => { props.onMessage?.(event); if (!webviewRef.current) { return; } const { type, body, bridgeId } = JSON.parse(event.nativeEvent.data); switch (type) { case "log": { const { method, args } = body as { method: LogType; args: string; }; debug && handleLog(method, args); return; } case "bridge": { const { method, args, eventId } = body as { method: string; args: unknown[]; eventId: string; }; handleBridge({ bridgeId, bridge, method, args, eventId, webview: webviewRef.current, }); return; } case "getBridgeState": { for (const ref of webviewRefList) { ref?.current?.injectJavaScript( SAFE_NATIVE_EMITTER_EMIT( "bridgeStateChange", bridge.getState(), ), ); } return; } case "registerWebMethod": { const { bridgeNames } = body as { bridgeNames: string[]; }; Object.assign( WebMethod.current, handleRegisterWebMethod( emitter, webviewRef.current, bridgeNames, responseTimeout, ), ); WebMethod.current.isReady = true; return; } case "webMethodResponse": { const { eventId, funcName, value } = body as { eventId: string; funcName: string; value: unknown; }; emitter.emit(`${funcName}-${eventId}`, value); return; } case "webMethodError": { const { eventId, funcName } = body as { eventId: string; funcName: string; value: unknown; }; emitter.emit(`${funcName}-${eventId}`, {}, true); return; } case "fallback": { const { method } = body as { method: keyof BridgeObject; }; fallback?.(method); return; } } }; return ( ); }), /** * @deprecated Use `postMessage` instead. And complete the type through the `postMessageSchema` option. * @see https://gronxb.github.io/webview-bridge/using-a-post-message.html * @example import { createWebView, postMessageSchema } from "@webview-bridge/react-native"; import { z } from "zod"; const appPostMessageSchema = postMessageSchema({ eventName1: z.object({ message: z.string(), }), eventName2: z.string(), }); // Export the event schema to be used in the web application export type AppPostMessageSchema = typeof appPostMessageSchema; // When you bridge a webview, a postMessage is extracted. export const { postMessage } = createWebView({ postMessageSchema: appPostMessageSchema, // Pass in the your schema. This is optional, so if the type doesn't matter to you, you don't need to include it. // .. }); // usage postMessage("eventName1", { message: "test", }); postMessage("eventName2", "test"); */ linkWebMethod() { return WebMethod as { current: WebMethod; }; }, }; };