diff --git a/package.json b/package.json index d81eed0..0a83884 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "embed-react-native-sdk", - "version": "4.0.1", + "version": "4.0.3", "description": "React Native SDK for Embedding TS", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", @@ -13,8 +13,7 @@ }, "peerDependencies": { "react": ">=16.8.0", - "react-native": ">=0.60.0", - "react-native-webview": ">=11.0.0" + "react-native": ">=0.60.0" }, "peerDependenciesMeta": { "@types/react": { @@ -32,6 +31,9 @@ "rollup-plugin-dts": "^6.1.1", "typescript": "^5.7.3" }, + "optionalDependencies": { + "react-native-webview": "*" + }, "scripts": { "build": "rollup -c" } diff --git a/src/LiveboardEmbedClass.ts b/src/LiveboardEmbedClass.ts index 945d504..3d3d56a 100644 --- a/src/LiveboardEmbedClass.ts +++ b/src/LiveboardEmbedClass.ts @@ -2,12 +2,12 @@ import { TSEmbed } from './tsEmbed'; import { componentFactory } from './componentFactory'; import { LiveboardViewConfig } from './types'; import WebView from 'react-native-webview'; -import { EmbedProps } from './util'; +import { EmbedProps, ErrorCallback } from './util'; import React from 'react'; class LiveboardEmbedClass extends TSEmbed { - constructor(webViewRef: React.RefObject, config?: T) { - super(webViewRef, config); + constructor(webViewRef: React.RefObject, onErrorSDK?: ErrorCallback, config?: T) { + super(webViewRef, onErrorSDK, config); } } diff --git a/src/componentFactory.tsx b/src/componentFactory.tsx index c8ebd9a..dff116a 100644 --- a/src/componentFactory.tsx +++ b/src/componentFactory.tsx @@ -7,7 +7,8 @@ import { ViewConfig, MessageCallback, } from './types'; -import { EmbedProps } from './util'; +import { EmbedProps, ERROR_MESSAGE, notifyErrorSDK } from './util'; +import { Text } from 'react-native'; export type EmbedEventHandlers = { [key in keyof typeof EmbedEvent as `on${Capitalize}`]?: MessageCallback }; @@ -40,40 +41,56 @@ const getViewPropsAndListeners = ( export const componentFactory = ( EmbedConstructor: T, ) => React.forwardRef, U>((props, forwardedRef): JSX.Element | null => { + const { onErrorSDK, ...restProps } = props; + const originalEmbedProps = restProps as unknown as U; + const embedInstance = React.useRef | null>(null); const webViewRef = React.useRef(null); + try{ - if(!embedInstance.current) { - embedInstance.current = new EmbedConstructor(webViewRef) as InstanceType; - } + if(!embedInstance.current) { + embedInstance.current = new EmbedConstructor(webViewRef, onErrorSDK) as InstanceType; + } - const renderedWebView = React.useMemo((): JSX.Element | null => { - return embedInstance.current?.render() ?? null; - }, [props]); + const renderedWebView = React.useMemo((): JSX.Element | null => { + return embedInstance.current?.render() ?? null; + }, [originalEmbedProps]); - const { viewConfig, listeners } = React.useMemo(() => getViewPropsAndListeners(props as U), [props]); + const { viewConfig, listeners } = React.useMemo(() => getViewPropsAndListeners(originalEmbedProps), [originalEmbedProps]); - React.useEffect(() => { - return () => { - embedInstance.current?.destroy(); - embedInstance.current = null; - } - }, []) + React.useEffect(() => { + try{ + return () => { + embedInstance.current?.destroy(); + embedInstance.current = null; + } + } catch(error) { + notifyErrorSDK(error as Error, onErrorSDK, ERROR_MESSAGE.COMPONENT_UNMOUNTED_ERROR); + } + }, []) + + useDeepCompareEffect(() => { + try{ + if(forwardedRef && typeof forwardedRef == 'object') { + (forwardedRef as React.MutableRefObject | null>).current = embedInstance?.current; + } + embedInstance?.current?.updateConfig(viewConfig); - useDeepCompareEffect(() => { - 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); + }); + } catch(error) { + notifyErrorSDK(error as Error, onErrorSDK, ERROR_MESSAGE.CONFIG_ERROR); + } + }, [viewConfig]); - Object.entries(listeners).forEach(([eventName, callback]) => { - embedInstance.current?.on(eventName as EmbedEvent, callback as MessageCallback); - }); - }, [viewConfig]); + if(!embedInstance.current) { + return null; + } - if(!embedInstance.current) { - return null; + return renderedWebView ?? null; + } catch(error) { + notifyErrorSDK(error as Error, onErrorSDK, ERROR_MESSAGE.INIT_ERROR); + return <>Unable to render the thoughtspot Webview; } - - return renderedWebView ?? null; }); \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..c4097aa --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,29 @@ +import { WebViewProps } from 'react-native-webview'; + +export const VERCEL_SHELL_URL = "https://embed-vercel-shell-git-class-based-final-yinstardevs-projects.vercel.app"; + +export enum MSG_TYPE { + INIT = "INIT", + EMBED = "EMBED", + INIT_VERCEL_SHELL = "INIT_VERCEL_SHELL", + REQUEST_AUTH_TOKEN = "REQUEST_AUTH_TOKEN", + EMBED_EVENT = "EMBED_EVENT", + HOST_EVENT_REPLY = "HOST_EVENT_REPLY", + EMBED_EVENT_REPLY = "EVENT_REPLY", + HOST_EVENT = "HOST_EVENT", + AUTH_TOKEN_RESPONSE = "AUTH_TOKEN_RESPONSE", +} + +export const DEFAULT_WEBVIEW_CONFIG: WebViewProps = { + javaScriptEnabled: true, + domStorageEnabled: true, + mixedContentMode: 'always', + keyboardDisplayRequiresUserAction: false, + automaticallyAdjustContentInsets: false, + scrollEnabled: false, + style: { + flex: 1, + height: '100%', + width: '100%', + }, +}; diff --git a/src/event-bridge.ts b/src/event-bridge.ts index 6bdbe30..5a1248a 100644 --- a/src/event-bridge.ts +++ b/src/event-bridge.ts @@ -1,8 +1,9 @@ import type { WebView } from "react-native-webview"; import { authFunctionCache } from "./init"; +import { MSG_TYPE } from './constants'; export interface EmbedMessage { - type: string; + type: MSG_TYPE; eventName?: string; eventId?: string; payload?: any; @@ -14,7 +15,7 @@ export class EmbedBridge { private events: Record = {}; private pendingReplies: Record = {}; - constructor(private webViewRef: React.RefObject) {} + constructor(private webViewRef: React.RefObject | null) {} registerEmbedEvent(eventName: string, callback: Function) { if (!this.events[eventName]) { @@ -24,7 +25,7 @@ export class EmbedBridge { } public trigger(hostEventName: string, payload?: any): Promise { - if (!this.webViewRef.current) { + if (!this.webViewRef?.current) { console.warn("webview is not ready for host event"); return Promise.resolve(undefined); } @@ -32,7 +33,7 @@ export class EmbedBridge { const eventId = this.generateEventId(); this.pendingReplies[eventId] = resolve; const message = { - type: "HOST_EVENT", + type: MSG_TYPE.HOST_EVENT, eventId, eventName: hostEventName, payload, @@ -43,17 +44,17 @@ export class EmbedBridge { handleMessage(msg: any) { switch (msg.type) { - case "REQUEST_AUTH_TOKEN": { + case MSG_TYPE.REQUEST_AUTH_TOKEN: { authFunctionCache?.().then((token: string) => { const replyTokenData = { - type: 'AUTH_TOKEN_RESPONSE', + type: MSG_TYPE.AUTH_TOKEN_RESPONSE, token, }; this.sendMessage(replyTokenData); }) break; } - case "EMBED_EVENT": { + case MSG_TYPE.EMBED_EVENT: { if(msg?.hasResponder) { this.triggerEventWithResponder(msg.eventName, msg.payload, msg.eventId); } else { @@ -61,7 +62,7 @@ export class EmbedBridge { } break; } - case "HOST_EVENT_REPLY": { + case MSG_TYPE.HOST_EVENT_REPLY: { if (msg.eventId && this.pendingReplies[msg.eventId]) { this.pendingReplies[msg.eventId](msg.payload); delete this.pendingReplies[msg.eventId]; @@ -83,7 +84,7 @@ export class EmbedBridge { handlers.forEach(handler => { handler(data, (responseData: any) => { this.sendMessage({ - type: 'EVENT_REPLY', + type: MSG_TYPE.EMBED_EVENT_REPLY, eventId, payload: responseData }); @@ -94,7 +95,7 @@ export class EmbedBridge { public sendMessage(msg: EmbedMessage) { const msgString = JSON.stringify(msg); const jsCode = `window.postMessage(${msgString}, "*");true;`; - this.webViewRef.current?.injectJavaScript(jsCode); + this.webViewRef?.current?.injectJavaScript(jsCode); } private generateEventId(): string { @@ -104,6 +105,6 @@ export class EmbedBridge { public destroy() { this.events = {}; this.pendingReplies = {}; - this.webViewRef = { current: null }; + this.webViewRef = null; } } diff --git a/src/init.ts b/src/init.ts index f6fd2ff..bfe2cef 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,11 +1,13 @@ -import { WebView } from "react-native-webview"; -import React, { useRef } from "react"; - +import { EmbedConfig } from "./types"; export let embedConfigCache: any = null; -export let authFunctionCache: (() => Promise) | null = null; +export let authFunctionCache: (() => Promise) | null | undefined = null; + +interface EmbedConfigMobile extends Omit { + getTokenFromSDK?: boolean; +} // TODO : add the webview at the time of the init. -export const init = (embedConfig: any) => { +export const init = (embedConfig: EmbedConfigMobile) => { embedConfigCache = embedConfig; authFunctionCache = embedConfig.getAuthToken; embedConfigCache.getTokenFromSDK = true; diff --git a/src/tsEmbed.tsx b/src/tsEmbed.tsx index c3f421f..7361d32 100644 --- a/src/tsEmbed.tsx +++ b/src/tsEmbed.tsx @@ -3,19 +3,22 @@ import { EmbedBridge } from "./event-bridge"; import React from "react"; import { ViewConfig } from "./types"; import { embedConfigCache } from "./init"; - +import * as Constants from './constants'; +import { MSG_TYPE, DEFAULT_WEBVIEW_CONFIG } from './constants'; +import { ERROR_MESSAGE, ErrorCallback, notifyErrorSDK } from "./util"; export class TSEmbed { - protected webViewRef: React.RefObject; + protected webViewRef: React.RefObject | null; protected embedBridge: EmbedBridge | null = null; - protected viewConfig: T; + protected viewConfig: T | null = null; protected vercelShellLoaded: boolean = false; private pendingHandlers: Array<[string, Function]> = []; - - constructor(webViewRef: React.RefObject, config?: T) { + private onErrorSDK?: ErrorCallback; + constructor(webViewRef: React.RefObject, onErrorSDK?: ErrorCallback, config?: T) { this.webViewRef = webViewRef; this.viewConfig = config || {} as T; this.handleMessage = this.handleMessage.bind(this); + this.onErrorSDK = onErrorSDK; } protected getEmbedType() { @@ -23,27 +26,27 @@ export class TSEmbed { } public updateConfig(config: Partial) { - this.viewConfig = { ...this.viewConfig, ...config }; + this.viewConfig = { ...this.viewConfig, ...config } as T; if(this.vercelShellLoaded) { this.sendConfigToShell(); } } public sendConfigToShell() { - if(!this.webViewRef.current || !this.vercelShellLoaded) { - console.log("[TSEmbed] Waiting for Vercel shell to load..."); + if(!this.webViewRef?.current || !this.vercelShellLoaded) { + console.info("Waiting for Vercel shell to load..."); return; } const initMsg = { - type: "INIT", + type: MSG_TYPE.INIT, payload: embedConfigCache, }; this.embedBridge?.sendMessage(initMsg); const message = { - type: "EMBED", + type: MSG_TYPE.EMBED, embedType: this.getEmbedType(), viewConfig: this.viewConfig, }; @@ -56,7 +59,7 @@ export class TSEmbed { if (this.embedBridge) { this.embedBridge.registerEmbedEvent(eventName, callback); } else { - console.log("[TSEmbed] Queuing event handler:", eventName); + console.info("Queuing event handler:", eventName); this.pendingHandlers.push([eventName, callback]); } } @@ -65,59 +68,52 @@ export class TSEmbed { return this.embedBridge?.trigger(hostEventName, payload); } - public handleMessage(event: WebViewMessageEvent) { + private handleInitVercelShell() { + this.vercelShellLoaded = true; + this.embedBridge = new EmbedBridge(this.webViewRef); + + this.pendingHandlers.forEach(([eventName, callback]) => { + this.embedBridge?.registerEmbedEvent(eventName, callback); + }); + this.pendingHandlers = []; + this.sendConfigToShell(); + } + + private 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(); + if (msg.type === MSG_TYPE.INIT_VERCEL_SHELL) { + this.handleInitVercelShell(); } this.embedBridge?.handleMessage(msg); } catch (err) { - console.error("[TsEmbed] handleMessage parse error:", err); + notifyErrorSDK(err as Error, this.onErrorSDK, ERROR_MESSAGE.EVENT_ERROR); } } public destroy() { + console.info("Destroying instance and cleaning up resources"); this.embedBridge?.destroy(); - + this.embedBridge = null; + this.webViewRef = null; + this.pendingHandlers = []; } public render(): JSX.Element { return ( { + {...DEFAULT_WEBVIEW_CONFIG} + onError = {(syntheticEvent) => { const { nativeEvent } = syntheticEvent; - console.warn("error in the webview", nativeEvent); + console.warn('Error in the WebView:', nativeEvent); }} - keyboardDisplayRequiresUserAction={false} - automaticallyAdjustContentInsets={false} - scrollEnabled={false} - onHttpError= {(syntheticEvent) => { + onHttpError = {(syntheticEvent) => { const { nativeEvent } = syntheticEvent; - console.warn("HTTP error in the webview", nativeEvent); + console.warn('HTTP error in the WebView:', nativeEvent); }} - style={{ flex: 1, - height: '100%', - width: '100%' - }} /> ); } diff --git a/src/util.ts b/src/util.ts index a427806..95d0dce 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,24 @@ import { EmbedEventHandlers } from "./componentFactory"; import { ViewConfig } from "./types"; +export type ErrorCallback = (error: Error) => void; + export interface EmbedProps extends ViewConfig, EmbedEventHandlers { + onErrorSDK?: ErrorCallback; +} + +//TODO : Emit the error message from the Vercel Shell +export const enum ERROR_MESSAGE { + INIT_ERROR = "Coudln't initialize the component", + EMBED_ERROR = "Coudln't embed the component", + AUTH_ERROR = "Authentication failed", + EVENT_ERROR = "Coudln't attach event handler", + COMPONENT_UNMOUNTED_ERROR = "Error while unmounting the component", + CONFIG_ERROR = "Error while updating the config", +} +export const notifyErrorSDK = (error: Error, onErrorSDK?: ErrorCallback, errorMessage?: ERROR_MESSAGE) => { + onErrorSDK?.(error); + console.error(error, errorMessage); + return; } \ No newline at end of file