Skip to content

Commit

Permalink
[NO CHANGELOG][Checkout Widget] Subscribe to checkout iframe events (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jhesgodi authored Jul 30, 2024
1 parent 1435e74 commit 1bd9673
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export enum CheckoutEventType {

CHECKOUT_APP_READY = 'CHECKOUT_APP_READY',
CHECKOUT_APP_EVENT = 'CHECKOUT_APP_EVENT',
}
/**
* Represents a successful connection.
* @property {Web3Provider} provider
* @property {WalletProviderName | undefined} walletProviderName
*/
export type CheckoutEventReady = {
};
82 changes: 45 additions & 37 deletions packages/checkout/sdk/src/widgets/definitions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,54 +131,62 @@ type ProviderEventMapping = {
*/
export type WidgetEventData = {
[WidgetType.CONNECT]: {
[ConnectEventType.SUCCESS]: ConnectionSuccess,
[ConnectEventType.FAILURE]: ConnectionFailed,
[ConnectEventType.CLOSE_WIDGET]: {},
[ConnectEventType.WALLETCONNECT_PROVIDER_UPDATED]: WalletConnectProviderChanged,
} & OrchestrationMapping & ProviderEventMapping,
[ConnectEventType.SUCCESS]: ConnectionSuccess;
[ConnectEventType.FAILURE]: ConnectionFailed;
[ConnectEventType.CLOSE_WIDGET]: {};
[ConnectEventType.WALLETCONNECT_PROVIDER_UPDATED]: WalletConnectProviderChanged;
} & OrchestrationMapping &
ProviderEventMapping;

[WidgetType.WALLET]: {
[WalletEventType.NETWORK_SWITCH]: WalletNetworkSwitch
[WalletEventType.DISCONNECT_WALLET]: WalletDisconnect
[WalletEventType.CLOSE_WIDGET]: {}
} & OrchestrationMapping & ProviderEventMapping,
[WalletEventType.NETWORK_SWITCH]: WalletNetworkSwitch;
[WalletEventType.DISCONNECT_WALLET]: WalletDisconnect;
[WalletEventType.CLOSE_WIDGET]: {};
} & OrchestrationMapping &
ProviderEventMapping;

[WidgetType.SWAP]: {
[SwapEventType.SUCCESS]: SwapSuccess,
[SwapEventType.FAILURE]: SwapFailed,
[SwapEventType.REJECTED]: SwapRejected,
[SwapEventType.CLOSE_WIDGET]: {},
} & OrchestrationMapping & ProviderEventMapping
[SwapEventType.SUCCESS]: SwapSuccess;
[SwapEventType.FAILURE]: SwapFailed;
[SwapEventType.REJECTED]: SwapRejected;
[SwapEventType.CLOSE_WIDGET]: {};
} & OrchestrationMapping &
ProviderEventMapping;

[WidgetType.BRIDGE]: {
[BridgeEventType.TRANSACTION_SENT]: BridgeTransactionSent,
[BridgeEventType.FAILURE]: BridgeFailed,
[BridgeEventType.CLOSE_WIDGET]: {}
[BridgeEventType.CLAIM_WITHDRAWAL_SUCCESS]: BridgeClaimWithdrawalSuccess
[BridgeEventType.CLAIM_WITHDRAWAL_FAILURE]: BridgeClaimWithdrawalFailed
} & OrchestrationMapping & ProviderEventMapping,
[BridgeEventType.TRANSACTION_SENT]: BridgeTransactionSent;
[BridgeEventType.FAILURE]: BridgeFailed;
[BridgeEventType.CLOSE_WIDGET]: {};
[BridgeEventType.CLAIM_WITHDRAWAL_SUCCESS]: BridgeClaimWithdrawalSuccess;
[BridgeEventType.CLAIM_WITHDRAWAL_FAILURE]: BridgeClaimWithdrawalFailed;
} & OrchestrationMapping &
ProviderEventMapping;

[WidgetType.ONRAMP]: {
[OnRampEventType.SUCCESS]: OnRampSuccess,
[OnRampEventType.FAILURE]: OnRampFailed,
[OnRampEventType.CLOSE_WIDGET]: {},
} & OrchestrationMapping & ProviderEventMapping,
[OnRampEventType.SUCCESS]: OnRampSuccess;
[OnRampEventType.FAILURE]: OnRampFailed;
[OnRampEventType.CLOSE_WIDGET]: {};
} & OrchestrationMapping &
ProviderEventMapping;

