Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/cowswap-frontend/src/locales/en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -6039,6 +6039,10 @@ msgstr "The address that will receive the tokens on the destination chain."
msgid "Creation"
msgstr "Creation"

#: apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx
msgid "An error occurred while loading the hook"
msgstr "An error occurred while loading the hook"

#: apps/cowswap-frontend/src/modules/hooksStore/pure/HookTooltip/index.tsx
msgid "AFTER"
msgstr "AFTER"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactNode, useLayoutEffect, useRef, useState } from 'react'

import { isHttpsUrlString } from '@cowprotocol/common-utils'
import { CoWHookDappEvents, hookDappIframeTransport } from '@cowprotocol/hook-dapp-lib'
import { EthereumProvider, IframeRpcProviderBridge } from '@cowprotocol/iframe-transport'
import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui'
Expand Down Expand Up @@ -50,10 +51,18 @@ const StyledProductLogo = styled(ProductLogo)`
}
`

interface IframeState {
isLoading: boolean
isActive: boolean
hasError: boolean
}

interface IframeDappContainerProps {
dapp: HookDappIframe
context: HookDappContextType
}

// eslint-disable-next-line max-lines-per-function
export function IframeDappContainer({ dapp, context }: IframeDappContainerProps): ReactNode {
const iframeRef = useRef<HTMLIFrameElement | null>(null)
const bridgeRef = useRef<IframeRpcProviderBridge | null>(null)
Expand All @@ -62,9 +71,17 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps)
const setSellTokenRef = useRef(context.setSellToken)
const setBuyTokenRef = useRef(context.setBuyToken)

const [isIframeActive, setIsIframeActive] = useState<boolean>(false)
const [isLoading, setIsLoading] = useState(true)
const [iframeState, setIframeState] = useState<IframeState>({
isLoading: true,
isActive: false,
hasError: false,
})

const dappOrigin = getDappOrigin(dapp.url)
const isHttpsUrl = dappOrigin && isHttpsUrlString(dappOrigin)

const { isLoading, isActive } = iframeState
const hasError = iframeState.hasError || !dappOrigin || !isHttpsUrl

// TODO M-6 COW-573
// This flow will be reviewed and updated later, to include a wagmi alternative
Expand All @@ -79,22 +96,37 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps)
// eslint-disable-next-line react-hooks/refs
setBuyTokenRef.current = context.setBuyToken

// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const handleIframeLoad = () => {
setIsLoading(false)
const handleIframeLoad = (): void => {
setIframeState({
isLoading: false,
isActive: false,
hasError: false,
})
}

const handleIframeError = (): void => {
setIframeState({
isLoading: false,
isActive: false,
hasError: true,
})
}

useLayoutEffect(() => {
const iframeWindow = iframeRef.current?.contentWindow

if (!iframeWindow || !dappOrigin) return
if (!iframeWindow || !dappOrigin || !isHttpsUrlString(dappOrigin)) return

const listeners = [
hookDappIframeTransport.listenToMessageFromWindow(
window,
CoWHookDappEvents.ACTIVATE,
() => setIsIframeActive(true),
() =>
setIframeState({
isLoading: false,
isActive: true,
hasError: false,
}),
dappOrigin,
),
]
Expand Down Expand Up @@ -143,7 +175,7 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps)
useLayoutEffect(() => {
const iframeWindow = iframeRef.current?.contentWindow

if (!iframeWindow || !isIframeActive || !dappOrigin) return
if (!iframeWindow || !isActive || !dappOrigin || !isHttpsUrlString(dappOrigin)) return

// Omit unnecessary parameter
const { addHook: _, editHook: _1, signer: _2, setSellToken: _3, setBuyToken: _4, ...iframeContext } = context
Expand All @@ -154,23 +186,40 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps)
iframeContext,
dappOrigin,
)
}, [context, dappOrigin, isIframeActive])
}, [context, dappOrigin, isActive])

