import { OkxEventListeners } from './events'; import { IframeEventEmitter } from './IframeEventEmitter'; import { IframeRpcProviderBridge } from './IframeRpcProviderBridge'; import { WindowListener, listenToMessageFromWindow, postMessageToWindow, stopListeningWindowListener, } from './messages'; import { EthereumProvider, IWidgetConfig, IWidgetParams, IWidgetProps, ProviderType, UpdateProviderParams, WidgetMethodsEmit, WidgetMethodsListen, } from './types'; import { checkUrlParam, createWidgetParams, getAddress, getChainId, getWalletInfo, validateWidgetParams, WALLET_TYPE, } from './widgetHelp'; import { updateIframeStyle, DEFAULT_HEIGHT, destroyStyleElement } from './updateIframeStyle'; /** * Callback function signature for updating the Okx Swap Widget. */ export interface OkxSwapWidgetHandler { updateParams: (params: IWidgetParams) => void; updateListeners: (newListeners?: OkxEventListeners) => void; updateProvider: (newProvider: EthereumProvider, providerType: ProviderType) => void; destroy: () => void; iframeWindow: Window; } const getConnectWalletParams = async (provider, providerType, walletName) => { const updateProviderParams = { providerType, walletType: WALLET_TYPE[providerType], chainId: await getChainId(provider, providerType), address: await getAddress(provider, providerType), walletInfo: getWalletInfo(walletName, provider), }; return updateProviderParams; } /** * Generates and injects a Okx Swap Widget into the provided container. * @param container - The HTML element to inject the widget into. * @param params - Parameters for configuring the widget. * @returns A callback function to update the widget with new settings. */ export function createOkxSwapWidget( container: HTMLElement, config: IWidgetConfig, ): OkxSwapWidgetHandler { console.log('createOkxSwapWidget====>', container, config); const { params, provider: providerAux, listeners } = config; let provider = providerAux; // eslint-disable-next-line prefer-const let { data: currentParams, url } = createWidgetParams(params); // 1. Create a brand new iframe const iframe = createIframe(params, url); // 2. Clear the content (delete any previous iFrame if it exists) container.innerHTML = ''; container.appendChild(iframe); const { contentWindow: iframeWindow } = iframe; if (!iframeWindow) { console.error('Iframe does not contain a window', iframe); throw new Error('Iframe does not contain a window!'); } // 3. Send appCode (once the widget posts the ACTIVATE message) const windowListeners: WindowListener[] = []; // todo: check this // windowListeners.push(sendAppCodeOnActivation(iframeWindow, params.appCode)); const updateProviderCallback = async () => { const providerParams = await getConnectWalletParams(provider, currentParams.providerType, currentParams.walletName); console.log('updateProviderEmitEvent====>dex-ready', providerParams, provider); updateProviderEmitEvent(iframeWindow, providerParams, provider); } // 4. Handle widget height changes // todo: check this windowListeners.push( ...listenToHeightChanges(iframe, params.height), listenToDexLoadReady(iframeWindow, currentParams, updateProviderCallback), ); // 5. Intercept deep links navigation in the iframe // windowListeners.push(interceptDeepLinks()); // 6. Handle and forward widget events to the listeners const iFrameOkxEventEmitter = new IframeEventEmitter(window, listeners); // 7. Wire up the iframeRpcProviderBridge with the provider (so RPC calls flow back and forth) let iframeRpcProviderBridge = updateProvider(iframeWindow, null, provider, params.providerType); // 8. Schedule the uploading of the params, once the iframe is loaded iframe.addEventListener('load', () => { console.log('updateParams====>load', provider, currentParams); updateParams(iframeWindow, currentParams); getConnectWalletParams( provider, currentParams.providerType, currentParams.walletName, ).then((updateProviderParams) => { console.log('updateProviderEmitEvent====>load-success', updateProviderParams, provider); updateProviderEmitEvent(iframeWindow, updateProviderParams, provider); }).catch((error) => { console.log('updateProviderEmitEvent====>load-error', provider, error); }) }); // 9. Listen for messages from the iframe // const iframeSafeSdkBridge = new IframeSafeSdkBridge(window, iframeWindow); // 10. Return the handler, so the widget, listeners, and provider can be updated return { updateParams: (newParams: IWidgetParams) => { // width, lang, theme const { width, lang, theme, walletName, extraParams } = newParams; updateIframeStyle(iframe, { width }); const nextParams = { ...params, lang, theme, walletName, extraParams, }; currentParams = createWidgetParams(nextParams).data; validateWidgetParams(currentParams); console.log('updateParams====>updateParamsFunction', provider, params); updateParams(iframeWindow, currentParams); }, updateListeners: (newListeners?: OkxEventListeners) => iFrameOkxEventEmitter.updateListeners(newListeners), updateProvider: async (newProvider, providerType: ProviderType, walletName?: string) => { validateWidgetParams(providerType); iframeRpcProviderBridge?.disconnect(); provider?.removeAllListeners?.(); // iframeSafeSdkBridge.stopListening(); provider = newProvider; const updateProviderParams = await getConnectWalletParams(provider, providerType, walletName); currentParams = { ...currentParams, ...updateProviderParams }; iframeRpcProviderBridge = updateProvider( iframeWindow, iframeRpcProviderBridge, newProvider, providerType, ); console.log('updateProvider====>updateProviderFunction', newProvider, providerType); updateProviderEmitEvent(iframeWindow, updateProviderParams, provider); // updateParams(iframeWindow, currentParams, newProvider); }, destroy: () => { // disconnect rpc provider and unsubscribe to events iframeRpcProviderBridge?.disconnect(); // Stop listening for Okx events iFrameOkxEventEmitter.stopListeningIframe(); // Disconnect all listeners windowListeners.forEach(listener => window.removeEventListener('message', listener)); // Stop listening for SDK messages // iframeSafeSdkBridge.stopListening(); // Destroy the iframe try { container.removeChild(iframe); } catch (e) { console.error('Error removing iframe, maybe iframe is removed', e); } destroyStyleElement(); }, iframeWindow, }; } /** * Update the provider for the iframeRpcProviderBridge. * * It will disconnect from the previous provider and connect to the new one. * * @param iframe iframe window * @param iframeRpcProviderBridge iframe RPC manager * @param newProvider new provider * * @returns the iframeRpcProviderBridge */ function updateProvider( iframe: Window, iframeRpcProviderBridge: IframeRpcProviderBridge | null, newProvider: EthereumProvider, providerType: ProviderType, ): IframeRpcProviderBridge { // Verify the params if (!newProvider) { return; } const Types = Object.values(ProviderType); if (!Types.includes(providerType)) { throw new Error('providerType is required'); } console.log('updateProvider iframeRpcProviderBridge===>', iframeRpcProviderBridge); // TODO: check provider // Disconnect from the previous provider bridge if (iframeRpcProviderBridge) { iframeRpcProviderBridge.disconnect(); } const providerBridge = new IframeRpcProviderBridge(iframe, providerType); // Connect to the new provider if (newProvider) { providerBridge.onConnect(newProvider); } return providerBridge; } /** * Creates an iframe element for the Okx Swap Widget based on provided parameters and settings. * @param params - Parameters for the widget. * @returns The generated HTMLIFrameElement. */ function createIframe(params: IWidgetParams, url: string): HTMLIFrameElement { // todo: check this const { width } = params; const iframe = document.createElement('iframe'); // Check if the URL is valid checkUrlParam(url); console.log('log-url', url); iframe.src = url; // update iframe style updateIframeStyle(iframe, { width }); iframe.scrolling = 'no'; iframe.style.border = 'none'; return iframe; } /** * Updates the Okx Swap Widget based on the new settings provided. * @param params - New params for the widget. * @param contentWindow - Window object of the widget's iframe. */ function updateProviderEmitEvent( contentWindow: Window, params: UpdateProviderParams, provider: EthereumProvider | undefined, ) { const hasProvider = !!provider; console.trace('updateProviderEmitEvent', params, provider); postMessageToWindow( contentWindow, WidgetMethodsListen.UPDATE_PROVIDER, { appParams: params, hasProvider, }, ); } /** * Updates the Okx Swap Widget based on the new settings provided. * @param params - New params for the widget. * @param contentWindow - Window object of the widget's iframe. */ function updateParams(contentWindow: Window, props: IWidgetProps) { console.log('updateParams====>end', props, contentWindow); postMessageToWindow(contentWindow, WidgetMethodsListen.UPDATE_PARAMS, { appParams: props, }); } /** * Listens for iframeHeight emitted by the widget, and applies dynamic height adjustments to the widget's iframe. * * @param iframe - The HTMLIFrameElement of the widget. * @param defaultHeight - Default height for the widget. */ function listenToHeightChanges( iframe: HTMLIFrameElement, defaultHeight = DEFAULT_HEIGHT, ): WindowListener[] { return [ listenToMessageFromWindow(window, WidgetMethodsEmit.UPDATE_HEIGHT, data => { iframe.style.height = data.height ? `${data.height}px` : defaultHeight; iframe.style.minHeight = data.height ? `${data.height}px` : defaultHeight; }), listenToMessageFromWindow(window, WidgetMethodsEmit.SET_FULL_HEIGHT, ({ isUpToSmall }) => { iframe.style.height = isUpToSmall ? defaultHeight : `${document.body.offsetHeight}px`; iframe.style.minHeight = isUpToSmall ? defaultHeight : `${document.body.offsetHeight}px`; }), ]; } function listenToDexLoadReady(iframeWindow: Window, params: IWidgetProps, updateProviderCallback: Function): WindowListener { const listener = listenToMessageFromWindow(window, WidgetMethodsEmit.LOAD_READY, () => { // updateParams again; console.log('updateParams=====>dex-ready', iframeWindow, params); updateParams(iframeWindow, params); // update provider again updateProviderCallback(); stopListeningWindowListener(window, listener); }); return listener; }