Skip to content

Commit

Permalink
Merge branch 'develop' into cow-fi-gtm
Browse files Browse the repository at this point in the history
  • Loading branch information
fairlighteth authored Nov 27, 2024
2 parents f58b9dc + 91bab8d commit 4f839ad
Show file tree
Hide file tree
Showing 20 changed files with 459 additions and 154 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Dispatch, SetStateAction, useEffect } from 'react'
import { Dispatch, SetStateAction, useEffect, useCallback } from 'react'

import { HookDappBase, HookDappType, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib'
import { fetchWithTimeout } from '@cowprotocol/common-utils'
import { HookDappType, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib'
import { useWalletInfo } from '@cowprotocol/wallet'

import { HookDappIframe } from '../../../types/hooks'
import { validateHookDappUrl } from '../../../utils/urlValidation'
import { validateHookDappManifest } from '../../../validateHookDappManifest'
import { ERROR_MESSAGES } from '../constants'

interface ExternalDappLoaderProps {
input: string
Expand All @@ -15,6 +18,21 @@ interface ExternalDappLoaderProps {
setManifestError: Dispatch<SetStateAction<string | React.ReactNode | null>>
}

const TIMEOUT = 5000

// Utility functions for error checking
const isJsonParseError = (error: unknown): boolean => {
return error instanceof Error && error.message?.includes('JSON')
}

const isTimeoutError = (error: unknown): boolean => {
return error instanceof Error && error.name === 'AbortError'
}

const isConnectionError = (error: unknown): boolean => {
return error instanceof TypeError && error.message === 'Failed to fetch'
}

export function ExternalDappLoader({
input,
setLoading,
Expand All @@ -25,27 +43,66 @@ export function ExternalDappLoader({
}: ExternalDappLoaderProps) {
const { chainId } = useWalletInfo()

useEffect(() => {
let isRequestRelevant = true
const setError = useCallback(
(message: string | React.ReactNode) => {
setManifestError(message)
setDappInfo(null)
setLoading(false)
},
[setManifestError, setDappInfo, setLoading],
)

const fetchManifest = useCallback(
async (url: string) => {
if (!url) return

setLoading(true)

try {
const validation = validateHookDappUrl(url)
if (!validation.isValid) {
setError(validation.error)
return
}

setLoading(true)
const trimmedUrl = url.trim()
const manifestUrl = `${trimmedUrl}${trimmedUrl.endsWith('/') ? '' : '/'}manifest.json`

fetch(`${input}/manifest.json`)
.then((res) => res.json())
.then((data) => {
if (!isRequestRelevant) return
const response = await fetchWithTimeout(manifestUrl, {
timeout: TIMEOUT,
timeoutMessage: ERROR_MESSAGES.TIMEOUT,
})
if (!response.ok) {
setError(`Failed to fetch manifest from ${manifestUrl}. Please verify the URL and try again.`)
return
}

const contentType = response.headers.get('content-type')
if (!contentType || !contentType.includes('application/json')) {
setError(
`Invalid content type: Expected JSON but received ${contentType || 'unknown'}. Make sure the URL points to a valid manifest file.`,
)
return
}

const data = await response.json()

if (!data.cow_hook_dapp) {
setError(`Invalid manifest format at ${manifestUrl}: missing cow_hook_dapp property`)
return
}

const dapp = data.cow_hook_dapp as HookDappBase
const dapp = data.cow_hook_dapp

const validationError = validateHookDappManifest(
data.cow_hook_dapp as HookDappBase,
dapp,
chainId,
isPreHook,
walletType === HookDappWalletCompatibility.SMART_CONTRACT,
)

if (validationError) {
setManifestError(validationError)
setError(validationError)
} else {
setManifestError(null)
setDappInfo({
Expand All @@ -54,23 +111,34 @@ export function ExternalDappLoader({
url: input,
})
}
})
.catch((error) => {
if (!isRequestRelevant) return

console.error(error)
setManifestError('Can not fetch the manifest.json')
})
.finally(() => {
if (!isRequestRelevant) return
} catch (error) {
console.error('Hook dapp loading error:', error)

if (isJsonParseError(error)) {
setError(ERROR_MESSAGES.INVALID_MANIFEST_HTML)
} else if (isTimeoutError(error)) {
setError(ERROR_MESSAGES.TIMEOUT)
} else if (isConnectionError(error)) {
setError(ERROR_MESSAGES.CONNECTION_ERROR)
} else {
setError(error instanceof Error ? error.message : ERROR_MESSAGES.GENERIC_MANIFEST_ERROR)
}
} finally {
setLoading(false)
})
}
},
[input, walletType, chainId, isPreHook, setDappInfo, setLoading, setManifestError, setError],
)

useEffect(() => {
if (input) {
fetchManifest(input)
}

return () => {
isRequestRelevant = false
setLoading(false)
}
}, [input, walletType, chainId, isPreHook, setDappInfo, setLoading, setManifestError])
}, [input, fetchManifest, setLoading])

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export const ERROR_MESSAGES = {
INVALID_URL_SPACES: 'Invalid URL: URLs cannot contain spaces',
INVALID_URL_SLASHES: 'Invalid URL: Path contains consecutive forward slashes',
HTTPS_REQUIRED: (
<>
HTTPS is required. Please use <code>https://</code>
</>
),
MANIFEST_PATH: 'Please enter the base URL of your dapp, not the direct manifest.json path',
TIMEOUT: 'Request timed out. Please try again.',
INVALID_MANIFEST: 'Invalid manifest format: Missing "cow_hook_dapp" property in manifest.json',
SMART_CONTRACT_INCOMPATIBLE: 'This hook is not compatible with smart contract wallets. It only supports EOA wallets.',
INVALID_HOOK_ID: 'Invalid hook dapp ID format. The ID must be a 64-character hexadecimal string.',
INVALID_MANIFEST_HTML: (
<>
The URL provided does not return a valid manifest file
<br />
<small>
The server returned an HTML page instead of the expected JSON manifest.json file. Please check if the URL is
correct and points to a valid hook dapp.
</small>
</>
),
CONNECTION_ERROR:
'Could not connect to the provided URL. Please check if the URL is correct and the server is accessible.',
GENERIC_MANIFEST_ERROR: 'Failed to load manifest. Please verify the URL and try again.',
NETWORK_COMPATIBILITY_ERROR: (
chainId: number,
chainLabel: string,
supportedNetworks: { id: number; label: string }[],
) => (
<p>
<b>Network compatibility error</b>
<br />
<br />
This app/hook doesn't support the current network:{' '}
<b>
{chainLabel} (Chain ID: {chainId})
</b>
.
<br />
<br />
Supported networks:
<br />
{supportedNetworks.map(({ id, label }) => (
<>
{label} (Chain ID: {id})
<br />
</>
))}
</p>
),
HOOK_POSITION_MISMATCH: (hookType: 'pre' | 'post') => (
<p>
Hook position mismatch:
<br />
This app/hook can only be used as a <strong>{hookType}-hook</strong>
<br />
and cannot be added as a {hookType === 'pre' ? 'post' : 'pre'}-hook.
</p>
),
MISSING_REQUIRED_FIELDS: (fields: string[]) => `Missing required fields in manifest: ${fields.join(', ')}`,
MANIFEST_NOT_FOUND: 'Invalid URL: No manifest.json file found. Please check the URL and try again.',
INVALID_URL_FORMAT: (error: Error) => (
<>
Invalid URL format
<br />
<small>Technical details: {error.message}</small>
</>
),
}
Loading

0 comments on commit 4f839ad

Please sign in to comment.