Skip to content

Commit

Permalink
[NO CHANGELOG][Checkout Widget] Refactor post message handler (#2077)
Browse files Browse the repository at this point in the history
  • Loading branch information
jhesgodi authored Aug 7, 2024
1 parent bc564a1 commit e985bc7
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 136 deletions.
1 change: 1 addition & 0 deletions packages/checkout/sdk/src/postMessageHandler/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './postMessageHandler';
export * from './postMessageEventTypes';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum PostMessageHandlerEventType {
SYN = 'IMTBL_POST_MESSAGE_SYN',
ACK = 'IMTBL_POST_MESSAGE_ACK',
PROVIDER_RELAY = 'IMTBL_PROVIDER_RELAY',
EIP_6963_EVENT = 'IMTBL_EIP_6963_EVENT',
WIDGET_EVENT = 'IMTBL_CHECKOUT_WIDGET_EVENT',
}

export type PostMessageProviderRelayData = any;

export type PostMessageEIP6963Data = any;

export type PostMessagePayload =
| PostMessageProviderRelayData
| PostMessageEIP6963Data;
158 changes: 105 additions & 53 deletions packages/checkout/sdk/src/postMessageHandler/postMessageHandler.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,148 @@
import {
PostMessageHandlerEventType,
PostMessagePayload,
} from './postMessageEventTypes';

export type PostMessageHandlerConfiguration = {
targetOrigin: string;
eventTarget: MinimalEventTargetInterface;
eventSource?: MinimalEventSourceInterface;
eventTarget: WindowProxy;
eventSource?: WindowProxy;
};
// todo put these in a types file
export enum PostMessageHandlerEventType {
PROVIDER_RELAY = 'IMTBL_PROVIDER_RELAY',
EIP_6963_EVENT = 'IMTBL_EIP_6963_EVENT',
WIDGET_EVENT = 'IMTBL_CHECKOUT_WIDGET_EVENT',
}

export type PostMessageProviderRelayData = any;

export type PostMessageEIP6963Data = any;

export type PostMessagePayaload =
| PostMessageProviderRelayData
| PostMessageEIP6963Data;

export type PostMessageData = {
type: PostMessageHandlerEventType;
payload: PostMessagePayaload;
payload: PostMessagePayload;
};

export interface MinimalEventSourceInterface {
addEventListener(
eventType: 'message',
handler: (message: MessageEvent) => void
): void;
removeEventListener(
eventType: 'message',
handler: (message: MessageEvent) => void
): void;
}
export class PostMessageHandler {
private init: boolean = false;

export interface MinimalEventTargetInterface {
postMessage(message: any, targetOrigin?: string): void;
}
private haveSyn: boolean = false;

export class PostMessageHandler {
private eventHandlers: Map<PostMessageHandlerEventType, (data: any) => void> = new Map();
private subscribers: Array<(message: PostMessageData) => void> = [];

private queue: PostMessageData[] = [];

private targetOrigin!: string;

private eventTarget!: MinimalEventTargetInterface;
private eventTarget!: WindowProxy;

private eventSource!: WindowProxy;

private logger: (...args: any[]) => void;

private eventSource!: MinimalEventSourceInterface;
static isSynOrAck = (type: PostMessageHandlerEventType): boolean => type === PostMessageHandlerEventType.SYN
|| type === PostMessageHandlerEventType.ACK;

constructor({
targetOrigin,
eventTarget,
eventSource = window,
}: PostMessageHandlerConfiguration) {
this.handleMessage = this.handleMessage.bind(this);
if (!targetOrigin) {
throw new Error('targetOrigin is required');
}

if (!eventTarget) {
throw new Error('eventTarget is required');
}

if (typeof eventTarget.postMessage !== 'function') {
throw new Error(
'eventTarget.postMessage is not a function. This class should only be instantiated in a Window.',
);
}

this.targetOrigin = targetOrigin;
this.eventSource = eventSource;
this.eventTarget = eventTarget;
this.eventHandlers = new Map();
this.logger = () => {};

this.eventSource.addEventListener('message', this.handleMessage);
this.eventSource.addEventListener('message', this.onMessage);
this.handshake();
}

public sendMessage(type: PostMessageHandlerEventType, payload: any) {
public setLogger(logger: any) {
this.logger = logger;
}

private handshake = (): void => {
this.postMessage(PostMessageHandlerEventType.SYN, null);
};

private onMessage = (event: MessageEvent): void => {
if (event.origin !== this.targetOrigin) return;

if (this.init) {
this.handleMessage(event);
} else if (event.data?.type === PostMessageHandlerEventType.SYN) {
this.haveSyn = true;
this.postMessage(PostMessageHandlerEventType.ACK, null);
} else if (event.data?.type === PostMessageHandlerEventType.ACK) {
this.init = true;
if (!this.haveSyn) {
this.postMessage(PostMessageHandlerEventType.ACK, null);
}
this.flushQueue();
}
};

private flushQueue(): void {
while (this.queue.length > 0) {
const message = this.queue.shift();

if (message) {
this.logger('Flush message:', message);
this.send(message.type, message.payload);
}
}
}

private postMessage(type: PostMessageHandlerEventType, payload: any): void {
const message: PostMessageData = { type, payload };
this.eventTarget.postMessage(message, this.targetOrigin);

if (!PostMessageHandler.isSynOrAck(type)) {
this.logger('Send message:', { type, payload });
}
}

public addEventHandler(
type: PostMessageHandlerEventType,
handler: (data: any) => void,
): void {
this.eventHandlers.set(type, handler);
public send(type: PostMessageHandlerEventType, payload: any): void {
if (this.init || PostMessageHandler.isSynOrAck(type)) {
this.postMessage(type, payload);
return;
}

this.logger('Queue message:', { type, payload });
this.queue.push({ type, payload });
}

public removeEventHandler(type: PostMessageHandlerEventType): void {
this.eventHandlers.delete(type);
public subscribe(handler: (message: PostMessageData) => void): () => void {
this.subscribers.push(handler);

return () => {
this.unsubscribe(handler);
};
}

private handleMessage(event: MessageEvent) {
if (event.origin !== this.targetOrigin) {
return;
private unsubscribe(handler: (message: PostMessageData) => void): void {
const index = this.subscribers.indexOf(handler);
if (index !== -1) {
this.subscribers.splice(index, 1);
}
}

private handleMessage = (event: MessageEvent) => {
const message: PostMessageData = event.data;

const handler = this.eventHandlers.get(message.type);
if (handler) {
handler(message.payload);
if (!PostMessageHandler.isSynOrAck(message.type)) {
this.logger('Received message:', message);
}
}

this.subscribers.forEach((handler) => handler(message));
};

public destroy() {
this.eventSource.removeEventListener('message', this.handleMessage);
this.eventSource.removeEventListener('message', this.onMessage);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Web3Provider } from '@ethersproject/providers';
import { useTranslation } from 'react-i18next';
import {
ChainId,
Checkout,
Expand Down Expand Up @@ -54,6 +55,8 @@ export function ConnectLoader({
web3Provider,
} = params;

const { t } = useTranslation();

const [connectLoaderState, connectLoaderDispatch] = useReducer(
connectLoaderReducer,
{ ...initialConnectLoaderState, checkout }, // set checkout instance here
Expand Down Expand Up @@ -259,7 +262,7 @@ export function ConnectLoader({
},
});
}}
actionText="Try Again"
actionText={t('views.ERROR_VIEW.actionText')}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useEffect, useMemo, useReducer } from 'react';
import { useMemo, useReducer } from 'react';
import {
Checkout,
CheckoutWidgetConfiguration,
CheckoutWidgetParams,
} from '@imtbl/checkout-sdk';
import {
CheckoutActions,
checkoutReducer,
initialCheckoutState,
} from './context/CheckoutContext';
Expand All @@ -23,7 +22,7 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) {
const { config, checkout, params } = props;
const { environment, publishableKey } = checkout.config;

const [, iframeUrl] = useMemo(() => {
const [, iframeURL] = useMemo(() => {
if (!publishableKey) return ['', ''];
return getIframeURL(params, config, environment, publishableKey);
}, [params, config, environment, publishableKey]);
Expand All @@ -34,23 +33,12 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) {
);
const checkoutReducerValues = useMemo(
() => ({
checkoutState: { ...checkoutState, iframeUrl, checkout },
checkoutState: { ...checkoutState, iframeURL, checkout },
checkoutDispatch,
}),
[checkoutState, checkoutDispatch, iframeUrl, checkout],
[checkoutState, checkoutDispatch, iframeURL, checkout],
);

useEffect(() => {
if (iframeUrl === undefined) return;

checkoutDispatch({
payload: {
type: CheckoutActions.SET_IFRAME_URL,
iframeUrl,
},
});
}, [iframeUrl]);

return (
<CheckoutContextProvider values={checkoutReducerValues}>
<CheckoutAppIframe />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export interface CheckoutState {
checkout: Checkout | null;
provider: Web3Provider | undefined;
passport: Passport | undefined;
iframeUrl: string | undefined;
checkoutAppIframe: Window | undefined;
iframeURL: string | undefined;
iframeContentWindow: Window | undefined;
postMessageHandler: PostMessageHandler | undefined;
providerRelay: ProviderRelay | undefined;
walletProviderName: WalletProviderName | null;
Expand All @@ -23,8 +23,8 @@ export const initialCheckoutState: CheckoutState = {
checkout: null,
provider: undefined,
passport: undefined,
iframeUrl: undefined,
checkoutAppIframe: undefined,
iframeURL: undefined,
iframeContentWindow: undefined,
postMessageHandler: undefined,
providerRelay: undefined,
walletProviderInfo: null,
Expand All @@ -44,9 +44,9 @@ export interface CheckoutAction {
type ActionPayload =
| SetCheckoutPayload
| SetProviderPayload
| SetIframeUrlPayload
| SetIframeURLPayload
| SetPostMessageHandlerPayload
| SetCheckoutAppIframePayload
| SetIframeContentWindowPayload
| SetProviderRelayPayload
| SetPassportPayload
| SetProviderNamePayload
Expand Down Expand Up @@ -74,14 +74,14 @@ export interface SetProviderPayload {
provider: Web3Provider;
}

export interface SetIframeUrlPayload {
export interface SetIframeURLPayload {
type: CheckoutActions.SET_IFRAME_URL;
iframeUrl: string;
iframeURL: string;
}

export interface SetCheckoutAppIframePayload {
export interface SetIframeContentWindowPayload {
type: CheckoutActions.SET_CHECKOUT_APP_IFRAME;
checkoutAppIframe: Window;
iframeContentWindow: Window;
}

export interface SetPostMessageHandlerPayload {
Expand Down Expand Up @@ -142,12 +142,12 @@ export const checkoutReducer: Reducer<CheckoutState, CheckoutAction> = (
case CheckoutActions.SET_IFRAME_URL:
return {
...state,
iframeUrl: action.payload.iframeUrl,
iframeURL: action.payload.iframeURL,
};
case CheckoutActions.SET_CHECKOUT_APP_IFRAME:
return {
...state,
checkoutAppIframe: action.payload.checkoutAppIframe,
iframeContentWindow: action.payload.iframeContentWindow,
};
case CheckoutActions.SET_POST_MESSAGE_HANDLER:
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,22 @@ export function CheckoutContextProvider({
const {
checkout,
provider,
checkoutAppIframe,
iframeContentWindow,
postMessageHandler,
iframeUrl,
iframeURL,
} = checkoutState;

useEffect(() => {
if (!checkoutAppIframe || !checkout || !iframeUrl) return;
if (!iframeContentWindow || !checkout || !iframeURL) return;

const postMessageHandlerInstance = new PostMessageHandler({
targetOrigin: new URL(iframeUrl).origin,
eventTarget: checkoutAppIframe,
targetOrigin: new URL(iframeURL).origin,
eventTarget: iframeContentWindow,
});

// TODO: remove logger after done with development
postMessageHandlerInstance.setLogger((...args: any[]) => {
console.log("🔔 PARENT – ", ...args); // eslint-disable-line
});

checkoutDispatch({
Expand All @@ -44,7 +49,7 @@ export function CheckoutContextProvider({
postMessageHandler: postMessageHandlerInstance,
},
});
}, [checkoutAppIframe, checkout, iframeUrl]);
}, [iframeContentWindow, checkout, iframeURL]);

useEffect(() => {
if (!provider || !postMessageHandler) return undefined;
Expand Down
Loading

0 comments on commit e985bc7

Please sign in to comment.