[WidgetType.SALE]: {
[SaleEventType.SUCCESS]: SaleSuccess,
[SaleEventType.FAILURE]: SaleFailed,
[SaleEventType.REJECTED]: any,
[SaleEventType.CLOSE_WIDGET]: {},
[SaleEventType.TRANSACTION_SUCCESS]: SaleTransactionSuccess,
[SaleEventType.PAYMENT_METHOD]: SalePaymentMethod,
[SaleEventType.PAYMENT_TOKEN]: SalePaymentToken,
[SaleEventType.REQUEST_BRIDGE]: {},
[SaleEventType.REQUEST_SWAP]: {},
[SaleEventType.REQUEST_ONRAMP]: {},
} & OrchestrationMapping & ProviderEventMapping,

[SaleEventType.SUCCESS]: SaleSuccess;
[SaleEventType.FAILURE]: SaleFailed;
[SaleEventType.REJECTED]: any;
[SaleEventType.CLOSE_WIDGET]: {};
[SaleEventType.TRANSACTION_SUCCESS]: SaleTransactionSuccess;
[SaleEventType.PAYMENT_METHOD]: SalePaymentMethod;
[SaleEventType.PAYMENT_TOKEN]: SalePaymentToken;
[SaleEventType.REQUEST_BRIDGE]: {};
[SaleEventType.REQUEST_SWAP]: {};
[SaleEventType.REQUEST_ONRAMP]: {};
} & OrchestrationMapping &
ProviderEventMapping;
[WidgetType.CHECKOUT]: {
} & OrchestrationMapping & ProviderEventMapping,
[CheckoutEventType.CHECKOUT_APP_READY]: {};
[CheckoutEventType.CHECKOUT_APP_EVENT]: Record<string, unknown>;
} & OrchestrationMapping &
ProviderEventMapping;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
import {
useContext, useEffect, useMemo, useReducer,
} from 'react';
import {
Checkout,
CheckoutEventType,
CheckoutWidgetConfiguration,
CheckoutWidgetParams,
IMTBLWidgetEvents,
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';
import {
sendCheckoutEvent,
sendCheckoutReadyEvent,
} from './CheckoutWidgetEvents';
import { EventTargetContext } from '../../context/event-target-context/EventTargetContext';

const widgetEventsList = [
IMTBLWidgetEvents.IMTBL_WIDGETS_PROVIDER,
IMTBLWidgetEvents.IMTBL_CONNECT_WIDGET_EVENT,
IMTBLWidgetEvents.IMTBL_WALLET_WIDGET_EVENT,
IMTBLWidgetEvents.IMTBL_SWAP_WIDGET_EVENT,
IMTBLWidgetEvents.IMTBL_BRIDGE_WIDGET_EVENT,
IMTBLWidgetEvents.IMTBL_ONRAMP_WIDGET_EVENT,
IMTBLWidgetEvents.IMTBL_SALE_WIDGET_EVENT,
IMTBLWidgetEvents.IMTBL_CHECKOUT_WIDGET_EVENT,
];

export type CheckoutWidgetInputs = {
checkout: Checkout;
Expand All @@ -29,25 +43,73 @@ export type CheckoutWidgetInputs = {
export default function CheckoutWidget(props: CheckoutWidgetInputs) {
const { config, checkout, params } = props;
const { environment, publishableKey } = checkout.config;
const {
eventTargetState: { eventTarget },
} = useContext(EventTargetContext);

const [checkoutState, checkoutDispatch] = useReducer(checkoutReducer, initialCheckoutState);
const [targetOrigin, iframeUrl] = useMemo(() => {
if (!publishableKey) return ['', ''];
return getIframeURL(params, config, environment, publishableKey);
}, [params, config, environment, publishableKey]);

const [checkoutState, checkoutDispatch] = useReducer(
checkoutReducer,
initialCheckoutState,
);
const checkoutReducerValues = useMemo(
() => ({ checkoutState, checkoutDispatch }),
[checkoutState, checkoutDispatch],
);

const handleIframeEvents = (
event: MessageEvent<{
type: IMTBLWidgetEvents;
detail: {
type: string;
data: Record<string, unknown>;
};
}>,
) => {
const { type } = event.data;
if (event.origin !== targetOrigin) return;
if (!widgetEventsList.includes(type)) return;

console.log('🍎 Ack 🍎', event.data); // eslint-disable-line

const { detail } = event.data;

switch (type) {
case IMTBLWidgetEvents.IMTBL_CHECKOUT_WIDGET_EVENT:
switch (detail.type) {
case CheckoutEventType.CHECKOUT_APP_READY:
sendCheckoutReadyEvent(eventTarget);
break;
default:
break;
}

break;
default:
sendCheckoutEvent(eventTarget, event.data);
break;
}
};

useEffect(() => {
if (!publishableKey) return;
window.addEventListener('message', handleIframeEvents);
return () => window.removeEventListener('message', handleIframeEvents);
}, []);

const iframeUrl = getIframeURL(params, config, environment, publishableKey);
useEffect(() => {
if (iframeUrl === undefined) return;

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

useEffect(() => {
checkoutDispatch({
Expand All @@ -58,9 +120,13 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) {
});

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

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

checkoutDispatch({
payload: {
Expand All @@ -75,7 +141,6 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) {

return (
<CheckoutContextProvider values={checkoutReducerValues}>
CheckoutWidgetComponent
<CheckoutAppIframe />
</CheckoutContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
WidgetEvent,
CheckoutEventType,
IMTBLWidgetEvents,
WidgetType,
} from '@imtbl/checkout-sdk';

export const sendCheckoutReadyEvent = (eventTarget: Window | EventTarget) => {
const event = new CustomEvent<
WidgetEvent<WidgetType.CHECKOUT, CheckoutEventType.CHECKOUT_APP_READY>
>(IMTBLWidgetEvents.IMTBL_CHECKOUT_WIDGET_EVENT, {
detail: {
type: CheckoutEventType.CHECKOUT_APP_READY,
data: {},
},
});
// eslint-disable-next-line no-console
console.log('checkout app ready ', eventTarget, event);
if (eventTarget !== undefined) eventTarget.dispatchEvent(event);
};

export const sendCheckoutEvent = (
eventTarget: Window | EventTarget,
data: Record<string, unknown>,
) => {
const event = new CustomEvent<
WidgetEvent<WidgetType.CHECKOUT, CheckoutEventType.CHECKOUT_APP_EVENT>
>(IMTBLWidgetEvents.IMTBL_CHECKOUT_WIDGET_EVENT, {
detail: {
type: CheckoutEventType.CHECKOUT_APP_EVENT,
data,
},
});
// eslint-disable-next-line no-console
console.log('checkout app event ', eventTarget, event);
if (eventTarget !== undefined) eventTarget.dispatchEvent(event);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { CHECKOUT_APP_URL } from '../../../lib/constants';
const toQueryString = (params: Record<string, unknown>): string => {
const sanitizedParams = Object.entries(params)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => {
const safeValue = Array.isArray(value) || typeof value === 'object'
? JSON.stringify(value)
: value;

return [key, safeValue];
})
.map(([key, value]) => [key, value] as [string, string]);

return new URLSearchParams(sanitizedParams).toString();
Expand Down Expand Up @@ -121,14 +128,14 @@ export const getIframeURL = (
config: CheckoutWidgetConfiguration,
environment: Environment,
publishableKey: string,
): string => {
) => {
const { flow } = params;

const language = params.language || config.language;

// const baseUrl = 'http://localhost:3001';
const baseUrl = CHECKOUT_APP_URL[environment];
const baseURL = CHECKOUT_APP_URL[environment];
const queryParams = getIframeParams(params, config);

return `${baseUrl}/${publishableKey}/${language}/${flow}?${queryParams}`;
const iframeURL = `${baseURL}/${publishableKey}/${language}/${flow}?${queryParams}`;

return [baseURL, iframeURL];
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Box } from '@biom3/react';
import {
useRef,
} from 'react';
import { useRef } from 'react';
import { CheckoutActions } from '../context/CheckoutContext';
import { useCheckoutContext } from '../context/CheckoutContextProvider';

Expand All @@ -13,9 +11,7 @@ export interface LoadingHandoverProps {
}
export function CheckoutAppIframe() {
const [checkoutState, checkoutDispatch] = useCheckoutContext();
const {
iframeUrl,
} = checkoutState;
const { iframeUrl } = checkoutState;
const iframeRef = useRef<HTMLIFrameElement>(null);

const onIframeLoad = () => {
Expand All @@ -39,10 +35,11 @@ export function CheckoutAppIframe() {
<Box
rc={(
<iframe
id="checkout-app"
title="checkout"
ref={iframeRef}
onLoad={onIframeLoad}
src={iframeUrl}
title="checkout"
onLoad={onIframeLoad}
/>
)}
sx={{
Expand Down
Loading

0 comments on commit 1bd9673

Please sign in to comment.