diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx index 36386b6953..2b7712546a 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx @@ -1,10 +1,12 @@ -import { useMemo, useReducer } from 'react'; +import { useEffect, useMemo, useReducer } from 'react'; import { Checkout, CheckoutWidgetConfiguration, CheckoutWidgetParams, } from '@imtbl/checkout-sdk'; +import { Web3Provider } from '@ethersproject/providers'; import { + CheckoutActions, checkoutReducer, initialCheckoutState, } from './context/CheckoutContext'; @@ -16,10 +18,13 @@ export type CheckoutWidgetInputs = { checkout: Checkout; params: CheckoutWidgetParams; config: CheckoutWidgetConfiguration; + provider?: Web3Provider }; export default function CheckoutWidget(props: CheckoutWidgetInputs) { - const { config, checkout, params } = props; + const { + config, checkout, params, provider, + } = props; const { environment, publishableKey } = checkout.config; const [, iframeURL] = useMemo(() => { @@ -33,12 +38,23 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) { ); const checkoutReducerValues = useMemo( () => ({ - checkoutState: { ...checkoutState, iframeURL, checkout }, + checkoutState: { + ...checkoutState, iframeURL, checkout, + }, checkoutDispatch, }), [checkoutState, checkoutDispatch, iframeURL, checkout], ); + useEffect(() => { + checkoutDispatch({ + payload: { + type: CheckoutActions.SET_PROVIDER, + provider, + }, + }); + }, [provider]); + return ( diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx index dd640a27fc..3704a70fc2 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx +++ b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx @@ -59,6 +59,7 @@ export class CheckoutWidgetRoot extends Base { } > void; @@ -26,7 +24,6 @@ export const initialCheckoutState: CheckoutState = { iframeURL: undefined, iframeContentWindow: undefined, postMessageHandler: undefined, - providerRelay: undefined, walletProviderInfo: null, walletProviderName: null, sendCloseEvent: () => { }, @@ -47,7 +44,6 @@ type ActionPayload = | SetIframeURLPayload | SetPostMessageHandlerPayload | SetIframeContentWindowPayload - | SetProviderRelayPayload | SetPassportPayload | SetProviderNamePayload | SetSendCloseEventPayload; @@ -58,7 +54,6 @@ export enum CheckoutActions { 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', @@ -71,7 +66,7 @@ export interface SetCheckoutPayload { export interface SetProviderPayload { type: CheckoutActions.SET_PROVIDER; - provider: Web3Provider; + provider: Web3Provider | undefined; } export interface SetIframeURLPayload { @@ -89,11 +84,6 @@ export interface SetPostMessageHandlerPayload { postMessageHandler: PostMessageHandler; } -export interface SetProviderRelayPayload { - type: CheckoutActions.SET_PROVIDER_RELAY; - providerRelay: ProviderRelay; -} - export interface SetPassportPayload { type: CheckoutActions.SET_PASSPORT; passport: Passport; @@ -154,11 +144,6 @@ export const checkoutReducer: Reducer = ( ...state, postMessageHandler: action.payload.postMessageHandler, }; - case CheckoutActions.SET_PROVIDER_RELAY: - return { - ...state, - providerRelay: action.payload.providerRelay, - }; case CheckoutActions.SET_WALLET_PROVIDER_NAME: return { ...state, diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/context/CheckoutContextProvider.tsx b/packages/checkout/widgets-lib/src/widgets/checkout/context/CheckoutContextProvider.tsx index 965326ebcf..c9ceb5abde 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/context/CheckoutContextProvider.tsx +++ b/packages/checkout/widgets-lib/src/widgets/checkout/context/CheckoutContextProvider.tsx @@ -1,4 +1,4 @@ -import { PostMessageHandler } from '@imtbl/checkout-sdk'; +import { PostMessageHandler, PostMessageHandlerEventType } from '@imtbl/checkout-sdk'; import { Dispatch, ReactNode, useContext, useEffect, } from 'react'; @@ -8,7 +8,6 @@ import { CheckoutContext, CheckoutState, } from './CheckoutContext'; -import { ProviderRelay } from './ProviderRelay'; type CheckoutContextProviderProps = { values: { @@ -54,11 +53,9 @@ export function CheckoutContextProvider({ useEffect(() => { if (!provider || !postMessageHandler) return; - checkoutDispatch({ - payload: { - type: CheckoutActions.SET_PROVIDER_RELAY, - providerRelay: new ProviderRelay(postMessageHandler, provider), - }, + postMessageHandler.send(PostMessageHandlerEventType.PROVIDER_UPDATED, { + isMetamask: provider.provider.isMetaMask, + isPassport: (provider.provider as any)?.isPassport, }); }, [provider, postMessageHandler]); diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/context/ProviderRelay.ts b/packages/checkout/widgets-lib/src/widgets/checkout/context/ProviderRelay.ts deleted file mode 100644 index 6d9e09ab00..0000000000 --- a/packages/checkout/widgets-lib/src/widgets/checkout/context/ProviderRelay.ts +++ /dev/null @@ -1,45 +0,0 @@ -// TODO ProviderRelay should live in a different folder - -import { Web3Provider } from '@ethersproject/providers'; -import { - PostMessageData, - PostMessageHandler, - PostMessageHandlerEventType, -} from '@imtbl/checkout-sdk'; - -export class ProviderRelay { - private provider: Web3Provider; - - private postMessageHandler: PostMessageHandler; - - constructor(postMessageHandler: PostMessageHandler, provider: Web3Provider) { - this.provider = provider; - this.postMessageHandler = postMessageHandler; - - postMessageHandler.subscribe(this.onMessage); - } - - private onMessage = ({ type, payload }: PostMessageData) => { - if (type !== PostMessageHandlerEventType.PROVIDER_RELAY) return; - - if (!this.provider.provider.request) { - throw new Error('Provider does not support request method'); - } - - this.provider.provider - .request({ method: payload.method, params: payload.params }) - .then((resp) => { - const formattedResponse = { - id: payload.id, - jsonrpc: '2.0', - result: resp, - }; - - // Relay the response back to proxied provider - this.postMessageHandler.send( - PostMessageHandlerEventType.PROVIDER_RELAY, - formattedResponse, - ); - }); - }; -} diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useEip6963Relayer.ts b/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useEip6963Relayer.ts index c7995640be..81e8fa1b8b 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useEip6963Relayer.ts +++ b/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useEip6963Relayer.ts @@ -1,10 +1,11 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { EIP6963ProviderDetail, PostMessageHandlerEventType } from '@imtbl/checkout-sdk'; import { useCheckoutContext } from '../context/CheckoutContextProvider'; export function useEip6963Relayer() { const [checkoutState] = useCheckoutContext(); const { postMessageHandler } = checkoutState; + const unsubscribePostMessageHandler = useRef<() => void>(); const onAnnounce = useCallback((event: CustomEvent) => { postMessageHandler?.send(PostMessageHandlerEventType.EIP_6963_EVENT, { @@ -32,8 +33,8 @@ export function useEip6963Relayer() { useEffect(() => { if (!postMessageHandler) return; - - postMessageHandler.subscribe((message) => { + unsubscribePostMessageHandler.current?.(); + unsubscribePostMessageHandler.current = postMessageHandler.subscribe((message) => { if (message.type === PostMessageHandlerEventType.EIP_6963_EVENT) { onRequest(message.payload); } diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useProviderRelay.ts b/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useProviderRelay.ts index e2906d1ba7..1e5d33f703 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useProviderRelay.ts +++ b/packages/checkout/widgets-lib/src/widgets/checkout/hooks/useProviderRelay.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { EIP6963ProviderInfo, PostMessageData, @@ -28,6 +28,8 @@ type ProviderRelayPayload = { export function useProviderRelay() { const [{ checkout, postMessageHandler, provider }, checkoutDispatch] = useCheckoutContext(); + const unsubscribePostMessageHandler = useRef<() => void>(); + /** * Execute a request using the provider * and relay the response back using the postMessageHandler @@ -71,14 +73,14 @@ export function useProviderRelay() { const injectedProviders = checkout.getInjectedProviders(); const targetProvider = injectedProviders.find( - (p) => p.info.uuid === providerRelayPayload.eip6963Info.uuid, + (p) => p.info.uuid === providerRelayPayload?.eip6963Info?.uuid, ); - if (!targetProvider) { + if (!targetProvider && providerRelayPayload.eip6963Info !== undefined) { // eslint-disable-next-line no-console console.error( 'PARENT - requested provider not found', - providerRelayPayload.eip6963Info, + providerRelayPayload?.eip6963Info, injectedProviders, ); return; @@ -86,24 +88,24 @@ export function useProviderRelay() { // If provider is not defined, connect the target provider let currentProvider = provider; - if (!currentProvider) { + if (!currentProvider && targetProvider) { 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, - }, - }); + // Set provider and execute the request + checkoutDispatch({ + payload: { + type: CheckoutActions.SET_PROVIDER, + provider: currentProvider, + }, + }); + } - postMessageHandler.send(PostMessageHandlerEventType.PROVIDER_UPDATED, { - eip6963Info: payload.eip6963Info, - }); + if (!currentProvider) { + throw new Error('Provider is not defined'); + } await execute(providerRelayPayload, currentProvider); }, @@ -115,7 +117,13 @@ export function useProviderRelay() { * Subscribe to provider relay messages */ useEffect(() => { + // TODO we need to unsubscribe everywhere if (!postMessageHandler) return; - postMessageHandler.subscribe(onJsonRpcRequestMessage); - }, [provider, postMessageHandler, execute, onJsonRpcRequestMessage]); + unsubscribePostMessageHandler.current?.(); + unsubscribePostMessageHandler.current = postMessageHandler?.subscribe(onJsonRpcRequestMessage); + }, [postMessageHandler, onJsonRpcRequestMessage]); } + +// TODO - +// 1 - commit the unsub part +// 2 - add unsubs to all postMessageHandlers subscriptions diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/views/CheckoutAppIframe.tsx b/packages/checkout/widgets-lib/src/widgets/checkout/views/CheckoutAppIframe.tsx index 078b9110d0..4facab63b7 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/views/CheckoutAppIframe.tsx +++ b/packages/checkout/widgets-lib/src/widgets/checkout/views/CheckoutAppIframe.tsx @@ -4,8 +4,7 @@ import { useContext, useEffect, useRef, useState, } from 'react'; import { - CheckoutEventType, - IMTBLWidgetEvents, + CheckoutEventType, IMTBLWidgetEvents, PostMessageHandlerEventType, WidgetEventData, WidgetType, @@ -16,7 +15,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, IFRAME_ALLOW_PERMISSIONS } from '../utils/config'; +import { + IFRAME_INIT_TIMEOUT_MS, + IFRAME_ALLOW_PERMISSIONS, +} from '../utils/config'; import { useEip6963Relayer } from '../hooks/useEip6963Relayer'; import { useProviderRelay } from '../hooks/useProviderRelay'; @@ -33,9 +35,12 @@ export function CheckoutAppIframe() { const [loadingError, setLoadingError] = useState(false); const [initialised, setInitialised] = useState(false); const [ - { iframeURL, postMessageHandler, iframeContentWindow }, + { + iframeURL, postMessageHandler, iframeContentWindow, + }, checkoutDispatch, ] = useCheckoutContext(); + const unsubscribePostMessageHandler = useRef<() => void>(); useEip6963Relayer(); useProviderRelay(); @@ -62,9 +67,11 @@ export function CheckoutAppIframe() { useEffect(() => { if (!postMessageHandler) return undefined; + unsubscribePostMessageHandler.current?.(); // subscribe to widget events - postMessageHandler.subscribe(({ type, payload }) => { + // TODO: Move to its own hook + unsubscribePostMessageHandler.current = postMessageHandler.subscribe(({ type, payload }) => { if (type !== PostMessageHandlerEventType.WIDGET_EVENT) return; // FIXME: improve typing @@ -76,6 +83,17 @@ export function CheckoutAppIframe() { }; } = 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); @@ -98,7 +116,7 @@ export function CheckoutAppIframe() { return () => { clearTimeout(timeoutRef.current!); }; - }, [postMessageHandler]); + }, [postMessageHandler, checkoutDispatch]); if (loadingError) { return ( diff --git a/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx b/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx index 1e3df64669..e25332c2d7 100644 --- a/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx +++ b/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Checkout, CheckoutFlowType, @@ -16,8 +16,13 @@ import { Environment } from "@imtbl/config"; import { WidgetsFactory } from "@imtbl/checkout-widgets"; import { passport } from "../marketplace-orchestrator/passport"; import { Box } from "@biom3/react"; +import { Web3Provider } from "@ethersproject/providers"; function CheckoutUI() { + const [web3Provider, setWeb3Provider] = useState( + undefined + ); + const checkout = useMemo( () => new Checkout({ @@ -30,37 +35,87 @@ function CheckoutUI() { ); const factory = useMemo( () => - new WidgetsFactory(checkout, { theme: WidgetTheme.DARK, language: "en" }), - [checkout] + web3Provider + ? new WidgetsFactory(checkout, { + theme: WidgetTheme.DARK, + language: "en", + }) + : undefined, + [checkout, web3Provider] ); const checkoutWidget = useMemo( () => - factory.create(WidgetType.CHECKOUT, { - config: { - theme: WidgetTheme.LIGHT, - wallet: { showNetworkMenu: false }, - sale: { - hideExcludedPaymentTypes: true, - }, - }, - }), - [checkout] + factory + ? factory.create(WidgetType.CHECKOUT, { + config: { + theme: WidgetTheme.LIGHT, + wallet: { showNetworkMenu: false }, + sale: { + hideExcludedPaymentTypes: true, + }, + }, + provider: web3Provider, + }) + : undefined, + [factory] ); + // Case 1: with MM + useEffect(() => { + (async () => { + const { provider: newProvider } = await checkout.createProvider({ + walletProviderName: WalletProviderName.METAMASK, + }); + + await checkout.connect({ + provider: newProvider, + }); + + const { isConnected } = await checkout.checkIsWalletConnected({ + provider: newProvider, + }); + + if (isConnected) { + setWeb3Provider(newProvider); + } + })(); + }, []); + + // Case 1: with Passport + // useEffect(() => { + // const passportProvider = passport.connectEvm(); + // setWeb3Provider(new Web3Provider(passportProvider)); + // }, []); + + // Case 2: with MM + // useEffect(() => { + // (async () => { + // const { provider: newProvider } = await checkout.createProvider({ + // walletProviderName: WalletProviderName.METAMASK, + // }); + + // setWeb3Provider(newProvider); + + // await checkout.connect({ + // provider: newProvider, + // }); + // })(); + // }, []); + const unmount = () => { - checkoutWidget.unmount(); + checkoutWidget?.unmount(); }; const update = (theme: WidgetTheme) => { - checkoutWidget.update({ config: { theme } }); + checkoutWidget?.update({ config: { theme } }); }; useEffect(() => { - passport.connectEvm(); - checkoutWidget.mount("checkout", { + if (!checkoutWidget) return; + checkoutWidget?.mount("checkout", { flow: CheckoutFlowType.WALLET, }); - }, []); + }, [checkoutWidget]); useEffect(() => { if (!checkoutWidget) return; @@ -77,39 +132,43 @@ function CheckoutUI() { console.log("----------> CLOSE", data); }); - checkoutWidget.addListener(CheckoutEventType.SUCCESS, (data) => { + checkoutWidget.addListener(CheckoutEventType.SUCCESS, (payload) => { + if (payload.flow === CheckoutFlowType.CONNECT) { + setWeb3Provider(payload.data.provider); + } + if ( - data.flow === CheckoutFlowType.SALE && - data.type === CheckoutSuccessEventType.SALE_SUCCESS + payload.flow === CheckoutFlowType.SALE && + payload.type === CheckoutSuccessEventType.SALE_SUCCESS ) { - console.log("----------> SUCCESS SALE_SUCESS", data); + console.log("----------> SUCCESS SALE_SUCESS", payload); } if ( - data.flow === CheckoutFlowType.SALE && - data.type === CheckoutSuccessEventType.SALE_TRANSACTION_SUCCESS + payload.flow === CheckoutFlowType.SALE && + payload.type === CheckoutSuccessEventType.SALE_TRANSACTION_SUCCESS ) { - console.log("----------> SUCCESS SALE_TRANSACTION_SUCCESS", data); + console.log("----------> SUCCESS SALE_TRANSACTION_SUCCESS", payload); } - if (data.flow === CheckoutFlowType.ONRAMP) { - console.log("----------> SUCCESS ONRAMP", data); + if (payload.flow === CheckoutFlowType.ONRAMP) { + console.log("----------> SUCCESS ONRAMP", payload); } if ( - data.flow === CheckoutFlowType.BRIDGE && - data.type === CheckoutSuccessEventType.BRIDGE_SUCCESS + payload.flow === CheckoutFlowType.BRIDGE && + payload.type === CheckoutSuccessEventType.BRIDGE_SUCCESS ) { - console.log("----------> SUCCESS BRIDGE_SUCCESS", data); + console.log("----------> SUCCESS BRIDGE_SUCCESS", payload); } if ( - data.flow === CheckoutFlowType.BRIDGE && - data.type === CheckoutSuccessEventType.BRIDGE_CLAIM_WITHDRAWAL_SUCCESS + payload.flow === CheckoutFlowType.BRIDGE && + payload.type === CheckoutSuccessEventType.BRIDGE_CLAIM_WITHDRAWAL_SUCCESS ) { console.log( "----------> SUCCESS BRIDGE_CLAIM_WITHDRAWAL_SUCCESS", - data.data + payload.data ); } - console.log("----------> SUCCESS", data); + console.log("----------> SUCCESS", payload); }); checkoutWidget.addListener(CheckoutEventType.FAILURE, (data) => { @@ -179,7 +238,7 @@ function CheckoutUI() { >