Skip to content

Commit

Permalink
[NO CHANGELOG][Checkout Widget] Handle widget event (#2109)
Browse files Browse the repository at this point in the history
Co-authored-by: Jhonatan Gonzalez <[email protected]>
  • Loading branch information
jwhardwick and jhesgodi authored Aug 23, 2024
1 parent 19566cb commit bcdd87e
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface CheckoutState {
walletProviderName: WalletProviderName | null;
walletProviderInfo: EIP6963ProviderInfo | null;
sendCloseEvent: () => void;
initialised: boolean;
}

export const initialCheckoutState: CheckoutState = {
Expand All @@ -27,6 +28,7 @@ export const initialCheckoutState: CheckoutState = {
walletProviderInfo: null,
walletProviderName: null,
sendCloseEvent: () => { },
initialised: false,
};

export interface CheckoutContextState {
Expand All @@ -46,7 +48,8 @@ type ActionPayload =
| SetIframeContentWindowPayload
| SetPassportPayload
| SetProviderNamePayload
| SetSendCloseEventPayload;
| SetSendCloseEventPayload
| SetInitialisedPayload;

export enum CheckoutActions {
SET_CHECKOUT = 'SET_CHECKOUT',
Expand All @@ -57,6 +60,7 @@ export enum CheckoutActions {
SET_PASSPORT = 'SET_PASSPORT',
SET_WALLET_PROVIDER_NAME = 'SET_WALLET_PROVIDER_NAME',
SET_SEND_CLOSE_EVENT = 'SET_SEND_CLOSE_EVENT',
SET_INITIALISED = 'SET_INITIALISED',
}

export interface SetCheckoutPayload {
Expand Down Expand Up @@ -99,6 +103,11 @@ export interface SetSendCloseEventPayload {
sendCloseEvent: () => void;
}

export interface SetInitialisedPayload {
type: CheckoutActions.SET_INITIALISED;
initialised: boolean;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export const CheckoutContext = createContext<CheckoutContextState>({
checkoutState: initialCheckoutState,
Expand Down Expand Up @@ -154,6 +163,11 @@ export const checkoutReducer: Reducer<CheckoutState, CheckoutAction> = (
...state,
sendCloseEvent: action.payload.sendCloseEvent,
};
case CheckoutActions.SET_INITIALISED:
return {
...state,
initialised: action.payload.initialised,
};
default:
return state;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useContext, useEffect, useRef } from 'react';
import {
CheckoutConnectSuccessEvent,
CheckoutEventType,
CheckoutFlowType,
CheckoutSuccessEvent,
IMTBLWidgetEvents,
PostMessageHandlerEventType,
WidgetEventData,
WidgetType,
} from '@imtbl/checkout-sdk';
import { useCheckoutContext } from '../context/CheckoutContextProvider';
import { sendCheckoutEvent } from '../CheckoutWidgetEvents';
import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext';
import { CheckoutActions } from '../context/CheckoutContext';

function isWidgetEvent(payload: any): payload is {
type: IMTBLWidgetEvents.IMTBL_CHECKOUT_WIDGET_EVENT;
detail: {
type: CheckoutEventType;
data: WidgetEventData[WidgetType.CHECKOUT][keyof WidgetEventData[WidgetType.CHECKOUT]];
};
} {
return payload.type === IMTBLWidgetEvents.IMTBL_CHECKOUT_WIDGET_EVENT;
}

export function useCheckoutEventsRelayer() {
const [{ postMessageHandler, provider }, checkoutDispatch] = useCheckoutContext();
const { eventTargetState: { eventTarget } } = useContext(EventTargetContext);
const unsubscribePostMessageHandler = useRef<(() => void) | undefined>();

useEffect(() => {
if (!postMessageHandler) return undefined;
unsubscribePostMessageHandler.current?.();

unsubscribePostMessageHandler.current = postMessageHandler.subscribe(({ type, payload }) => {
if (type !== PostMessageHandlerEventType.WIDGET_EVENT || !isWidgetEvent(payload)) return;

if (payload.detail.type === CheckoutEventType.SUCCESS
&& (payload.detail.data as CheckoutSuccessEvent).flow === CheckoutFlowType.CONNECT) {
const checkoutConnectSuccessEvent = payload.detail as unknown as CheckoutConnectSuccessEvent;
if (!provider) {
throw new Error('Provider not found, unable to send checkout connect success event');
}
checkoutConnectSuccessEvent.data.provider = provider;
sendCheckoutEvent(eventTarget, { type: payload.detail.type, data: checkoutConnectSuccessEvent });
return;
}

if (payload.detail.type === CheckoutEventType.DISCONNECTED) {
checkoutDispatch({
payload: {
type: CheckoutActions.SET_PROVIDER,
provider: undefined,
},
});
}

sendCheckoutEvent(eventTarget, payload.detail);

if (payload.detail.type === CheckoutEventType.INITIALISED) {
checkoutDispatch({
payload: {
type: CheckoutActions.SET_INITIALISED,
initialised: true,
},
});
}
});

return () => {
unsubscribePostMessageHandler.current?.();
};
}, [postMessageHandler, checkoutDispatch, eventTarget, provider]);
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { Box } from '@biom3/react';
import { useTranslation } from 'react-i18next';
import {
useContext, useEffect, useRef, useState,
} from 'react';
import {
CheckoutEventType, IMTBLWidgetEvents,
PostMessageHandlerEventType,
WidgetEventData,
WidgetType,
CheckoutEventType,
} from '@imtbl/checkout-sdk';
import { CheckoutActions } from '../context/CheckoutContext';
import { useCheckoutContext } from '../context/CheckoutContextProvider';
import { sendCheckoutEvent } from '../CheckoutWidgetEvents';
import {
useContext,
useEffect,
useRef, useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext';
import { LoadingView } from '../../../views/loading/LoadingView';
import { ErrorView } from '../../../views/error/ErrorView';
import { LoadingView } from '../../../views/loading/LoadingView';
import { sendCheckoutEvent } from '../CheckoutWidgetEvents';
import { CheckoutActions } from '../context/CheckoutContext';
import { useCheckoutContext } from '../context/CheckoutContextProvider';
import { useCheckoutEventsRelayer } from '../hooks/useCheckoutEventsRelayer';
import { useEip6963Relayer } from '../hooks/useEip6963Relayer';
import { useProviderRelay } from '../hooks/useProviderRelay';
import {
IFRAME_INIT_TIMEOUT_MS,
IFRAME_ALLOW_PERMISSIONS,
IFRAME_INIT_TIMEOUT_MS,
} from '../utils/config';
import { useEip6963Relayer } from '../hooks/useEip6963Relayer';
import { useProviderRelay } from '../hooks/useProviderRelay';

export interface LoadingHandoverProps {
text: string;
Expand All @@ -31,17 +31,16 @@ export interface LoadingHandoverProps {
export function CheckoutAppIframe() {
const { t } = useTranslation();
const iframeRef = useRef<HTMLIFrameElement>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [loadingError, setLoadingError] = useState<boolean>(false);
const [initialised, setInitialised] = useState<boolean>(false);
const [
{
iframeURL, postMessageHandler, iframeContentWindow,
iframeURL, iframeContentWindow, initialised,
},
checkoutDispatch,
] = useCheckoutContext();
const unsubscribePostMessageHandler = useRef<() => void>();

const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const initialisedRef = useRef(initialised);
useCheckoutEventsRelayer();
useEip6963Relayer();
useProviderRelay();

Expand All @@ -51,6 +50,23 @@ export function CheckoutAppIframe() {
eventTargetState: { eventTarget },
} = useContext(EventTargetContext);

useEffect(() => {
initialisedRef.current = initialised;
}, [initialised]);

useEffect(() => {
timeoutRef.current = setTimeout(() => {
if (initialisedRef.current) return;

setLoadingError(true);
clearTimeout(timeoutRef.current!);
}, IFRAME_INIT_TIMEOUT_MS);

return () => {
clearTimeout(timeoutRef.current!);
};
}, []);

const onIframeLoad = () => {
const iframe = iframeRef.current;
if (!iframe?.contentWindow) {
Expand All @@ -65,59 +81,6 @@ export function CheckoutAppIframe() {
});
};

useEffect(() => {
if (!postMessageHandler) return undefined;
unsubscribePostMessageHandler.current?.();

// subscribe to widget events
// TODO: Move to its own hook
unsubscribePostMessageHandler.current = postMessageHandler.subscribe(({ type, payload }) => {
if (type !== PostMessageHandlerEventType.WIDGET_EVENT) return;

// FIXME: improve typing
const customEvent: {
type: IMTBLWidgetEvents.IMTBL_CHECKOUT_WIDGET_EVENT;
detail: {
type: CheckoutEventType;
data: WidgetEventData[WidgetType.CHECKOUT][keyof WidgetEventData[WidgetType.CHECKOUT]];
};
} = payload as any;

// TODO: intercept connect success and inject the state provider
// FIXME: events type narrowing is not working properly
if (customEvent.detail.type === CheckoutEventType.DISCONNECTED) {
checkoutDispatch({
payload: {
type: CheckoutActions.SET_PROVIDER,
provider: undefined,
},
});
}

// Forward widget events
sendCheckoutEvent(eventTarget, customEvent.detail);

// If iframe has been initialised, set widget as initialised
if (customEvent.detail.type === CheckoutEventType.INITIALISED) {
setInitialised(true);
clearTimeout(timeoutRef.current!);
}
});

// Expire iframe initialisation after timeout
// and set a loading error
timeoutRef.current = setTimeout(() => {
if (!initialised) {
setLoadingError(true);
clearTimeout(timeoutRef.current!);
}
}, IFRAME_INIT_TIMEOUT_MS);

return () => {
clearTimeout(timeoutRef.current!);
};
}, [postMessageHandler, checkoutDispatch]);

if (loadingError) {
return (
<ErrorView
Expand Down

0 comments on commit bcdd87e

Please sign in to comment.