diff --git a/libs/widget-lib/src/widgetIframeLoading.ts b/libs/widget-lib/src/widgetIframeLoading.ts index e8189300d8..0dc59eefa6 100644 --- a/libs/widget-lib/src/widgetIframeLoading.ts +++ b/libs/widget-lib/src/widgetIframeLoading.ts @@ -1,66 +1,151 @@ +import { WIDGET_IFRAME_ALLOW, WIDGET_IFRAME_REFERRER_POLICY, WIDGET_IFRAME_SANDBOX } from './cowSwapWidget.constants' +import { WidgetMethodsEmit } from './types' +import { widgetIframeTransport } from './widgetIframeTransport' + const IFRAME_LOADING_TIMEOUT = 30_000 // 30 sec +/** After the probe iframe document loads, wait this long for READY before treating the probe as failed. */ +const PROBE_READY_WAIT_TIMEOUT = 10_000 // 10 sec +const WIDGET_TRANSPORT_KEY = 'cowSwapWidget' +const WIDGET_LOAD_RETRY = 'WIDGET_LOAD_RETRY' -const RELOAD_BUTTON_CLASS = 'coWWidgetContentReloadButton' +const RELOAD_BUTTON_CLASS = 'reloadButton' +const RETRY_BUTTON_LABEL = 'Retry' +const RETRY_BUTTON_LOADING_LABEL = 'Loading...' type IframeLoadingState = { cancelWidgetLoading: () => void; onWidgetReady: () => void } +type WindowListener = (event: MessageEvent) => void + +function isWidgetLoadRetryMessage(data: unknown): boolean { + return ( + typeof data === 'object' && + data !== null && + 'key' in data && + data.key === WIDGET_TRANSPORT_KEY && + 'method' in data && + data.method === WIDGET_LOAD_RETRY + ) +} +// eslint-disable-next-line max-lines-per-function export function widgetIframeLoading( iframe: HTMLIFrameElement, onWidgetLoadingError?: () => void, customErrorStyles?: string, ): IframeLoadingState { const originalSrc = iframe.src + const widgetOrigin = new URL(originalSrc).origin let cancelled = false let isLoaded = false - let loadingTimeout: ReturnType | undefined + let loadingTimeoutID = 0 + let tempIframe: HTMLIFrameElement | null = null + let checkIfCowSwapLoadsTimeoutID = 0 + let activeProbeReadyListener: WindowListener | null = null + let isCheckingIfCowSwapLoads = false + + function cleanUpLoadCheck(isChecking = false): void { + clearTimeout(checkIfCowSwapLoadsTimeoutID) + isCheckingIfCowSwapLoads = isChecking - function onIframeLoadingError(): void { - iframe.srcdoc = ERROR_DOCUMENT - onWidgetLoadingError?.() + if (activeProbeReadyListener) { + window.removeEventListener('message', activeProbeReadyListener) + activeProbeReadyListener = null + } + + if (tempIframe) { + tempIframe.remove() + tempIframe = null + } } - function startLoadingTimeout(): void { - clearTimeout(loadingTimeout) + function showErrorDocument(emitEvent = false): void { + if (cancelled || isLoaded) return - loadingTimeout = setTimeout(() => { - if (cancelled || isLoaded) return + clearTimeout(loadingTimeoutID) - onIframeLoadingError() - }, IFRAME_LOADING_TIMEOUT) + iframe.srcdoc = buildErrorDocument(customErrorStyles) + + if (emitEvent && onWidgetLoadingError) onWidgetLoadingError() } - function retryWidgetLoading(): void { + function startLoadingTimeout(): void { + clearTimeout(loadingTimeoutID) + + loadingTimeoutID = window.setTimeout(() => showErrorDocument(true), IFRAME_LOADING_TIMEOUT) + } + + function completeCleanUpLoadCheck(succeeded: boolean): void { + if (!isCheckingIfCowSwapLoads) return + + cleanUpLoadCheck() + if (cancelled || isLoaded) return - // `srcdoc` takes precedence over `src`, so it must be removed to load the widget again - iframe.removeAttribute('srcdoc') - iframe.src = originalSrc + if (succeeded) { + // `srcdoc` takes precedence over `src`, so it must be removed to load the widget again + iframe.removeAttribute('srcdoc') + iframe.src = originalSrc + startLoadingTimeout() + return + } - startLoadingTimeout() + // Reset the error document + showErrorDocument(!iframe.hasAttribute('srcdoc')) } - /** - * Once the error document is loaded, attaches the retry handler and integrator styles. - * The widget itself is cross-origin, so `contentDocument` is null and this is a no-op for it. - */ - function onIframeLoad(): void { - if (cancelled) return - - const errorDocument = iframe.contentDocument - const reloadButton = errorDocument?.querySelector(`.${RELOAD_BUTTON_CLASS}`) + function checkIfCowSwapLoads(): void { + if (cancelled || isLoaded || isCheckingIfCowSwapLoads) return - if (!errorDocument || !reloadButton) return + cleanUpLoadCheck(true) - reloadButton.addEventListener('click', retryWidgetLoading) + tempIframe = document.createElement('iframe') + tempIframe.setAttribute('sandbox', iframe.getAttribute('sandbox') ?? WIDGET_IFRAME_SANDBOX) + tempIframe.referrerPolicy = iframe.referrerPolicy || WIDGET_IFRAME_REFERRER_POLICY + tempIframe.allow = iframe.allow || WIDGET_IFRAME_ALLOW + tempIframe.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden' + document.body.appendChild(tempIframe) - if (customErrorStyles) { - const customStylesEl = errorDocument.createElement('style') + const iframeContentWindow = tempIframe.contentWindow - // textContent is not parsed as HTML, so the styles cannot break out of the ` : '' + + return ` + ${integratorStyles}
- Couldn't load the widget. Please try again later. +

Couldn't load the page. Please, try again later.

- +
+ ` +}