Skip to content

Commit

Permalink
eip6963 proxying connect flow (#2090)
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 21, 2024
1 parent 5fe632a commit 5c06f0e
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export enum PostMessageHandlerEventType {
SYN = 'IMTBL_POST_MESSAGE_SYN',
ACK = 'IMTBL_POST_MESSAGE_ACK',
PROVIDER_RELAY = 'IMTBL_PROVIDER_RELAY',
PROVIDER_UPDATED = 'IMTBL_PROVIDER_UPDATED',
EIP_6963_EVENT = 'IMTBL_EIP_6963_EVENT',
WIDGET_EVENT = 'IMTBL_CHECKOUT_WIDGET_EVENT',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,14 @@ export function CheckoutContextProvider({
}, [iframeContentWindow, checkout, iframeURL]);

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

checkoutDispatch({
payload: {
type: CheckoutActions.SET_PROVIDER_RELAY,
providerRelay: new ProviderRelay(postMessageHandler, provider),
},
});

return () => {
postMessageHandler?.destroy();
};
}, [provider, postMessageHandler]);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useCallback, useEffect } from 'react';
import { EIP6963ProviderDetail, PostMessageHandlerEventType } from '@imtbl/checkout-sdk';
import { useCheckoutContext } from '../context/CheckoutContextProvider';

export function useEip6963Relayer() {
const [checkoutState] = useCheckoutContext();
const { postMessageHandler } = checkoutState;

const onAnnounce = useCallback((event: CustomEvent<EIP6963ProviderDetail>) => {
postMessageHandler?.send(PostMessageHandlerEventType.EIP_6963_EVENT, {
message: 'eip6963:announceProvider',
info: event.detail.info,
});
}, [postMessageHandler]);

useEffect(
() => {
if (!postMessageHandler) return () => { };

window.addEventListener('eip6963:announceProvider', onAnnounce as any);

return () => window.removeEventListener('eip6963:announceProvider', onAnnounce as any);
},
[postMessageHandler, onAnnounce],
);

const onRequest = useCallback((payload: any) => {
if (payload.message !== 'eip6963:requestProvider') return;

window.dispatchEvent(new CustomEvent('eip6963:requestProvider'));
}, [postMessageHandler]);

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

postMessageHandler.subscribe((message) => {
if (message.type === PostMessageHandlerEventType.EIP_6963_EVENT) {
onRequest(message.payload);
}
});
}, [postMessageHandler, onRequest]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useCallback, useEffect } from 'react';
import {
EIP6963ProviderInfo,
PostMessageData,
PostMessageHandlerEventType,
} from '@imtbl/checkout-sdk';
import { Web3Provider } from '@ethersproject/providers';
import { useCheckoutContext } from '../context/CheckoutContextProvider';
import { CheckoutActions } from '../context/CheckoutContext';

// TODO these types should be in sync with Checkout App
type MessageId = number | string | null;

interface JsonRpcRequestMessage<TParams = any> {
type: 'dapp';
jsonrpc: '2.0';
// Optional in the request.
id?: MessageId;
method: string;
params?: TParams;
}

type ProviderRelayPayload = {
jsonRpcRequestMessage: JsonRpcRequestMessage;
eip6963Info: EIP6963ProviderInfo;
};

