Skip to content

Commit

Permalink
feat(hooks-store): import custom hook-dapps (#4910)
Browse files Browse the repository at this point in the history
* feat(hooks-store): add order params and signer to hook dapp context

* feat(hooks-store): setup blank app for iframe hook

* refactor: generalise CowEventEmitter in order to reuse it in hook dapps

* feat: iframe transport library

* feat: use iframe-transport lib for widget

* feat: hook-dapp-lib for hook-dapps in iframe

* feat(hooks-store): add omnibridge hook-dapp via iframe

* chore: specify key

* docs: doc @cowprotocol/hook-dapp-lib

* chore: revert ff

* refactor: simplify hooks store state

* chore: fix build

* chore: fix build

* chore: remove boilerplate

* Update libs/hook-dapp-lib/README.md

Co-authored-by: Leandro <[email protected]>

* chore: fix package json

* chore: remove wrong permit check

* feat: add isSmartContract to hook dapp context

* refactor: fix WidgetEthereumProvider deps

* chore: pull develop

* chore: hook dapp lib deps

* chore: fix permit hook adding

* feat(hooks-store): callbacks to changes sell buy tokens

* feat(hooks-store): import external hook-dapps

* fix: rpc bridge duplication

* chore: make conditions optional

* chore: polish UI

* chore: fix lint

* refactor(events): generalise SimpleCowEventEmitter for reusing porposes (#4886)

* refactor: generalise CowEventEmitter in order to reuse it in hook dapps

* feat: library iframe-transport to communicate between app and iframe (#4887)

* feat: iframe transport library

* chore: specify key

* feat(widget): integrate iframe-transport lib in widget (#4888)

* feat: use iframe-transport lib for widget

* feat(hooks-store): library for building external hook-dapps (#4889)

* feat: hook-dapp-lib for hook-dapps in iframe

* docs: doc @cowprotocol/hook-dapp-lib

* Update libs/hook-dapp-lib/README.md

Co-authored-by: Leandro <[email protected]>

* chore: fix package json

* feat(hooks-store): integrate an iframe hook-dapp for example (#4890)

* feat(hooks-store): add omnibridge hook-dapp via iframe

* chore: revert ff

* refactor: simplify hooks store state

* chore: fix build

* chore: fix build

* chore: remove wrong permit check

* feat: add isSmartContract to hook dapp context

* refactor: fix WidgetEthereumProvider deps

* chore: pull develop

* chore: hook dapp lib deps

* chore: fix permit hook adding

* feat(hooks-store): callbacks to changes sell buy tokens

* chore: fix lint

* chore: fix merge

* chore: up version

* chore: remove unused hook

* chore: fix build

* fix(hooks-store): use recipient override only in hooks page

* chore: trim hook url

* chore: fix build

* feat(hooks-store): style custom hook-dapps (#4912)

* feat: styling tabs + counter

* feat: styling tabs + counter

* feat: add inline search

* chore: fix build

* feat: style inline search and custom hooks add

* feat: add simulatin styling

* feat: merge conflic fixes

* feat: handle walletcompat undefined

* fix: missing property

* refactor: simlify hooks-store code

* refactor: rename externalHooks to customHooks

* refactor: separate pre/post custom hooks

* chore: fix UI

* chore: fix tenderly simulation

* chore: fix styles

* chore: fix styles

* chore: fix recipient resetting

* chore: fix hooks-store ui issues

* feat(hook-dapp-lib): add isDarkMode parameter to context

* fix(hooks-store): always display SwapWidget to keep updaters active

* chore: fix style

* chore: fix style

---------

Co-authored-by: Alexandr Kazachenko <[email protected]>

---------

Co-authored-by: Leandro <[email protected]>
Co-authored-by: fairlight <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2024
1 parent 897ce91 commit 5847017
Show file tree
Hide file tree
Showing 49 changed files with 1,129 additions and 220 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ReactElement } from 'react'

import { Command } from '@cowprotocol/types'

import { AlertTriangle } from 'react-feather'

import * as styledEl from './styled'

interface ExternalSourceAlertProps {
children: ReactElement
title: ReactElement | string
onChange: Command
}
export function ExternalSourceAlert({ onChange, title, children }: ExternalSourceAlertProps) {
return (
<styledEl.Contents>
<AlertTriangle size={48} strokeWidth={1} />
<h3>{title}</h3>
{children}

<styledEl.AcceptanceBox>
<input type="checkbox" onChange={onChange} />
<span>I understand</span>
</styledEl.AcceptanceBox>
</styledEl.Contents>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { UI } from '@cowprotocol/ui'

import styled from 'styled-components/macro'

export const Contents = styled.div`
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 5px;
padding: 20px;
margin: 0;
border-radius: 20px;
color: var(${UI.COLOR_DANGER_TEXT});
background: var(${UI.COLOR_DANGER_BG});
h3 {
font-size: 24px;
text-align: center;
margin: 18px 0;
font-weight: bold;
}
> svg > path,
> svg > line {
stroke: var(${UI.COLOR_DANGER_TEXT});
stroke-width: 2px;
}
`

export const AcceptanceBox = styled.label`
display: flex;
gap: 6px;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 24px auto 0;
padding: 24px 0;
border-top: 1px solid var(${UI.COLOR_DANGER_TEXT});
width: 100%;
font-size: 18px;
font-weight: bold;
transition: all var(${UI.ANIMATION_DURATION}) ease-in-out;
border-radius: 0;
&:hover {
background: var(${UI.COLOR_DANGER_BG});
border-radius: 12px;
}
> input {
--size: 18px;
width: var(--size);
height: var(--size);
border-radius: 4px;
border: 1px solid var(${UI.COLOR_DANGER_TEXT});
background: var(${UI.COLOR_PAPER});
appearance: none;
position: relative;
&:checked {
background: var(${UI.COLOR_DANGER_TEXT});
&::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(${UI.COLOR_PAPER});
font-size: 14px;
font-weight: bold;
}
}
}
`
4 changes: 2 additions & 2 deletions apps/cowswap-frontend/src/common/pure/NewModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ export interface NewModalProps {
}

export function NewModal({
maxWidth = 450,
minHeight = 350,
maxWidth,
minHeight,
contentPadding,
justifyContent,
modalMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Command } from '@cowprotocol/types'
import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet'
import { useWalletProvider } from '@cowprotocol/wallet-provider'

import { useIsDarkMode } from 'legacy/state/user/hooks'

import { useTradeState, useTradeNavigate } from 'modules/trade'

import { useAddHook } from '../../hooks/useAddHook'
Expand Down Expand Up @@ -32,6 +34,8 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho
const provider = useWalletProvider()
const tradeState = useTradeState()
const tradeNavigate = useTradeNavigate()
const isDarkMode = useIsDarkMode()

const { inputCurrencyId = null, outputCurrencyId = null } = tradeState.state || {}
const signer = useMemo(() => provider?.getSigner(), [provider])

Expand All @@ -44,6 +48,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho
signer,
isSmartContract,
isPreHook,
isDarkMode,
editHook(...args) {
editHook(...args)
onDismiss()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { useCallback, useEffect, useMemo, useState } from 'react'

import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg'
import { Command } from '@cowprotocol/types'
import { useWalletInfo } from '@cowprotocol/wallet'
import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui'
import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet'

import { NewModal } from 'common/pure/NewModal'

import { HookDappsList, Wrapper } from './styled'
import { EmptyList, HookDappsList, Wrapper } from './styled'

import { POST_HOOK_REGISTRY, PRE_HOOK_REGISTRY } from '../../hookRegistry'
import { useAddCustomHookDapp } from '../../hooks/useAddCustomHookDapp'
import { useCustomHookDapps } from '../../hooks/useCustomHookDapps'
import { useHookById } from '../../hooks/useHookById'
import { useRemoveCustomHookDapp } from '../../hooks/useRemoveCustomHookDapp'
import { AddCustomHookForm } from '../../pure/AddCustomHookForm'
import { HookDappDetails } from '../../pure/HookDappDetails'
import { HookDetailHeader } from '../../pure/HookDetailHeader'
import { HookListItem } from '../../pure/HookListItem'
import { HookDapp } from '../../types/hooks'
import { HookListsTabs } from '../../pure/HookListsTabs'
import { HookDapp, HookDappIframe } from '../../types/hooks'
import { findHookDappById, isHookDappIframe } from '../../utils'
import { HookDappContainer } from '../HookDappContainer'
import { HookSearchInput } from '../HookSearchInput'

interface HookStoreModal {
onDismiss: Command
isPreHook: boolean
Expand All @@ -25,16 +35,55 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore
const [selectedDapp, setSelectedDapp] = useState<HookDapp | null>(null)
const [dappDetails, setDappDetails] = useState<HookDapp | null>(null)

const [isAllHooksTab, setIsAllHooksTab] = useState<boolean>(true)

const isSmartContractWallet = useIsSmartContractWallet()
const addCustomHookDapp = useAddCustomHookDapp(isPreHook)
const removeCustomHookDapp = useRemoveCustomHookDapp()
const customHookDapps = useCustomHookDapps(isPreHook)
const hookToEditDetails = useHookById(hookToEdit, isPreHook)
const dapps = isPreHook ? PRE_HOOK_REGISTRY[chainId] : POST_HOOK_REGISTRY[chainId]

// State for Search Input
const [searchQuery, setSearchQuery] = useState<string>('')

// Clear search input handler
const handleClearSearch = useCallback(() => {
setSearchQuery('')
}, [])

const internalHookDapps = useMemo(() => {
return (isPreHook ? PRE_HOOK_REGISTRY[chainId] : POST_HOOK_REGISTRY[chainId]) || []
}, [isPreHook, chainId])

const currentDapps = useMemo(() => {
return isAllHooksTab ? internalHookDapps.concat(customHookDapps) : customHookDapps
}, [isAllHooksTab, internalHookDapps, customHookDapps])

// Compute filteredDapps based on searchQuery
const filteredDapps = useMemo(() => {
if (!searchQuery) return currentDapps

const lowerQuery = searchQuery.toLowerCase()

return currentDapps.filter((dapp) => {
const name = dapp.name?.toLowerCase() || ''
const description = dapp.descriptionShort?.toLowerCase() || ''

return name.includes(lowerQuery) || description.includes(lowerQuery)
})
}, [currentDapps, searchQuery])

const customHooksCount = customHookDapps.length
const allHooksCount = internalHookDapps.length + customHooksCount

// Compute title based on selected dapp or details
const title = useMemo(() => {
if (selectedDapp) return selectedDapp.name
if (dappDetails) return 'Hook description'

return 'Hook Store'
}, [selectedDapp, dappDetails])

// Handle modal dismiss
const onDismissModal = useCallback(() => {
if (hookToEdit) {
setSelectedDapp(null)
Expand All @@ -51,13 +100,79 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore
}
}, [onDismiss, selectedDapp, dappDetails, hookToEdit])

// Handle hookToEditDetails
useEffect(() => {
if (!hookToEditDetails) {
setSelectedDapp(null)
} else {
setSelectedDapp(dapps.find((i) => i.name === hookToEditDetails.dappName) || null)
setSelectedDapp(findHookDappById(currentDapps, hookToEditDetails) || null)
}
}, [hookToEditDetails, dapps])
}, [hookToEditDetails, currentDapps])

// Reset dappDetails when tab changes
useEffect(() => {
setDappDetails(null)
}, [isAllHooksTab])

// Handle add custom hook button
const handleAddCustomHook = useCallback(() => {
setIsAllHooksTab(false)
}, [setIsAllHooksTab])

// Determine the message for EmptyList based on the active tab and search query
const emptyListMessage = useMemo(() => {
if (isAllHooksTab) {
return searchQuery ? 'No hooks match your search.' : 'No hooks available.'
} else {
return "You haven't added any custom hooks yet. Add a custom hook to get started."
}
}, [isAllHooksTab, searchQuery])

const DappsListContent = (
<>
{isAllHooksTab && (
<DismissableInlineBanner
orientation={BannerOrientation.Horizontal}
customIcon={ICON_HOOK}
iconSize={36}
bannerId="hooks-store-banner-tradeContainer-customHooks"
margin="0 10px 10px"
width="auto"
>
<p>
Can't find a hook that you like?{' '}
<span onClick={handleAddCustomHook} style={{ cursor: 'pointer', textDecoration: 'underline' }}>
Add a custom hook
</span>
</p>
</DismissableInlineBanner>
)}

<HookSearchInput
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value?.trim())}
placeholder="Search hooks by title or description"
ariaLabel="Search hooks"
onClear={handleClearSearch}
/>

{filteredDapps.length > 0 ? (
<HookDappsList>
{filteredDapps.map((dapp) => (
<HookListItem
key={isHookDappIframe(dapp) ? dapp.url : dapp.name}
dapp={dapp}
onRemove={isAllHooksTab ? undefined : () => removeCustomHookDapp(dapp as HookDappIframe)}
onSelect={() => setSelectedDapp(dapp)}
onOpenDetails={() => setDappDetails(dapp)}
/>
))}
</HookDappsList>
) : (
<EmptyList>{emptyListMessage}</EmptyList>
)}
</>
)

return (
<Wrapper>
Expand All @@ -68,6 +183,15 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore
contentPadding="0"
justifyContent="flex-start"
>
{!dappDetails && !hookToEditDetails && (
<HookListsTabs
isAllHooksTab={isAllHooksTab}
setIsAllHooksTab={setIsAllHooksTab}
allHooksCount={allHooksCount}
customHooksCount={customHooksCount}
onAddCustomHook={handleAddCustomHook}
/>
)}
{(() => {
if (selectedDapp) {
return (
Expand All @@ -87,17 +211,16 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore
return <HookDappDetails dapp={dappDetails} onSelect={() => setSelectedDapp(dappDetails)} />
}

return (
<HookDappsList>
{dapps.map((dapp) => (
<HookListItem
key={dapp.name}
dapp={dapp}
onSelect={() => setSelectedDapp(dapp)}
onOpenDetails={() => setDappDetails(dapp)}
/>
))}
</HookDappsList>
return isAllHooksTab ? (
DappsListContent
) : (
<AddCustomHookForm
isPreHook={isPreHook}
isSmartContractWallet={isSmartContractWallet}
addHookDapp={addCustomHookDapp}
>
{DappsListContent}
</AddCustomHookForm>
)
})()}
</NewModal>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UI } from '@cowprotocol/ui'

import styled from 'styled-components/macro'
import { WIDGET_MAX_WIDTH } from 'theme'

Expand All @@ -10,8 +12,8 @@ export const Wrapper = styled.div`

export const HookDappsList = styled.ul`
list-style: none;
padding: 10px;
margin: 0 auto;
padding: 10px;
gap: 8px;
width: 100%;
display: flex;
Expand All @@ -22,4 +24,14 @@ export const HookDappsList = styled.ul`
flex: 1;
`


export const EmptyList = styled.div`
color: var(${UI.COLOR_TEXT_OPACITY_50});
background: transparent;
min-height: 160px;
font-size: 16px;
padding: 30px 10px;
border-radius: 10px;
margin: 10px 0;
line-height: 1.3;
text-align: center;
`
Loading

0 comments on commit 5847017

Please sign in to comment.