let overlayNode: ReactNode | null = null

if (hasError) {
overlayNode = (
<LoadingWrapper>
<StyledProductLogo variant={ProductVariant.CowSwap} logoIconOnly height={56} />
<LoadingText>
<Trans>An error occurred while loading the hook</Trans>
</LoadingText>
</LoadingWrapper>
)
} else if (isLoading) {
overlayNode = (
<LoadingWrapper>
<StyledProductLogo variant={ProductVariant.CowSwap} logoIconOnly height={56} />
<LoadingText>
<Trans>Loading hook...</Trans>
</LoadingText>
</LoadingWrapper>
)
}

return (
<>
{isLoading && (
<LoadingWrapper>
<StyledProductLogo variant={ProductVariant.CowSwap} logoIconOnly height={56} />
<LoadingText>
<Trans>Loading hook...</Trans>
</LoadingText>
</LoadingWrapper>
)}
{overlayNode}
<Iframe
ref={iframeRef}
src={dapp.url}
allow="clipboard-read; clipboard-write"
onLoad={handleIframeLoad}
onAbort={handleIframeError}
onError={handleIframeError}
$isLoading={isLoading}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isInjectedWidget } from '@cowprotocol/common-utils'
import { isInjectedWidget, getNullableParentOrigin, UrlString } from '@cowprotocol/common-utils'
import { jotaiStore } from '@cowprotocol/core'
import {
WidgetHookEvents,
Expand All @@ -11,13 +11,12 @@ import {
import ms from 'ms.macro'

import { injectedWidgetHooksEnabledAtom } from '../state/injectedWidgetHooksEnabledAtom'
import { getParentOrigin } from '../utils/getParentOrigin.utils'

const callsRegistry = new Map<string, (result: boolean) => void>()
const HOOK_RESPONSE_TIMEOUT_MS = ms`2m`
let isListenerRegistered = false

function ensureListenerRegistered(parentOrigin: string): void {
function ensureListenerRegistered(parentOrigin: UrlString): void {
if (isListenerRegistered) {
return
}
Expand Down Expand Up @@ -66,7 +65,7 @@ export function callWidgetHook<T extends WidgetHookEvents>(
resolve(result)
})

const parentOrigin = getParentOrigin()
const parentOrigin = getNullableParentOrigin()

if (!parentOrigin) {
callsRegistry.delete(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import {
CowWidgetEventPayloadMap,
CowWidgetEvents,
} from '@cowprotocol/events'
import { getNullableParentOrigin } from '@cowprotocol/iframe-transport'
import { widgetIframeTransport, WidgetMethodsEmit } from '@cowprotocol/widget-lib'

import { WIDGET_EVENT_EMITTER } from 'widgetEventEmitter'

import { getParentOrigin } from '../utils/getParentOrigin.utils'

const ALL_EVENTS = Object.values(CowWidgetEvents)

export function CowEventsUpdater(): null {
Expand Down Expand Up @@ -41,7 +40,7 @@ function forwardEventToIframe<T extends CowWidgetEvents>(
event: CowWidgetEvents,
payload: CowWidgetEventPayloadMap[T],
): void {
const parentOrigin = getParentOrigin()
const parentOrigin = getNullableParentOrigin()

if (!parentOrigin) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import { useAtomValue } from 'jotai'
import { useLayoutEffect, useRef } from 'react'

import { isIframe, isInjectedWidget } from '@cowprotocol/common-utils'
import { getNullableParentOrigin } from '@cowprotocol/iframe-transport'
import { MEDIA_WIDTHS } from '@cowprotocol/ui'
import { widgetIframeTransport, WidgetMethodsEmit } from '@cowprotocol/widget-lib'

import { openModalState } from 'common/state/openModalState'

import { getParentOrigin } from '../utils/getParentOrigin.utils'

export function IframeResizer(): null {
const isModalOpen = useAtomValue(openModalState)
const previousHeightRef = useRef(0)

useLayoutEffect(() => {
if (!isIframe() || !isInjectedWidget()) return
const parentOrigin = getParentOrigin()

const parentOrigin = getNullableParentOrigin()

if (!parentOrigin) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useAtom, useSetAtom } from 'jotai'
import { ReactNode, useEffect, useRef } from 'react'

import { usePrevious } from '@cowprotocol/common-hooks'
import { deepEqual } from '@cowprotocol/common-utils'
import { deepEqual, getNullableParentOrigin } from '@cowprotocol/common-utils'
import {
UpdateParamsPayload,
widgetIframeTransport,
Expand All @@ -20,7 +20,6 @@ import { WidgetParamsErrorsScreen } from '../pure/WidgetParamsErrorsScreen'
import { injectedWidgetHooksEnabledAtom } from '../state/injectedWidgetHooksEnabledAtom'
import { injectedWidgetMetaDataAtom } from '../state/injectedWidgetMetaDataAtom'
import { injectedWidgetParamsAtom } from '../state/injectedWidgetParamsAtom'
import { getParentOrigin } from '../utils/getParentOrigin.utils'
import { validateWidgetParams } from '../utils/validateWidgetParams'
import {
cacheWidgetMessage,
Expand All @@ -32,7 +31,7 @@ import {
const isInIframe = window.parent !== window.self

const parent = window.parent
const parentOrigin = getParentOrigin()
const parentOrigin = getNullableParentOrigin()

if (!parent || !isInIframe || !parentOrigin) return

Expand Down Expand Up @@ -102,7 +101,7 @@ export function InjectedWidgetUpdater(): ReactNode {
// Stop listening of message outside of React
window.removeEventListener('message', cacheWidgetMessage)

const parentOrigin = getParentOrigin()
const parentOrigin = getNullableParentOrigin()

if (!parentOrigin) {
return
Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions libs/common-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ export * from './i18n'
export * from './cowProtocolContracts'
export * from './errors'
export * from './logger'
export * from './url'
export { getAvailableDestinationChains } from './getAvailableDestinationChains'
69 changes: 69 additions & 0 deletions libs/common-utils/src/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export type HttpsUrlString = `https://${string}`

export type HttpUrlString = `http://${string}`

export type UrlString = HttpsUrlString | HttpUrlString

export function isHttpsUrlString(urlString: string): urlString is HttpsUrlString {
const url = new URL(urlString)

return urlString.startsWith('https://') || url.hostname === 'localhost' || url.hostname === '127.0.0.1'
}

export function assertHttpsUrlString(urlString: string): asserts urlString is HttpsUrlString {
if (!isHttpsUrlString(urlString)) {
throw new Error('URL is not a valid HTTPS URL')
}
}

export function getNullableParentOrigin(): UrlString | null {
return getAncestorOrigin() || getReferrerOrigin() || getParentLocationOrigin() || null
}

export function getParentOriginOrThrow(): UrlString {
const parentOrigin = getNullableParentOrigin()

if (!parentOrigin) {
throw new Error('Parent origin not found')
}

return parentOrigin
}

function getAncestorOrigin(): UrlString | undefined {
if (typeof window === 'undefined') {
return undefined
}

const ancestorOrigins = window.location.ancestorOrigins

if (!ancestorOrigins || ancestorOrigins.length === 0) {
return undefined
}

return ancestorOrigins[0] as UrlString
}

function getReferrerOrigin(): UrlString | undefined {
if (typeof document === 'undefined' || !document.referrer) {
return undefined
}

try {
return new URL(document.referrer).origin as UrlString
} catch {
return undefined
}
}

function getParentLocationOrigin(): UrlString | undefined {
if (typeof window === 'undefined' || !window.parent || window.parent === window) {
return undefined
}

try {
return window.parent.location.origin as UrlString
} catch {
return undefined
}
}
Loading
Loading