Skip to content

Commit

Permalink
Feat: Checkout App Setup (#2030)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwhardwick authored Jul 30, 2024
1 parent 6790dbe commit 1435e74
Show file tree
Hide file tree
Showing 10 changed files with 479 additions and 22 deletions.
7 changes: 7 additions & 0 deletions packages/checkout/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ export type {
WalletInfo,
} from './types';

export {
PostMessageHandler,
PostMessageHandlerConfiguration,
PostMessageHandlerEventType,
PostMessageData,
} from './postMessageHandler';

export type { ErrorType } from './errors';

export { CheckoutErrorType } from './errors';
Expand Down
1 change: 1 addition & 0 deletions packages/checkout/sdk/src/postMessageHandler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './postMessageHandler';
77 changes: 77 additions & 0 deletions packages/checkout/sdk/src/postMessageHandler/postMessageHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export type PostMessageHandlerConfiguration = {
targetOrigin: string;
eventTarget: MinimalEventTargetInterface;
eventSource?: MinimalEventSourceInterface;
};
// todo put these in a types file
export enum PostMessageHandlerEventType {
PROVIDER_RELAY = 'PROVIDER_RELAY',
}

export type PostMessageData = {
type: PostMessageHandlerEventType;
payload: any;
};

export interface MinimalEventSourceInterface {
addEventListener(eventType: 'message', handler: (message: MessageEvent) => void): void;
removeEventListener(eventType: 'message', handler: (message: MessageEvent) => void): void;
}

export interface MinimalEventTargetInterface {
postMessage(message: any, targetOrigin?: string): void;
}

export class PostMessageHandler {
private eventHandlers: Map<PostMessageHandlerEventType, (data: any) => void> = new Map();

private targetOrigin!: string;

private eventTarget!: MinimalEventTargetInterface;

private eventSource!: MinimalEventSourceInterface;

constructor({
targetOrigin,
eventTarget,
eventSource = window,
}: PostMessageHandlerConfiguration) {
this.handleMessage = this.handleMessage.bind(this);
this.targetOrigin = targetOrigin;
this.eventSource = eventSource;
this.eventTarget = eventTarget;
this.eventHandlers = new Map();

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

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

public addEventHandler(type: PostMessageHandlerEventType, handler: (data: any) => void): void {
this.eventHandlers.set(type, handler);
}

public removeEventHandler(type: PostMessageHandlerEventType): void {
this.eventHandlers.delete(type);
}

private handleMessage(event: MessageEvent) {
if (event.origin !== this.targetOrigin) {
return;
}

const message: PostMessageData = event.data;

const handler = this.eventHandlers.get(message.type);
if (handler) {
handler(message.payload);
}
}

public destroy() {
this.eventSource.removeEventListener('message', this.handleMessage);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { useEffect, useState } from 'react';
import { Box } from '@biom3/react';
import { CheckoutWidgetParams, Checkout, CheckoutWidgetConfiguration } from '@imtbl/checkout-sdk';
import {
Checkout,
CheckoutWidgetConfiguration,
CheckoutWidgetParams,
WalletProviderName,
} from '@imtbl/checkout-sdk';
import {
useEffect,
useMemo,
useReducer,
} from 'react';
import {
CheckoutActions,
checkoutReducer,
initialCheckoutState,
} from './context/CheckoutContext';
import { CheckoutContextProvider } from './context/CheckoutContextProvider';
import { CheckoutAppIframe } from './views/CheckoutAppIframe';
// import { CHECKOUT_APP_URL } from '../../lib/constants';

import { getIframeURL } from './functions/iframeParams';

Expand All @@ -14,32 +30,53 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) {
const { config, checkout, params } = props;
const { environment, publishableKey } = checkout.config;

const [iframeURL, setIframeURL] = useState<string>();
const [checkoutState, checkoutDispatch] = useReducer(checkoutReducer, initialCheckoutState);
const checkoutReducerValues = useMemo(
() => ({ checkoutState, checkoutDispatch }),
[checkoutState, checkoutDispatch],
);

useEffect(() => {
if (!publishableKey) return;

const url = getIframeURL(params, config, environment, publishableKey);
setIframeURL(url);
const iframeUrl = getIframeURL(params, config, environment, publishableKey);

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

// TODO:
// on iframe load error, go to error view 500
// on iframe loading, show loading view, requires iframe to trigger an initialised event
useEffect(() => {
checkoutDispatch({
payload: {
type: CheckoutActions.SET_CHECKOUT,
checkout,
},
});

const connectProvider = async () => {
const createProviderResult = await checkout.createProvider({ walletProviderName: WalletProviderName.METAMASK });

const connectResult = await checkout.connect({ provider: createProviderResult.provider });

checkoutDispatch({
payload: {
type: CheckoutActions.SET_PROVIDER,
provider: connectResult.provider,
},
});
};

if (!iframeURL) {
return null;
}
connectProvider();
}, [checkout]);

return (
<Box
rc={<iframe id="checkout-app" src={iframeURL} title="checkout" />}
sx={{
w: '100%',
h: '100%',
border: 'none',
boxShadow: 'none',
}}
/>
<CheckoutContextProvider values={checkoutReducerValues}>
CheckoutWidgetComponent
<CheckoutAppIframe />
</CheckoutContextProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Web3Provider } from '@ethersproject/providers';
import { createContext } from 'react';
import {
Checkout, EIP6963ProviderInfo, PostMessageHandler, WalletProviderName,
} from '@imtbl/checkout-sdk';
import { Passport } from '@imtbl/passport';
import { ProviderRelay } from './ProviderRelay';

export interface CheckoutState {
checkout: Checkout | null;
provider: Web3Provider | undefined;
passport: Passport | undefined;
iframeUrl: string | undefined;
checkoutAppIframe: Window | undefined;
postMessageHandler: PostMessageHandler | undefined;
providerRelay: ProviderRelay | undefined;
walletProviderName: WalletProviderName | null;
walletProviderInfo: EIP6963ProviderInfo | null;
sendCloseEvent: () => void;
}

export const initialCheckoutState: CheckoutState = {
checkout: null,
provider: undefined,
passport: undefined,
iframeUrl: undefined,
checkoutAppIframe: undefined,
postMessageHandler: undefined,
providerRelay: undefined,
walletProviderInfo: null,
walletProviderName: null,
sendCloseEvent: () => { },
};

export interface CheckoutContextState {
checkoutState: CheckoutState;
checkoutDispatch: React.Dispatch<CheckoutAction>;
}

export interface CheckoutAction {
payload: ActionPayload;
}

type ActionPayload =
| SetCheckoutPayload
| SetProviderPayload
| SetIframeUrlPayload
| SetPostMessageHandlerPayload
| SetCheckoutAppIframePayload
| SetProviderRelayPayload
| SetPassportPayload
| SetProviderNamePayload
| SetSendCloseEventPayload;

export enum CheckoutActions {
SET_CHECKOUT = 'SET_CHECKOUT',
SET_PROVIDER = 'SET_PROVIDER',
SET_IFRAME_URL = 'SET_IFRAME_URL',
SET_POST_MESSAGE_HANDLER = 'SET_POST_MESSAGE_HANDLER',
SET_CHECKOUT_APP_IFRAME = 'SET_CHECKOUT_APP_IFRAME',
SET_PROVIDER_RELAY = 'SET_PROVIDER_RELAY',
SET_PASSPORT = 'SET_PASSPORT',
SET_WALLET_PROVIDER_NAME = 'SET_WALLET_PROVIDER_NAME',
SET_SEND_CLOSE_EVENT = 'SET_SEND_CLOSE_EVENT',
}

export interface SetCheckoutPayload {
type: CheckoutActions.SET_CHECKOUT;
checkout: Checkout;
}

export interface SetProviderPayload {
type: CheckoutActions.SET_PROVIDER;
provider: Web3Provider;
}

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

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

export interface SetPostMessageHandlerPayload {
type: CheckoutActions.SET_POST_MESSAGE_HANDLER;
postMessageHandler: PostMessageHandler;
}

export interface SetProviderRelayPayload {
type: CheckoutActions.SET_PROVIDER_RELAY;
providerRelay: ProviderRelay;
}

export interface SetPassportPayload {
type: CheckoutActions.SET_PASSPORT;
passport: Passport;
}

export interface SetProviderNamePayload {
type: CheckoutActions.SET_WALLET_PROVIDER_NAME;
walletProviderName: WalletProviderName;
}

export interface SetSendCloseEventPayload {
type: CheckoutActions.SET_SEND_CLOSE_EVENT;
sendCloseEvent: () => void;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export const CheckoutContext = createContext<CheckoutContextState>({
checkoutState: initialCheckoutState,
checkoutDispatch: () => { },
});

CheckoutContext.displayName = 'CheckoutContext'; // help with debugging Context in browser

export type Reducer<S, A> = (prevState: S, action: A) => S;

export const checkoutReducer: Reducer<CheckoutState, CheckoutAction> = (
state: CheckoutState,
action: CheckoutAction,
) => {
switch (action.payload.type) {
case CheckoutActions.SET_CHECKOUT:
return {
...state,
checkout: action.payload.checkout,
};
case CheckoutActions.SET_PROVIDER:
return {
...state,
provider: action.payload.provider,
};
case CheckoutActions.SET_PASSPORT:
return {
...state,
passport: action.payload.passport,
};
case CheckoutActions.SET_IFRAME_URL:
return {
...state,
iframeUrl: action.payload.iframeUrl,
};
case CheckoutActions.SET_CHECKOUT_APP_IFRAME:
return {
...state,
checkoutAppIframe: action.payload.checkoutAppIframe,
};
case CheckoutActions.SET_POST_MESSAGE_HANDLER:
return {
...state,
postMessageHandler: action.payload.postMessageHandler,
};
case CheckoutActions.SET_PROVIDER_RELAY:
return {
...state,
providerRelay: action.payload.providerRelay,
};
case CheckoutActions.SET_WALLET_PROVIDER_NAME:
return {
...state,
walletProviderName: action.payload.walletProviderName,
};
case CheckoutActions.SET_SEND_CLOSE_EVENT:
return {
...state,
sendCloseEvent: action.payload.sendCloseEvent,
};
default:
return state;
}
};
Loading

0 comments on commit 1435e74

Please sign in to comment.