diff --git a/package.json b/package.json index 3e1bcf4..888f9bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "embed-react-native-sdk", - "version": "1.1.0", + "version": "3.0.10", "description": "React Native SDK for Embedding TS", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", @@ -26,11 +26,6 @@ "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-typescript": "^12.1.2", - "@types/react": "^18.3.18", - "@types/react-native": "^0.72.8", - "react": "^18.3.1", - "react-native": "^0.77.0", - "react-native-webview": "^13.13.2", "rollup": "^4.32.0", "rollup-plugin-dts": "^6.1.1", "typescript": "^5.7.3" diff --git a/src/BaseEmbed.tsx b/src/BaseEmbed.tsx deleted file mode 100644 index bc946e7..0000000 --- a/src/BaseEmbed.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { - useRef, - useEffect, - useImperativeHandle, - forwardRef, - useMemo, - useState, -} from "react"; -import { WebView, WebViewMessageEvent } from "react-native-webview"; -import { EmbedBridge, EmbedMessage } from "./event-bridge"; -import { embedConfigCache } from "./init"; - -interface BaseEmbedProps { - typeofEmbed: string; - [key: string]: any; -} - -export interface TSEmbedRef { - trigger: (hostEventName: string, payload?: any) => Promise; -} - -export const BaseEmbed = forwardRef( - (props, ref) => { - const webViewRef = useRef(null); - const embedBridge = useMemo(() => new EmbedBridge(webViewRef), []); - const [vercelShellLoaded, setVercelShellLoaded] = useState(false); - const [viewConfig, setViewConfig] = useState>({}); - - useEffect(() => { - const newViewConfig: Record = {}; - Object.keys(props).forEach((key) => { - if (key.startsWith("on")) { - const eventName = key.substring(2); - embedBridge.registerEmbedEvent(eventName, props[key]); - } else if (key !== 'embedType') { - newViewConfig[key] = props[key]; - } - }); - setViewConfig(newViewConfig); - }, [props, embedBridge]); - - useEffect(() => { - if (!webViewRef.current || !vercelShellLoaded) { - console.log("[BaseEmbed] Waiting for Vercel shell to load..."); - return; - } - - const initMsg = { - type: "INIT", - payload: embedConfigCache, - }; - embedBridge.sendMessage(initMsg); - - const message = { - type: "EMBED", - embedType: props.embedType, - viewConfig: viewConfig, - }; - embedBridge.sendMessage(message); - }, [viewConfig, embedBridge, props.embedType, vercelShellLoaded]); - - useImperativeHandle(ref, () => ({ - trigger: (hostEventName: string, payload?: any) => { - return embedBridge.trigger(hostEventName, payload); - }, - })); - - const handleMessage = (event: WebViewMessageEvent) => { - try { - const msg = JSON.parse(event.nativeEvent.data); - if (msg.type === "INIT_VERCEL_SHELL") { - setVercelShellLoaded(true); - } - embedBridge.handleMessage(msg); - } catch (err) { - console.error("Unable to parse the message from the webview", err); - } - }; - - return ( - { - const { nativeEvent } = syntheticEvent; - console.warn("error in the webview", nativeEvent); - }} - keyboardDisplayRequiresUserAction={false} // Add this for iOS - automaticallyAdjustContentInsets={false} // Add this - scrollEnabled={false} - onHttpError= {(syntheticEvent) => { - const { nativeEvent } = syntheticEvent; - console.warn("HTTP error in the webview", nativeEvent); - }} - style={{ flex: 1, - height: '100%', // Explicit height - width: '100%' - }} - /> - ); - } -); diff --git a/src/LiveboardEmbed.tsx b/src/LiveboardEmbed.tsx deleted file mode 100644 index af99729..0000000 --- a/src/LiveboardEmbed.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { forwardRef } from "react"; -import { BaseEmbed, TSEmbedRef } from "./BaseEmbed"; -import { LiveboardViewConfig, EmbedEvent } from "./types"; - -type EventHandlers = { - [K in EmbedEvent as `on${Capitalize}`]?: (event: any) => void; -} - -export type LiveboardEmbedRef = TSEmbedRef; - -export const LiveboardEmbed = forwardRef( - (props, ref) => { - return ( - - ); - } -); diff --git a/src/LiveboardEmbedClass.ts b/src/LiveboardEmbedClass.ts new file mode 100644 index 0000000..7e108a8 --- /dev/null +++ b/src/LiveboardEmbedClass.ts @@ -0,0 +1,16 @@ +import { TSEmbed } from './tsEmbed'; +import { componentFactory } from './componentFactory'; +import { LiveboardViewConfig } from './types'; +import WebView from 'react-native-webview'; +import { EmbedProps } from './util'; +import React from 'react'; + +class LiveboardEmbedClass extends TSEmbed { + constructor(webViewRef: React.RefObject, config?: T) { + super(webViewRef, config); + } +} + +export interface LiveboardEmbedProps extends LiveboardViewConfig, EmbedProps {} + +export const LiveboardEmbed = componentFactory(LiveboardEmbedClass) || null; \ No newline at end of file diff --git a/src/componentFactory.tsx b/src/componentFactory.tsx new file mode 100644 index 0000000..bfc9e9a --- /dev/null +++ b/src/componentFactory.tsx @@ -0,0 +1,77 @@ +import React, { forwardRef } from 'react'; +import { WebView } from 'react-native-webview'; +import { TSEmbed } from './tsEmbed'; +import { + EmbedEvent, + ViewConfig, + MessageCallback, +} from './types'; +import { EmbedProps } from './util'; + +export type EmbedEventHandlers = { [key in keyof typeof EmbedEvent as `on${Capitalize}`]?: MessageCallback }; + + +export interface ViewConfigAndListeners { + viewConfig: T; + listeners: { [key in EmbedEvent]?: MessageCallback }; +} + +const getViewPropsAndListeners = ( + props: T +): ViewConfigAndListeners => { + return Object.keys(props).reduce( + (accu, key) => { + if (key.startsWith('on')) { + const eventName = key.slice(2) as keyof typeof EmbedEvent; + (accu.listeners as Record)[EmbedEvent[eventName]] = props[key as keyof T] as MessageCallback; + } else { + (accu.viewConfig as Record)[key] = props[key as keyof T]; + } + return accu as ViewConfigAndListeners; + }, + { + viewConfig: {} as U, + listeners: {}, + }, + ); +}; + +export const componentFactory = ( + EmbedConstructor: T, +) => React.forwardRef, U>((props, forwardedRef): JSX.Element | null => { + const embedInstance = React.useRef | null>(null); + const webViewRef = React.useRef(null); + + if(!embedInstance.current) { + embedInstance.current = new EmbedConstructor(webViewRef) as InstanceType; + } + + const renderedWebView = React.useMemo((): JSX.Element | null => { + return embedInstance.current?.render() as React.JSX.Element ?? null; + }, [props]); + + React.useEffect(() => { + return () => { + embedInstance.current?.destroy(); + embedInstance.current = null; + } + }, []) + + React.useEffect(() => { + const { viewConfig, listeners } = getViewPropsAndListeners(props as U); + if(forwardedRef && typeof forwardedRef == 'object') { + (forwardedRef as React.MutableRefObject | null>).current = embedInstance?.current; + } + embedInstance?.current?.updateConfig(viewConfig); + + Object.entries(listeners).forEach(([eventName, callback]) => { + embedInstance.current?.on(eventName as EmbedEvent, callback as MessageCallback); + }); + }, [props]); + + if(!embedInstance.current) { + return null; + } + + return renderedWebView ?? null; +}); \ No newline at end of file diff --git a/src/event-bridge.ts b/src/event-bridge.ts index cb37c70..3c9e17d 100644 --- a/src/event-bridge.ts +++ b/src/event-bridge.ts @@ -85,4 +85,10 @@ export class EmbedBridge { private generateEventId(): string { return `evt_${Date.now()}_${Math.floor(Math.random() * 100000)}`; } + + public destroy() { + this.events = {}; + this.pendingReplies = {}; + this.webViewRef = { current: null }; + } } diff --git a/src/hooks/useLiveboardRef.ts b/src/hooks/useLiveboardRef.ts deleted file mode 100644 index 5bdaaeb..0000000 --- a/src/hooks/useLiveboardRef.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useRef } from "react"; -import { TSEmbedRef } from "src/BaseEmbed"; - -export const useLiveboardRef = () => { - const liveboardRef = useRef(null); - return liveboardRef; -}; diff --git a/src/index.ts b/src/index.ts index 71f707b..be79ee6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,5 @@ -import { useLiveboardRef } from "./hooks/useLiveboardRef"; import { init } from "./init"; -import { LiveboardEmbed, LiveboardEmbedRef } from "./LiveboardEmbed"; +import { LiveboardEmbed } from "./LiveboardEmbedClass"; +import { AuthType, ViewConfig } from "./types"; - -export { init, LiveboardEmbed, useLiveboardRef }; -export type { LiveboardEmbedRef }; - -const EmbedReactNativeSDK = { - init, - LiveboardEmbed, - useLiveboardRef, -}; -export default EmbedReactNativeSDK; \ No newline at end of file +export { init, LiveboardEmbed, AuthType, ViewConfig }; diff --git a/src/tsEmbed.tsx b/src/tsEmbed.tsx new file mode 100644 index 0000000..ad776d8 --- /dev/null +++ b/src/tsEmbed.tsx @@ -0,0 +1,125 @@ +import { WebView, WebViewMessageEvent } from "react-native-webview"; +import { EmbedBridge } from "./event-bridge"; +import React from "react"; +import { ViewConfig } from "./types"; +import { embedConfigCache } from "./init"; + + +export class TSEmbed { + protected webViewRef: React.RefObject; + protected embedBridge: EmbedBridge | null = null; + protected viewConfig: T; + protected vercelShellLoaded: boolean = false; + private pendingHandlers: Array<[string, Function]> = []; + + constructor(webViewRef: React.RefObject, config?: T) { + this.webViewRef = webViewRef; + this.viewConfig = config || {} as T; + this.handleMessage = this.handleMessage.bind(this); + } + + protected getEmbedType() { + return this.constructor.name.replace('EmbedClass', ''); + } + + public updateConfig(config: Partial) { + this.viewConfig = { ...this.viewConfig, ...config }; + if(this.vercelShellLoaded) { + this.sendConfigToShell(); + } + } + + public sendConfigToShell() { + if(!this.webViewRef.current || !this.vercelShellLoaded) { + console.log("[TSEmbed] Waiting for Vercel shell to load..."); + return; + } + + const initMsg = { + type: "INIT", + payload: embedConfigCache, + }; + + this.embedBridge?.sendMessage(initMsg); + + const message = { + type: "EMBED", + embedType: this.getEmbedType(), + viewConfig: this.viewConfig, + }; + + this.embedBridge?.sendMessage(message); + + } + + public on(eventName: string, callback: Function) { + if (this.embedBridge) { + this.embedBridge.registerEmbedEvent(eventName, callback); + } else { + console.log("[TSEmbed] Queuing event handler:", eventName); + this.pendingHandlers.push([eventName, callback]); + } + } + + public trigger(hostEventName: string, payload?: any) { + return this.embedBridge?.trigger(hostEventName, payload); + } + + public handleMessage(event: WebViewMessageEvent) { + try { + const msg = JSON.parse(event.nativeEvent.data); + if (msg.type === "INIT_VERCEL_SHELL") { + this.vercelShellLoaded = true; + this.embedBridge = new EmbedBridge(this.webViewRef); + + // Process pending handlers + this.pendingHandlers.forEach(([eventName, callback]) => { + this.embedBridge?.registerEmbedEvent(eventName, callback); + }); + this.pendingHandlers = []; + + this.sendConfigToShell(); + } + this.embedBridge?.handleMessage(msg); + } catch (err) { + console.error("[TsEmbed] handleMessage parse error:", err); + } + } + + public destroy() { + this.embedBridge?.destroy(); + + } + + public render(): JSX.Element { + return ( + { + const { nativeEvent } = syntheticEvent; + console.warn("error in the webview", nativeEvent); + }} + keyboardDisplayRequiresUserAction={false} + automaticallyAdjustContentInsets={false} + scrollEnabled={false} + onHttpError= {(syntheticEvent) => { + const { nativeEvent } = syntheticEvent; + console.warn("HTTP error in the webview", nativeEvent); + }} + style={{ flex: 1, + height: '100%', + width: '100%' + }} + /> + ); + } + +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..a427806 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,6 @@ +import { EmbedEventHandlers } from "./componentFactory"; +import { ViewConfig } from "./types"; + +export interface EmbedProps extends ViewConfig, EmbedEventHandlers { + +} \ No newline at end of file