export function useProviderRelay() {
const [{ checkout, postMessageHandler, provider }, checkoutDispatch] = useCheckoutContext();

/**
* Execute a request using the provider
* and relay the response back using the postMessageHandler
*/
const execute = useCallback(
async (payload: ProviderRelayPayload, executeProvider: Web3Provider) => {
if (!executeProvider?.provider.request) {
throw new Error("Provider only supports 'request' method");
}

if (!postMessageHandler) {
throw new Error(
'Provider can execute request because PostMessageHandler is not initialized',
);
}

const { id, params, method } = payload.jsonRpcRequestMessage;

// Execute the request
const result = await executeProvider.provider.request({ method, params });
const formattedResponse = { id, result, jsonrpc: '2.0' };

// Send the response using the postMessageHandler
postMessageHandler.send(PostMessageHandlerEventType.PROVIDER_RELAY, {
response: formattedResponse,
eip6963Info: payload.eip6963Info,
});
},
[postMessageHandler],
);

/**
* Handle incoming provider relay messages
*/
const onJsonRpcRequestMessage = useCallback(
async ({ type, payload }: PostMessageData) => {
if (!postMessageHandler || !checkout) return;
if (type !== PostMessageHandlerEventType.PROVIDER_RELAY) return;

const providerRelayPayload = payload as ProviderRelayPayload;

const injectedProviders = checkout.getInjectedProviders();
const targetProvider = injectedProviders.find(
(p) => p.info.uuid === providerRelayPayload.eip6963Info.uuid,
);

if (!targetProvider) {
// eslint-disable-next-line no-console
console.error(
'PARENT - requested provider not found',
providerRelayPayload.eip6963Info,
injectedProviders,
);
return;
}

// If provider is not defined, connect the target provider
let currentProvider = provider;
if (!currentProvider) {
const connectResponse = await checkout.connect({
provider: new Web3Provider(targetProvider.provider),
});
currentProvider = connectResponse.provider;
}

// Set provider and execute the request
checkoutDispatch({
payload: {
type: CheckoutActions.SET_PROVIDER,
provider: currentProvider,
},
});

postMessageHandler.send(PostMessageHandlerEventType.PROVIDER_UPDATED, {
eip6963Info: payload.eip6963Info,
});

await execute(providerRelayPayload, currentProvider);
},

[provider, postMessageHandler, checkout, execute],
);

/**
* Subscribe to provider relay messages
*/
useEffect(() => {
if (!postMessageHandler) return;
postMessageHandler.subscribe(onJsonRpcRequestMessage);
}, [provider, postMessageHandler, execute, onJsonRpcRequestMessage]);
}
19 changes: 19 additions & 0 deletions packages/checkout/widgets-lib/src/widgets/checkout/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,22 @@
* The timeout in milliseconds for the iframe to be initialized.
*/
export const IFRAME_INIT_TIMEOUT_MS = 10000;

/**
* The permissions to allow on the iframe.
*/
export const IFRAME_ALLOW_PERMISSIONS = `
accelerometer;
camera;
microphone;
geolocation;
gyroscope;
fullscreen;
autoplay;
encrypted-media;
picture-in-picture;
clipboard-write;
clipboard-read;
`
.trim()
.replace(/\n/g, '');
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,10 @@ import { sendCheckoutEvent } from '../CheckoutWidgetEvents';
import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext';
import { LoadingView } from '../../../views/loading/LoadingView';
import { ErrorView } from '../../../views/error/ErrorView';
import { IFRAME_INIT_TIMEOUT_MS } from '../utils/config';
import { IFRAME_INIT_TIMEOUT_MS, IFRAME_ALLOW_PERMISSIONS } from '../utils/config';
import { useEip6963Relayer } from '../hooks/useEip6963Relayer';
import { useProviderRelay } from '../hooks/useProviderRelay';

const permissions = `
accelerometer;
camera;
microphone;
geolocation;
gyroscope;
fullscreen;
autoplay;
encrypted-media;
picture-in-picture;
clipboard-write;
clipboard-read;
`;
export interface LoadingHandoverProps {
text: string;
duration?: number;
Expand All @@ -48,6 +37,9 @@ export function CheckoutAppIframe() {
checkoutDispatch,
] = useCheckoutContext();

useEip6963Relayer();
useProviderRelay();

const loading = !iframeURL || !iframeContentWindow || !initialised;

const {
Expand All @@ -73,28 +65,29 @@ export function CheckoutAppIframe() {

// subscribe to widget events
postMessageHandler.subscribe(({ type, payload }) => {
if (type !== PostMessageHandlerEventType.WIDGET_EVENT) return;

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

if (type !== PostMessageHandlerEventType.WIDGET_EVENT) return;
// Forward widget events
sendCheckoutEvent(eventTarget, customEvent.detail);

// forward events
sendCheckoutEvent(eventTarget, event.detail);

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

// check if loaded correctly
// Expire iframe initialisation after timeout
// and set a loading error
timeoutRef.current = setTimeout(() => {
if (!initialised) {
setLoadingError(true);
Expand All @@ -103,7 +96,6 @@ export function CheckoutAppIframe() {
}, IFRAME_INIT_TIMEOUT_MS);

return () => {
postMessageHandler.destroy();
clearTimeout(timeoutRef.current!);
};
}, [postMessageHandler]);
Expand Down Expand Up @@ -135,11 +127,11 @@ export function CheckoutAppIframe() {
<iframe
id="checkout-app"
title="checkout"
loading="lazy"
ref={iframeRef}
src={iframeURL}
onLoad={onIframeLoad}
allow={permissions.trim().replace(/\n/g, '')}
loading="lazy"
allow={IFRAME_ALLOW_PERMISSIONS}
/>
)}
sx={{
Expand Down

0 comments on commit 5c06f0e

Please sign in to comment.