Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "embed-react-native-sdk",
"version": "1.1.0",
"version": "2.0.6",
"description": "React Native SDK for Embedding TS",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
Expand All @@ -14,23 +14,10 @@
"react-native": ">=0.60.0",
"react-native-webview": ">=11.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-native": {
"optional": true
}
},
"devDependencies": {
"@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"
Expand Down
109 changes: 0 additions & 109 deletions src/BaseEmbed.tsx

This file was deleted.

21 changes: 0 additions & 21 deletions src/LiveboardEmbed.tsx

This file was deleted.

20 changes: 20 additions & 0 deletions src/LiveboardEmbedClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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<T extends LiveboardViewConfig = LiveboardViewConfig> extends TSEmbed<T> {
constructor(webViewRef: React.RefObject<WebView>, config?: T) {
super(webViewRef, config);
}
}

export interface LiveboardEmbedProps extends LiveboardViewConfig, EmbedProps {}

export const LiveboardEmbed: React.FC<LiveboardEmbedProps> = componentFactory<
typeof LiveboardEmbedClass,
LiveboardViewConfig,
LiveboardEmbedProps
>(LiveboardEmbedClass);
78 changes: 78 additions & 0 deletions src/componentFactory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React 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<key>}`]?: MessageCallback };


export interface ViewConfigAndListeners<T extends ViewConfig> {
viewConfig: T;
listeners: { [key in EmbedEvent]?: MessageCallback };
}

const getViewPropsAndListeners = <T extends EmbedProps, U extends ViewConfig>(
props: T
): ViewConfigAndListeners<U> => {
return Object.keys(props).reduce(
(accu, key) => {
if (key.startsWith('on')) {
const eventName = key.slice(2) as keyof typeof EmbedEvent;
(accu.listeners as Record<string, MessageCallback>)[EmbedEvent[eventName]] = props[key as keyof T] as MessageCallback;
} else {
(accu.viewConfig as Record<string, any>)[key] = props[key as keyof T];
}
return accu as ViewConfigAndListeners<U>;
},
{
viewConfig: {} as U,
listeners: {},
},
);
};

export const componentFactory = <T extends typeof TSEmbed, V extends ViewConfig, U extends EmbedProps>(
EmbedConstructor: T,
) => React.forwardRef<InstanceType<T>, U>(
(props, forwardedRef): JSX.Element => {
const embedInstance = React.useRef<InstanceType<T> | null>(null);
const webViewRef = React.useRef<WebView>(null);

// Creating the instance immediately
if (!embedInstance.current) {
embedInstance.current = new EmbedConstructor(webViewRef) as InstanceType<T>;
}

// Memoize the rendered WebView
const renderedWebView = React.useMemo(() => {
return embedInstance?.current?.render();
}, []);

React.useEffect(() => {
const { viewConfig, listeners } = getViewPropsAndListeners<U, V>(props as U);

if (embedInstance.current) {
embedInstance.current.updateConfig(viewConfig);

Object.entries(listeners).forEach(([eventName, callback]) => {
embedInstance.current?.on(eventName as EmbedEvent, callback as MessageCallback);
});
}

if (forwardedRef && typeof forwardedRef === 'object') {
forwardedRef.current = embedInstance.current;
}

return () => {
embedInstance.current?.destroy();
};
}, [props]);

return renderedWebView as JSX.Element;
}
);
74 changes: 46 additions & 28 deletions src/event-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,55 +11,54 @@ export interface EmbedMessage {
}

export class EmbedBridge {
private events: Record<string, Function[]> = {};
private eventHandlers: Record<string, Function[]> = {};
private pendingReplies: Record<string, Function> = {};

constructor(private webViewRef: React.RefObject<WebView>) {}

registerEmbedEvent(eventName: string, callback: Function) {
if (!this.events[eventName]) {
this.events[eventName] = [];
public on(eventName: string, callback: Function) {
if (!this.eventHandlers[eventName]) {
this.eventHandlers[eventName] = [];
}
this.events[eventName].push(callback);
this.eventHandlers[eventName].push(callback);
}

public trigger(hostEventName: string, payload?: any): Promise<any> {
public emit(eventName: string, payload?: any): Promise<any> {
if (!this.webViewRef.current) {
console.warn("webview is not ready for host event");
console.warn("webview is not ready for event:", eventName);
return Promise.resolve(undefined);
}

return new Promise((resolve) => {
const eventId = this.generateEventId();
this.pendingReplies[eventId] = resolve;
const message = {
type: "HOST_EVENT",

this.sendMessage({
type: 'EVENT',
eventId,
eventName: hostEventName,
eventName,
payload,
};
this.sendMessage(message);
});
});
}

handleMessage(msg: any) {
switch (msg.type) {
case "REQUEST_AUTH_TOKEN": {
authFunctionCache?.().then((token: string) => {
const replyTokenData = {
type: 'AUTH_TOKEN_RESPONSE',
token,
};
this.sendMessage(replyTokenData);
})
break;
authFunctionCache?.().then((token: string) => {
this.sendMessage({ type: 'AUTH_TOKEN_RESPONSE',token });
});
break;
}
case "EMBED_EVENT": {
if (msg.eventName) {
this.triggerEmbedEvent(msg.eventName, msg.payload);
case "EVENT": {
if (msg.hasResponder) {
this.triggerEventWithResponder(msg.eventName, msg.payload, msg.eventId);
} else {
this.triggerEvent(msg.eventName, msg.payload);
}
break;
}
case "HOST_EVENT_REPLY": {
case "EVENT_REPLY": {
if (msg.eventId && this.pendingReplies[msg.eventId]) {
this.pendingReplies[msg.eventId](msg.payload);
delete this.pendingReplies[msg.eventId];
Expand All @@ -71,12 +70,25 @@ export class EmbedBridge {
}
}

private triggerEmbedEvent(eventName: string, data: any) {
const callbacks = this.events[eventName] || [];
callbacks.forEach((cb) => cb(data));
private triggerEvent(eventName: string, data: any) {
const handlers = this.eventHandlers[eventName] || [];
handlers.forEach(handler => handler(data));
}

private triggerEventWithResponder(eventName: string, data: any, eventId: string) {
const handlers = this.eventHandlers[eventName] || [];
handlers.forEach(handler => {
handler(data, (responseData: any) => {
this.sendMessage({
type: 'EVENT_REPLY',
eventId,
payload: responseData
});
});
});
}

public sendMessage(msg: EmbedMessage) {
public sendMessage(msg: any) {
const msgString = JSON.stringify(msg);
const jsCode = `window.postMessage(${msgString}, "*");true;`;
this.webViewRef.current?.injectJavaScript(jsCode);
Expand All @@ -85,4 +97,10 @@ export class EmbedBridge {
private generateEventId(): string {
return `evt_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
}

public destroy() {
this.eventHandlers = {};
this.pendingReplies = {};
this.webViewRef = { current: null };
}
}
7 changes: 0 additions & 7 deletions src/hooks/useLiveboardRef.ts

This file was deleted.

Loading