From 05b38f53dd362de643aeff11dea7ab6c71593494 Mon Sep 17 00:00:00 2001 From: "Prashant.patil" Date: Mon, 24 Feb 2025 22:47:21 +0530 Subject: [PATCH 1/3] some refactoring --- package.json | 5 ++-- src/constants.ts | 37 +++++++++++++++++++++++++++ src/event-bridge.ts | 15 +++++------ src/tsEmbed.tsx | 61 ++++++++++++++++----------------------------- 4 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 src/constants.ts diff --git a/package.json b/package.json index d81eed0..80aae2c 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,13 @@ "dist/**/*" ], "dependencies": { - "use-deep-compare-effect": "^1.8.1" + "use-deep-compare-effect": "^1.8.1", + "react-native-webview": "*" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.60.0", - "react-native-webview": ">=11.0.0" + "react-native-webview": "*" }, "peerDependenciesMeta": { "@types/react": { diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..2cad60c --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,37 @@ +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, + onError: (syntheticEvent) => { + const { nativeEvent } = syntheticEvent; + console.warn('Error in the WebView:', nativeEvent); + }, + onHttpError: (syntheticEvent) => { + const { nativeEvent } = syntheticEvent; + console.warn('HTTP error in the WebView:', nativeEvent); + }, + style: { + flex: 1, + height: '100%', + width: '100%', + }, +}; diff --git a/src/event-bridge.ts b/src/event-bridge.ts index 6bdbe30..eff92c6 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; @@ -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 }); diff --git a/src/tsEmbed.tsx b/src/tsEmbed.tsx index c3f421f..3f1a6b8 100644 --- a/src/tsEmbed.tsx +++ b/src/tsEmbed.tsx @@ -3,7 +3,8 @@ 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'; export class TSEmbed { protected webViewRef: React.RefObject; @@ -36,14 +37,14 @@ export class TSEmbed { } 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, }; @@ -65,59 +66,41 @@ 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); + console.error("HandleMessage parse error:", err); } } public destroy() { this.embedBridge?.destroy(); - + this.embedBridge = null; } 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%' - }} + {...DEFAULT_WEBVIEW_CONFIG} /> ); } From 590b12120c7b5134ea034ce315e9389de6177bf4 Mon Sep 17 00:00:00 2001 From: "Prashant.patil" Date: Tue, 25 Feb 2025 07:50:20 +0530 Subject: [PATCH 2/3] Error Boundry initial and optDeps --- package.json | 11 ++++++----- src/ErrorBoundry.ts | 26 ++++++++++++++++++++++++++ src/util.ts | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 src/ErrorBoundry.ts diff --git a/package.json b/package.json index 80aae2c..fd2c26d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "embed-react-native-sdk", - "version": "4.0.1", + "version": "4.0.2", "description": "React Native SDK for Embedding TS", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", @@ -9,13 +9,11 @@ "dist/**/*" ], "dependencies": { - "use-deep-compare-effect": "^1.8.1", - "react-native-webview": "*" + "use-deep-compare-effect": "^1.8.1" }, "peerDependencies": { "react": ">=16.8.0", - "react-native": ">=0.60.0", - "react-native-webview": "*" + "react-native": ">=0.60.0" }, "peerDependenciesMeta": { "@types/react": { @@ -33,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/ErrorBoundry.ts b/src/ErrorBoundry.ts new file mode 100644 index 0000000..eeedbfa --- /dev/null +++ b/src/ErrorBoundry.ts @@ -0,0 +1,26 @@ +import React from 'react'; + +export class ErrorBoundary extends React.Component<{ + fallback: React.ReactNode, + hasError: boolean, + children: React.ReactNode +}> { + state = { hasError: false }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + + //TODO : Error logging + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error(error, errorInfo); + } + + render() { + if (this.state.hasError) { + return this.props.fallback; + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index a427806..6c3de1b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2,5 +2,5 @@ import { EmbedEventHandlers } from "./componentFactory"; import { ViewConfig } from "./types"; export interface EmbedProps extends ViewConfig, EmbedEventHandlers { - + onErrorSDK?: (error: Error, context?: Record) => void; } \ No newline at end of file From ec71936dce6fab6043aceb227ac50e0d8f14bb99 Mon Sep 17 00:00:00 2001 From: "Prashant.patil" Date: Fri, 28 Feb 2025 07:01:51 +0530 Subject: [PATCH 3/3] v 4.0.4 publish --- package.json | 2 +- src/ErrorBoundry.ts | 26 -------------- src/LiveboardEmbedClass.ts | 6 ++-- src/componentFactory.tsx | 71 +++++++++++++++++++++++--------------- src/constants.ts | 8 ----- src/event-bridge.ts | 8 ++--- src/init.ts | 12 ++++--- src/tsEmbed.tsx | 31 ++++++++++++----- src/util.ts | 20 ++++++++++- 9 files changed, 100 insertions(+), 84 deletions(-) delete mode 100644 src/ErrorBoundry.ts diff --git a/package.json b/package.json index fd2c26d..0a83884 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "embed-react-native-sdk", - "version": "4.0.2", + "version": "4.0.3", "description": "React Native SDK for Embedding TS", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", diff --git a/src/ErrorBoundry.ts b/src/ErrorBoundry.ts deleted file mode 100644 index eeedbfa..0000000 --- a/src/ErrorBoundry.ts +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -export class ErrorBoundary extends React.Component<{ - fallback: React.ReactNode, - hasError: boolean, - children: React.ReactNode -}> { - state = { hasError: false }; - - static getDerivedStateFromError() { - return { hasError: true }; - } - - //TODO : Error logging - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - console.error(error, errorInfo); - } - - render() { - if (this.state.hasError) { - return this.props.fallback; - } - - return this.props.children; - } -} \ No newline at end of file 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 index 2cad60c..c4097aa 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -21,14 +21,6 @@ export const DEFAULT_WEBVIEW_CONFIG: WebViewProps = { keyboardDisplayRequiresUserAction: false, automaticallyAdjustContentInsets: false, scrollEnabled: false, - onError: (syntheticEvent) => { - const { nativeEvent } = syntheticEvent; - console.warn('Error in the WebView:', nativeEvent); - }, - onHttpError: (syntheticEvent) => { - const { nativeEvent } = syntheticEvent; - console.warn('HTTP error in the WebView:', nativeEvent); - }, style: { flex: 1, height: '100%', diff --git a/src/event-bridge.ts b/src/event-bridge.ts index eff92c6..5a1248a 100644 --- a/src/event-bridge.ts +++ b/src/event-bridge.ts @@ -15,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]) { @@ -25,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); } @@ -95,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 { @@ -105,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 3f1a6b8..7361d32 100644 --- a/src/tsEmbed.tsx +++ b/src/tsEmbed.tsx @@ -5,18 +5,20 @@ 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() { @@ -24,15 +26,15 @@ 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; } @@ -57,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]); } } @@ -85,13 +87,16 @@ export class TSEmbed { } this.embedBridge?.handleMessage(msg); } catch (err) { - console.error("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 { @@ -101,6 +106,14 @@ export class TSEmbed { source={{ uri: Constants.VERCEL_SHELL_URL }} onMessage={this.handleMessage} {...DEFAULT_WEBVIEW_CONFIG} + onError = {(syntheticEvent) => { + const { nativeEvent } = syntheticEvent; + console.warn('Error in the WebView:', nativeEvent); + }} + onHttpError = {(syntheticEvent) => { + const { nativeEvent } = syntheticEvent; + console.warn('HTTP error in the WebView:', nativeEvent); + }} /> ); } diff --git a/src/util.ts b/src/util.ts index 6c3de1b..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?: (error: Error, context?: Record) => void; + 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