diff --git a/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/index.tsx b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/index.tsx new file mode 100644 index 0000000000..537658bcf1 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/index.tsx @@ -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 ( + + +

{title}

+ {children} + + + + I understand + +
+ ) +} diff --git a/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/styled.tsx b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/styled.tsx new file mode 100644 index 0000000000..b20b0d3578 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/ExternalSourceAlert/styled.tsx @@ -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; + } + } + } +` diff --git a/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx b/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx index cc48110140..be7c66c654 100644 --- a/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx @@ -161,8 +161,8 @@ export interface NewModalProps { } export function NewModal({ - maxWidth = 450, - minHeight = 350, + maxWidth, + minHeight, contentPadding, justifyContent, modalMode, diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index c37892b475..339bc4aa22 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -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' @@ -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]) @@ -44,6 +48,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho signer, isSmartContract, isPreHook, + isDarkMode, editHook(...args) { editHook(...args) onDismiss() diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx index 093befe57d..3107eb5ab1 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx @@ -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 @@ -25,16 +35,55 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore const [selectedDapp, setSelectedDapp] = useState(null) const [dappDetails, setDappDetails] = useState(null) + const [isAllHooksTab, setIsAllHooksTab] = useState(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('') + + // 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) @@ -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 && ( + +

+ Can't find a hook that you like?{' '} + + Add a custom hook + +

+
+ )} + + setSearchQuery(e.target.value?.trim())} + placeholder="Search hooks by title or description" + ariaLabel="Search hooks" + onClear={handleClearSearch} + /> + + {filteredDapps.length > 0 ? ( + + {filteredDapps.map((dapp) => ( + removeCustomHookDapp(dapp as HookDappIframe)} + onSelect={() => setSelectedDapp(dapp)} + onOpenDetails={() => setDappDetails(dapp)} + /> + ))} + + ) : ( + {emptyListMessage} + )} + + ) return ( @@ -68,6 +183,15 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore contentPadding="0" justifyContent="flex-start" > + {!dappDetails && !hookToEditDetails && ( + + )} {(() => { if (selectedDapp) { return ( @@ -87,17 +211,16 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore return setSelectedDapp(dappDetails)} /> } - return ( - - {dapps.map((dapp) => ( - setSelectedDapp(dapp)} - onOpenDetails={() => setDappDetails(dapp)} - /> - ))} - + return isAllHooksTab ? ( + DappsListContent + ) : ( + + {DappsListContent} + ) })()} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/styled.tsx index 0bb4f34f9b..95721aabc9 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/styled.tsx @@ -1,3 +1,5 @@ +import { UI } from '@cowprotocol/ui' + import styled from 'styled-components/macro' import { WIDGET_MAX_WIDTH } from 'theme' @@ -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; @@ -22,4 +24,14 @@ export const HookDappsList = styled.ul` flex: 1; ` - \ No newline at end of file +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; +` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookSearchInput/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookSearchInput/index.tsx new file mode 100644 index 0000000000..58038035bb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookSearchInput/index.tsx @@ -0,0 +1,90 @@ +import IMG_CLOSE_ICON from '@cowprotocol/assets/cow-swap/x.svg' +import { SearchInput } from '@cowprotocol/ui' + +import SVG from 'react-inlinesvg' +import styled from 'styled-components/macro' + +interface HookSearchInputProps { + value: string + onChange: (e: React.ChangeEvent) => void + placeholder?: string + ariaLabel?: string + style?: React.CSSProperties + onClear?: () => void +} + +const SearchContainer = styled.div` + position: relative; + width: 100%; + padding: 0 10px; +` + +const ButtonIcon = styled.button` + display: flex; + align-items: center; + justify-content: center; + padding: 0; + background: transparent; + border: none; + cursor: pointer; + width: 24px; + height: 24px; + transition: opacity 0.2s ease-in-out; + + &:hover { + opacity: 0.8; + } +` + +const ClearButton = styled(ButtonIcon)` + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + padding: 0; + background: transparent; + border: none; + cursor: pointer; + opacity: 0.4; + transition: all 0.2s ease-in-out; + + &:hover { + opacity: 1; + } + + > svg { + width: 16px; + height: 16px; + color: inherit; + } + + > svg > path { + fill: currentColor; + } +` + +export function HookSearchInput({ + value, + onChange, + placeholder = 'Search hooks...', + ariaLabel = 'Search hooks', + onClear, +}: HookSearchInputProps) { + return ( + + + {value && ( + + + + )} + + ) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 0b42f6ffcd..d0e1862643 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -3,6 +3,8 @@ import { useCallback, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' +import styled from 'styled-components/macro' + import { SwapWidget } from 'modules/swap' import { useIsSellNative } from 'modules/trade' @@ -16,6 +18,13 @@ type HookPosition = 'pre' | 'post' console.log(ICON_HOOK) +const TradeWidgetWrapper = styled.div<{ visible$: boolean }>` + visibility: ${({ visible$ }) => (visible$ ? 'visible' : 'hidden')}; + height: ${({ visible$ }) => (visible$ ? '' : '0px')}; + width: ${({ visible$ }) => (visible$ ? '100%' : '0px')}; + overflow: hidden; +` + export function HooksStoreWidget() { const [selectedHookPosition, setSelectedHookPosition] = useState(null) const [hookToEdit, setHookToEdit] = useState(undefined) @@ -40,9 +49,7 @@ export function HooksStoreWidget() { useSetupHooksStoreOrderParams() useSetRecipientOverride() - if (selectedHookPosition || hookToEdit) { - return - } + const isHookSelectionOpen = !!(selectedHookPosition || hookToEdit) const shouldNotUseHooks = isNativeSell @@ -69,5 +76,14 @@ export function HooksStoreWidget() { setSelectedHookPosition('post')} onEditHook={onPostHookEdit} /> ) - return + return ( + <> + + + + {isHookSelectionOpen && ( + + )} + + ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx index 0cceddc227..0a25b9ffb5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx @@ -19,12 +19,12 @@ interface IframeDappContainerProps { } export function IframeDappContainer({ dapp, context }: IframeDappContainerProps) { const iframeRef = useRef(null) + const bridgeRef = useRef(null) const addHookRef = useRef(context.addHook) const editHookRef = useRef(context.editHook) const setSellTokenRef = useRef(context.setSellToken) const setBuyTokenRef = useRef(context.setBuyToken) - const [bridge, setBridge] = useState(null) const [isIframeActive, setIsIframeActive] = useState(false) const walletProvider = useWalletProvider() @@ -45,8 +45,7 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps) ), ] - const rpcBridge = new IframeRpcProviderBridge(iframeWindow) - setBridge(rpcBridge) + bridgeRef.current = new IframeRpcProviderBridge(iframeWindow) listeners.push( hookDappIframeTransport.listenToMessageFromWindow(window, CoWHookDappEvents.ADD_HOOK, (payload) => @@ -65,15 +64,15 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps) return () => { listeners.forEach((listener) => hookDappIframeTransport.stopListeningWindowListener(window, listener)) - rpcBridge.disconnect() + bridgeRef.current?.disconnect() } }, []) useLayoutEffect(() => { - if (!walletProvider || !bridge) return + if (!walletProvider || !walletProvider.provider || !bridgeRef.current) return - bridge.onConnect(walletProvider.provider as EthereumProvider) - }, [bridge, walletProvider]) + bridgeRef.current.onConnect(walletProvider.provider as EthereumProvider) + }, [walletProvider]) useLayoutEffect(() => { const iframeWindow = iframeRef.current?.contentWindow @@ -81,7 +80,7 @@ export function IframeDappContainer({ dapp, context }: IframeDappContainerProps) if (!iframeWindow || !isIframeActive) return // Omit unnecessary parameter - const { addHook: _, editHook: _1, signer: _2, ...iframeContext } = context + const { addHook: _, editHook: _1, signer: _2, setSellToken: _3, setBuyToken: _4, ...iframeContext } = context hookDappIframeTransport.postMessageToWindow(iframeWindow, CoWHookDappEvents.CONTEXT_UPDATE, iframeContext) }, [context, isIframeActive]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx index 807ac4d804..520efa0e75 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx @@ -3,7 +3,7 @@ import { useWalletInfo } from '@cowprotocol/wallet' import SVG from 'react-inlinesvg' -import { POST_HOOK_REGISTRY } from '../../hookRegistry' +import { useAllHookDapps } from '../../hooks/useAllHookDapps' import { useHooks } from '../../hooks/useHooks' import { useRemoveHook } from '../../hooks/useRemoveHook' import { useReorderHooks } from '../../hooks/useReorderHooks' @@ -19,11 +19,11 @@ export interface PostHookButtonProps { const isPreHook = false export function PostHookButton({ onOpen, onEditHook }: PostHookButtonProps) { - const { account, chainId } = useWalletInfo() + const { account } = useWalletInfo() const { postHooks } = useHooks() const removeHook = useRemoveHook(isPreHook) const moveHook = useReorderHooks('postHooks') - const dapps = POST_HOOK_REGISTRY[chainId] + const dapps = useAllHookDapps(isPreHook) return ( <> diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/PreHookButton/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/PreHookButton/index.tsx index 48e4288d5c..c16379d861 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/PreHookButton/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/PreHookButton/index.tsx @@ -5,7 +5,7 @@ import SVG from 'react-inlinesvg' import * as styledEl from './styled' -import { PRE_HOOK_REGISTRY } from '../../hookRegistry' +import { useAllHookDapps } from '../../hooks/useAllHookDapps' import { useHooks } from '../../hooks/useHooks' import { useRemoveHook } from '../../hooks/useRemoveHook' import { useReorderHooks } from '../../hooks/useReorderHooks' @@ -20,11 +20,11 @@ export interface PreHookButtonProps { const isPreHook = true export function PreHookButton({ onOpen, onEditHook }: PreHookButtonProps) { - const { account, chainId } = useWalletInfo() + const { account } = useWalletInfo() const { preHooks } = useHooks() const removeHook = useRemoveHook(isPreHook) const moveHook = useReorderHooks('preHooks') - const dapps = PRE_HOOK_REGISTRY[chainId] + const dapps = useAllHookDapps(isPreHook) return ( <> diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/styled.tsx index 9c2c674f7e..5160132a97 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/styled.tsx @@ -2,27 +2,11 @@ import { UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' -export const ClaimableAmountContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - - background-color: var(${UI.COLOR_BACKGROUND}); - - padding: 0.75rem; - margin-top: 1rem; - margin-bottom: 1rem; - border-radius: 0.75rem; - - span { - font-weight: 600; - } -` export const ContentWrapper = styled.div` display: flex; flex-flow: column wrap; justify-content: space-between; - padding: 24px 0 0; + padding: 24px 0; align-items: center; text-align: center; flex: 1 1 auto; @@ -50,9 +34,7 @@ export const Wrapper = styled.div` display: flex; flex-flow: column wrap; width: 100%; - - padding-bottom: 1rem; - + padding: 10px; flex-grow: 1; ` export const Label = styled.span` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx index 9d2b3f186e..1b2ac92766 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx @@ -62,13 +62,24 @@ export function ClaimGnoHookApp({ context }: HookDappProps) { return } - context.addHook({ - hook: { - callData, - gasLimit: gasLimit.toString(), - target: SBC_DEPOSIT_CONTRACT_ADDRESS, - }, - }) + if (context.hookToEdit) { + context.editHook({ + ...context.hookToEdit, + hook: { + callData, + gasLimit: gasLimit.toString(), + target: SBC_DEPOSIT_CONTRACT_ADDRESS, + }, + }) + } else { + context.addHook({ + hook: { + callData, + gasLimit: gasLimit.toString(), + target: SBC_DEPOSIT_CONTRACT_ADDRESS, + }, + }) + } }, [callData, gasLimit, context, claimable]) return ( @@ -84,7 +95,11 @@ export function ClaimGnoHookApp({ context }: HookDappProps) { )} - {claimable && !error && Add Pre-hook} + {claimable && !error && ( + + {context.hookToEdit ? 'Update Pre-hook' : 'Add Pre-hook'} + + )} ) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/styled.tsx index 7f526c5661..23462d1f88 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/styled.tsx @@ -6,7 +6,7 @@ export const Wrapper = styled.div` display: flex; flex-flow: column wrap; width: 100%; - padding-bottom: 10px; + padding: 10px; flex-grow: 1; ` @@ -16,12 +16,7 @@ export const ContentWrapper = styled.div` justify-content: center; align-items: center; flex-flow: column wrap; - margin-right: 10px; - display: flex; - justify-content: center; - align-items: center; - padding: 1em; text-align: center; ` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index c71bd885d7..93fa2b9975 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -1,11 +1,9 @@ -import { isLocal } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { AIRDROP_HOOK_APP } from './dapps/AirdropHookApp/hook' import { PRE_BUILD, POST_BUILD } from './dapps/BuildHookApp/hook' import { PRE_CLAIM_GNO } from './dapps/ClaimGnoHookApp/hook' import { PERMIT_HOOK } from './dapps/PermitHookApp/hook' -import { OMNIBRIDGE_POST_HOOK } from './iframeHookDapps' import { HookDapp } from './types/hooks' export const PRE_HOOK_REGISTRY: Record = { @@ -21,7 +19,3 @@ export const POST_HOOK_REGISTRY: Record = { [SupportedChainId.SEPOLIA]: [POST_BUILD, AIRDROP_HOOK_APP, PERMIT_HOOK], [SupportedChainId.ARBITRUM_ONE]: [POST_BUILD], } - -if (isLocal) { - POST_HOOK_REGISTRY[SupportedChainId.GNOSIS_CHAIN].push(OMNIBRIDGE_POST_HOOK) -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts new file mode 100644 index 0000000000..d0998491d9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts @@ -0,0 +1,16 @@ +import { useSetAtom } from 'jotai' +import { useCallback } from 'react' + +import { addCustomHookDappAtom } from '../state/customHookDappsAtom' +import { HookDappIframe } from '../types/hooks' + +export function useAddCustomHookDapp(isPreHook: boolean) { + const setState = useSetAtom(addCustomHookDappAtom) + + return useCallback( + (dapp: HookDappIframe) => { + return setState(isPreHook, dapp) + }, + [setState, isPreHook], + ) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts index 8432929eb7..b8bf146d11 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts @@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid' import { setHooksAtom } from '../state/hookDetailsAtom' import { AddHook, CowHookDetailsSerialized, HookDapp } from '../types/hooks' +import { getHookDappId } from '../utils' export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { const updateHooks = useSetAtom(setHooksAtom) @@ -14,7 +15,10 @@ export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { console.log('[hooks] Add ' + (isPreHook ? 'pre-hook' : 'post-hook'), hookToAdd, isPreHook) const uuid = uuidv4() - const hookDetails: CowHookDetailsSerialized = { hookDetails: { ...hookToAdd, uuid }, dappName: dapp.name } + const hookDetails: CowHookDetailsSerialized = { + hookDetails: { ...hookToAdd, uuid }, + dappId: getHookDappId(dapp), + } updateHooks((hooks) => { if (isPreHook) { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts new file mode 100644 index 0000000000..f441010de0 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts @@ -0,0 +1,17 @@ +import { useMemo } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useCustomHookDapps } from './useCustomHookDapps' + +import { POST_HOOK_REGISTRY, PRE_HOOK_REGISTRY } from '../hookRegistry' +import { HookDapp } from '../types/hooks' + +export function useAllHookDapps(isPreHook: boolean): HookDapp[] { + const { chainId } = useWalletInfo() + const customHookDapps = useCustomHookDapps(isPreHook) + + return useMemo(() => { + return (isPreHook ? PRE_HOOK_REGISTRY : POST_HOOK_REGISTRY)[chainId].concat(customHookDapps) + }, [customHookDapps, chainId]) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useCustomHookDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useCustomHookDapps.ts new file mode 100644 index 0000000000..d3b3a7644b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useCustomHookDapps.ts @@ -0,0 +1,10 @@ +import { useAtomValue } from 'jotai/index' + +import { customPostHookDappsAtom, customPreHookDappsAtom } from '../state/customHookDappsAtom' + +export function useCustomHookDapps(isPreHook: boolean) { + const pre = useAtomValue(customPreHookDappsAtom) + const post = useAtomValue(customPostHookDappsAtom) + + return isPreHook ? pre : post +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveCustomHookDapp.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveCustomHookDapp.ts new file mode 100644 index 0000000000..05274a42b5 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useRemoveCustomHookDapp.ts @@ -0,0 +1,7 @@ +import { useSetAtom } from 'jotai' + +import { removeCustomHookDappAtom } from '../state/customHookDappsAtom' + +export function useRemoveCustomHookDapp() { + return useSetAtom(removeCustomHookDappAtom) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts index 016e54ab56..63fb6bbe4e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetRecipientOverride.ts @@ -1,16 +1,23 @@ -import { useEffect } from 'react' +import { useLayoutEffect } from 'react' import { useSwapActionHandlers } from 'modules/swap/hooks/useSwapState' +import { useIsHooksTradeType, useIsNativeIn } from 'modules/trade' import { usePostHooksRecipientOverride } from './usePostHooksRecipientOverride' export function useSetRecipientOverride() { const { onChangeRecipient } = useSwapActionHandlers() const hookRecipientOverride = usePostHooksRecipientOverride() + const isHooksTradeType = useIsHooksTradeType() + const isNativeIn = useIsNativeIn() - useEffect(() => { - if (!hookRecipientOverride) return + /** + * Don't remove isNativeIn from dependencies + * the hooks should be re-executed when sell token changes from native + */ + useLayoutEffect(() => { + if (!hookRecipientOverride || !isHooksTradeType) return onChangeRecipient(hookRecipientOverride) - }, [hookRecipientOverride]) + }, [hookRecipientOverride, isHooksTradeType, isNativeIn]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/iframeHookDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/iframeHookDapps.ts deleted file mode 100644 index 2d56c3ea7d..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/iframeHookDapps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { HookDappIframe, HookDappType, HookDappWalletCompatibility } from './types/hooks' - -export const OMNIBRIDGE_POST_HOOK: HookDappIframe = { - url: 'http://localhost:3000/hook-dapp-omnibridge/', - type: HookDappType.IFRAME, - name: 'Omnibridge', - descriptionShort: 'Bridge from Gnosis Chain to Mainnet', - description: - 'The Omnibridge can be used to bridge ERC-20 tokens between Ethereum and Gnosis. The first time a token is bridged, a new ERC677 token contract is deployed on GC with an additional suffix to differentiate the token. It will say "token name on xDai", as this was the original chain name prior to re-branding. If a token has been bridged previously, the previously deployed contract is used. The requested token amount is minted and sent to the account initiating the transfer (or an alternative receiver account specified by the sender).', - version: '0.0.1', - website: 'https://omni.legacy.gnosischain.com', - image: 'http://localhost:3000/hook-dapp-omnibridge/android-chrome-192x192.png', - walletCompatibility: [HookDappWalletCompatibility.EOA, HookDappWalletCompatibility.SMART_CONTRACT], -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx new file mode 100644 index 0000000000..c24890d833 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx @@ -0,0 +1,98 @@ +import { Dispatch, SetStateAction, useEffect } from 'react' + +import { HookDappBase, HookDappIframe, HookDappType } from '../../../types/hooks' + +interface HookDappConditions { + position?: 'post' | 'pre' + smartContractWalletSupported?: boolean +} + +type HookDappBaseInfo = Omit + +type HookDappManifest = HookDappBaseInfo & { + conditions?: HookDappConditions +} + +const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['name', 'image', 'version', 'website'] + +interface ExternalDappLoaderProps { + input: string + isPreHook: boolean + isSmartContractWallet: boolean | undefined + setDappInfo: Dispatch> + setLoading: Dispatch> + setManifestError: Dispatch> +} + +export function ExternalDappLoader({ + input, + setLoading, + setManifestError, + setDappInfo, + isSmartContractWallet, + isPreHook, +}: ExternalDappLoaderProps) { + useEffect(() => { + let isRequestRelevant = true + + setLoading(true) + + fetch(`${input}/manifest.json`) + .then((res) => res.json()) + .then((data) => { + if (!isRequestRelevant) return + + const { conditions = {}, ...dapp } = data.cow_hook_dapp as HookDappManifest + + if (dapp) { + const emptyFields = MANDATORY_DAPP_FIELDS.filter((field) => typeof dapp[field] === 'undefined') + + if (emptyFields.length > 0) { + setManifestError(`${emptyFields.join(',')} fields are no set.`) + } else { + if (conditions.smartContractWalletSupported === false && isSmartContractWallet === true) { + setManifestError('The app does not support smart-contract wallets.') + } else if (conditions.position === 'post' && isPreHook) { + setManifestError( +

+ This app/hook can only be used as a post-hook and cannot be added as a pre-hook. +

, + ) + } else if (conditions.position === 'pre' && !isPreHook) { + setManifestError( +

+ This app/hook can only be used as a pre-hook and cannot be added as a post-hook. +

, + ) + } else { + setManifestError(null) + setDappInfo({ + ...dapp, + type: HookDappType.IFRAME, + url: input, + }) + } + } + } else { + setManifestError('Manifest does not contain "cow_hook_dapp" property.') + } + }) + .catch((error) => { + if (!isRequestRelevant) return + + console.error(error) + setManifestError('Can not fetch the manifest.json') + }) + .finally(() => { + if (!isRequestRelevant) return + + setLoading(false) + }) + + return () => { + isRequestRelevant = false + } + }, [input]) + + return null +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx new file mode 100644 index 0000000000..5f43f8b616 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/index.tsx @@ -0,0 +1,142 @@ +import { ReactElement, useCallback, useEffect, useState } from 'react' + +import { uriToHttp } from '@cowprotocol/common-utils' +import { ButtonOutlined, ButtonPrimary, InlineBanner, Loader, SearchInput } from '@cowprotocol/ui' + +import { ExternalSourceAlert } from 'common/pure/ExternalSourceAlert' + +import { ExternalDappLoader } from './CustomDappLoader' +import { Wrapper } from './styled' + +import { HookDappIframe } from '../../types/hooks' +import { HookDappDetails } from '../HookDappDetails' + +interface AddCustomHookFormProps { + isPreHook: boolean + isSmartContractWallet: boolean | undefined + addHookDapp(dapp: HookDappIframe): void + children: ReactElement | null +} + +export function AddCustomHookForm({ addHookDapp, children, isPreHook, isSmartContractWallet }: AddCustomHookFormProps) { + const [input, setInput] = useState(undefined) + const [isSearchOpen, setSearchOpen] = useState(false) + const [isWarningAccepted, setWarningAccepted] = useState(false) + const [isUrlValid, setUrlValid] = useState(true) + const [loading, setLoading] = useState(false) + const [isFinalStep, setFinalStep] = useState(false) + const [manifestError, setManifestError] = useState(null) + const [dappInfo, setDappInfo] = useState(null) + + // Function to reset all states + const dismiss = useCallback(() => { + setDappInfo(null) + setManifestError(null) + setUrlValid(true) + setLoading(false) + setFinalStep(false) + setWarningAccepted(false) + }, []) + + // Function to handle going back from search mode + const goBack = useCallback(() => { + dismiss() + setInput(undefined) + setSearchOpen(false) + }, [dismiss]) + + // Function to handle adding the hook dapp + const addHookDappCallback = useCallback(() => { + if (!dappInfo) return + addHookDapp(dappInfo) + goBack() + }, [addHookDapp, dappInfo, goBack]) + + // Effect to validate the URL whenever the input changes + useEffect(() => { + // Reset state whenever input changes + dismiss() + setUrlValid(input ? uriToHttp(input).length > 0 : true) + }, [input, dismiss]) + + return ( + <> + {/* Render children when not in search mode */} + {!isSearchOpen && children} + + {/* Render the "Add custom hook" button when not in search mode */} + {!isSearchOpen && ( + + setSearchOpen(true)}> + {loading ? : 'Add custom hook'} + + + )} + + {/* Render the search form when in search mode */} + {isSearchOpen && ( + + {/* Search Input Field */} + setInput(e.target.value?.trim())} + /> + + {/* Validation and Error Messages */} + {input && !isUrlValid && ( + + Hook Dapp URL must match "https://website" format + + )} + {manifestError && ( + + {manifestError} + + )} + + {/* Load external dapp information */} + {input && isUrlValid && ( + + )} + + {/* Display dapp details */} + {dappInfo && !isFinalStep && setFinalStep(true)} />} + + {/* Final Step: Warning and Confirmation */} + {isFinalStep && ( + <> + setWarningAccepted((state) => !state)} + > +

+ Adding this app/hook grants it access to your wallet actions and trading information. Ensure you + understand the implications.
+
+ Always review wallet requests carefully before approving. +

+
+ + Add custom hook + + + )} + + {/* Display the "Back" button */} + + Back + +
+ )} + + ) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/styled.ts b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/styled.ts new file mode 100644 index 0000000000..d36f2224a8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/styled.ts @@ -0,0 +1,12 @@ +import styled from 'styled-components/macro' + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; +` + +export const Input = styled.div` + display: block; +` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx index 900d01950e..92add9f383 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -1,8 +1,12 @@ +// src/modules/hooksStore/pure/AppliedHookItem/index.tsx + +import ICON_CHECK_ICON from '@cowprotocol/assets/cow-swap/check-singular.svg' import ICON_GRID from '@cowprotocol/assets/cow-swap/grid.svg' import TenderlyLogo from '@cowprotocol/assets/cow-swap/tenderly-logo.svg' +import ICON_X from '@cowprotocol/assets/cow-swap/x.svg' import { InfoTooltip } from '@cowprotocol/ui' -import { Edit2, Trash2 } from 'react-feather' +import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather' import SVG from 'react-inlinesvg' import * as styledEl from './styled' @@ -20,6 +24,9 @@ interface HookItemProp { index: number } +// TODO: remove once a tenderly bundle simulation is ready +const isBundleSimulationReady = false + export function AppliedHookItem({ account, hookDetails: { hookDetails }, @@ -29,6 +36,20 @@ export function AppliedHookItem({ removeHook, index, }: HookItemProp) { + // TODO: Determine the simulation status based on actual simulation results + // For demonstration, using a placeholder. Replace with actual logic. + const simulationPassed = true // TODO: Replace with actual condition + const simulationStatus = simulationPassed ? 'Simulation successful' : 'Simulation failed' + const simulationTooltip = simulationPassed + ? 'The Tenderly simulation was successful. Your transaction is expected to succeed.' + : 'The Tenderly simulation failed. Please review your transaction.' + + // TODO: Placeholder for Tenderly simulation URL; replace with actual logic when available + const tenderlySimulationUrl = '' // e.g., 'https://tenderly.co/simulation/12345' + + // TODO: Determine if simulation passed or failed + const isSimulationSuccessful = simulationPassed + return ( @@ -50,12 +71,31 @@ export function AppliedHookItem({ - {account && ( - + {account && isBundleSimulationReady && ( + + {isSimulationSuccessful ? ( + + ) : ( + + )} + {tenderlySimulationUrl ? ( + + {simulationStatus} + + + ) : ( + {simulationStatus} + )} + + + )} + + {!isBundleSimulationReady && ( +
Run a simulation - + Powered by @@ -65,7 +105,7 @@ export function AppliedHookItem({
- + )} ) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx index 25e3cc338a..ac71a21f81 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx @@ -2,8 +2,6 @@ import { UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' -import { CloseIcon as CloseIconOriginal } from 'common/pure/CloseIcon' - export const HookItemWrapper = styled.li` border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10}); border-radius: 16px; @@ -142,31 +140,51 @@ export const ActionBtn = styled.button<{ actionType?: 'remove' | 'edit' }>` } ` -export const CustomLink = styled.a` - margin: 0.5em 0; - padding: 0 10em; - text-decoration: none; +export const SimulateContainer = styled.div<{ isSuccessful: boolean }>` + --colorBG: ${({ isSuccessful }) => (isSuccessful ? `var(${UI.COLOR_SUCCESS_BG})` : `var(${UI.COLOR_DANGER_BG})`)}; + --colorText: ${({ isSuccessful }) => + isSuccessful ? `var(${UI.COLOR_SUCCESS_TEXT})` : `var(${UI.COLOR_DANGER_TEXT})`}; + + border: 1px solid var(--colorBG); + background: var(--colorBG); + color: var(--colorText); + border-radius: 9px; + padding: 10px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 10px; + font-size: 14px; + + > svg { + margin: 0 7px 0 0; + color: inherit; + } - :hover { - text-decoration: underline; + > svg > path { + fill: currentColor; } -` -export const CloseIcon = styled(CloseIconOriginal)` - position: absolute; - top: 0; - right: 0; + > a, + > span { + margin: 0 auto 0 0; + color: inherit; + display: flex; + align-items: center; + gap: 4px; + } ` -export const SimulateContainer = styled.div` - border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10}); +export const OldSimulateContainer = styled.div` + border: 1px solid var(${UI.COLOR_TEXT_OPACITY_25}); border-radius: 4px; padding: 10px; display: flex; flex-direction: row; justify-content: space-between; align-items: center; - margin: 10px; + font-size: 13px; ` export const SimulateHeader = styled.div` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx index ef79b15682..fb741ebbf3 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx @@ -4,6 +4,7 @@ import Sortable from 'sortablejs' import styled from 'styled-components/macro' import { CowHookDetailsSerialized, HookDapp } from '../../types/hooks' +import { findHookDappById } from '../../utils' import { AppliedHookItem } from '../AppliedHookItem' const HookList = styled.ul` @@ -68,7 +69,7 @@ export function AppliedHookList({ return ( i.name === hookDetails.dappName)!} + dapp={findHookDappById(dapps, hookDetails)!} index={index} account={account} hookDetails={hookDetails} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx index 2d70967c9e..0107376e85 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx @@ -15,7 +15,7 @@ interface HookDappDetailsProps { export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { const tags = useMemo(() => { - const { version, website, type, walletCompatibility } = dapp + const { version, website, type, walletCompatibility = [] } = dapp const getWalletCompatibilityTooltip = () => { const isSmartContract = walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT) @@ -47,8 +47,11 @@ export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { }, { label: 'Wallet support', - value: walletCompatibility.join(', '), - tooltip: getWalletCompatibilityTooltip(), + value: walletCompatibility.length > 0 ? walletCompatibility.join(', ') : 'N/A', + tooltip: + walletCompatibility.length > 0 + ? getWalletCompatibilityTooltip() + : 'No wallet compatibility information available.', }, ] }, [dapp]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx index e581650b4c..fb9293bdff 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx @@ -11,16 +11,25 @@ interface HookListItemProps { dapp: HookDapp onSelect: Command onOpenDetails: Command + onRemove?: Command } -export function HookListItem({ dapp, onSelect, onOpenDetails }: HookListItemProps) { +export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookListItemProps) { const { name, descriptionShort, image, version } = dapp + const handleItemClick = (event: React.MouseEvent) => { + const target = event.target as HTMLElement + // Check if the click target is not a button or the info icon + if (!target.closest('.link-button') && !target.closest('.remove-button') && !target.closest('i')) { + onOpenDetails() + } + } + return ( - + {name} - +

{name}

{descriptionShort} @@ -31,7 +40,17 @@ export function HookListItem({ dapp, onSelect, onOpenDetails }: HookListItemProp Add - + {onRemove ? ( + + Remove + + ) : null} + { + e.stopPropagation() + onOpenDetails() + }} + > details diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx index 7e5b90bbe8..a009113cac 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx @@ -1,25 +1,29 @@ import { UI, Media } from '@cowprotocol/ui' -import styled from 'styled-components/macro' +import styled, { css } from 'styled-components/macro' -export const LinkButton = styled.button` +const BaseButton = css` display: flex; cursor: pointer; - background: var(${UI.COLOR_PRIMARY}); - color: var(${UI.COLOR_PAPER}); - border: none; outline: none; - font-weight: 600; - font-size: 16px; + font-size: 14px; text-decoration: none; padding: 11px; line-height: 1; - display: block; margin: 0; border-radius: 21px; min-width: 84px; justify-content: center; transition: all 0.2s ease-in-out; +` + +export const LinkButton = styled.button` + ${BaseButton} + background: var(${UI.COLOR_PRIMARY}); + color: var(${UI.COLOR_PAPER}); + border: none; + font-weight: 600; + font-size: 16px; ${Media.upToSmall()} { width: 100%; @@ -31,6 +35,19 @@ export const LinkButton = styled.button` } ` +export const RemoveButton = styled.button` + ${BaseButton} + background: transparent; + border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10}); + color: var(${UI.COLOR_TEXT}); + + &:hover { + background: var(${UI.COLOR_DANGER_BG}); + color: var(${UI.COLOR_DANGER_TEXT}); + border-color: var(${UI.COLOR_DANGER_BG}); + } +` + export const HookDappListItem = styled.li<{ isDescriptionView?: boolean }>` width: 100%; background: transparent; @@ -83,6 +100,7 @@ export const HookDappListItem = styled.li<{ isDescriptionView?: boolean }>` display: flex; align-items: center; gap: 3px; + margin: 10px 0 0; transition: all 0.2s ease-in-out; font-style: normal; color: var(${UI.COLOR_TEXT_OPACITY_50}); diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListsTabs/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListsTabs/index.tsx new file mode 100644 index 0000000000..e7a17acbf7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListsTabs/index.tsx @@ -0,0 +1,29 @@ +import { Dispatch, SetStateAction } from 'react' + +import * as styledEl from './styled' + +interface HookListsTabsProps { + isAllHooksTab: boolean + setIsAllHooksTab: Dispatch> + allHooksCount: number + customHooksCount: number + onAddCustomHook: () => void +} + +export function HookListsTabs({ + isAllHooksTab, + setIsAllHooksTab, + allHooksCount, + customHooksCount, +}: HookListsTabsProps) { + return ( + + setIsAllHooksTab(true)}> + All Hooks ({allHooksCount}) + + setIsAllHooksTab(false)}> + My Custom Hooks ({customHooksCount}) + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListsTabs/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListsTabs/styled.tsx new file mode 100644 index 0000000000..edd2544640 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListsTabs/styled.tsx @@ -0,0 +1,49 @@ +import { UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +export const TabsContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + margin: 16px 0 10px; + border-bottom: 1px solid var(${UI.COLOR_TEXT_OPACITY_10}); +` + +export const Tab = styled.button<{ active$: boolean }>` + background: none; + margin: 0; + outline: none; + border: 0; + cursor: pointer; + color: ${({ active$ }) => (active$ ? 'var(' + UI.COLOR_INFO + ')' : 'var(' + UI.COLOR_TEXT + ')')}; + opacity: ${({ active$ }) => (active$ ? 1 : 0.5)}; + padding: 14px 16px; + font-size: 15px; + font-weight: 600; + position: relative; + transition: all 0.2s ease-in-out; + border-radius: 5px; + flex: 1 1 auto; + + &::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 2px; + background-color: var(${UI.COLOR_INFO}); + opacity: ${({ active$ }) => (active$ ? 1 : 0)}; + transition: all 0.2s ease-in-out; + } + + &:hover { + opacity: 1; + background-color: var(${UI.COLOR_PRIMARY_OPACITY_10}); + } + + &:disabled { + cursor: default; + } +` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts new file mode 100644 index 0000000000..1610661e01 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts @@ -0,0 +1,79 @@ +import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +import { getJotaiIsolatedStorage } from '@cowprotocol/core' +import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' +import { walletInfoAtom } from '@cowprotocol/wallet' + +import { setHooksAtom } from './hookDetailsAtom' + +import { HookDappIframe } from '../types/hooks' +import { getHookDappId } from '../utils' + +type CustomHookDapps = Record + +type CustomHooksState = { + pre: CustomHookDapps + post: CustomHookDapps +} + +const EMPTY_STATE: CustomHooksState = { pre: {}, post: {} } + +const customHookDappsInner = atomWithStorage>( + 'customHookDappsAtom:v1', + mapSupportedNetworks(EMPTY_STATE), + getJotaiIsolatedStorage(), +) + +export const customHookDappsAtom = atom((get) => { + const { chainId } = get(walletInfoAtom) + const state = get(customHookDappsInner) + + return state[chainId] || EMPTY_STATE +}) + +export const customPreHookDappsAtom = atom((get) => { + return Object.values(get(customHookDappsAtom).pre) as HookDappIframe[] +}) + +export const customPostHookDappsAtom = atom((get) => { + return Object.values(get(customHookDappsAtom).post) as HookDappIframe[] +}) + +export const addCustomHookDappAtom = atom(null, (get, set, isPreHook: boolean, dapp: HookDappIframe) => { + const { chainId } = get(walletInfoAtom) + const state = get(customHookDappsInner) + + set(customHookDappsInner, { + ...state, + [chainId]: { + ...state[chainId], + [isPreHook ? 'pre' : 'post']: { + ...state[chainId][isPreHook ? 'pre' : 'post'], + [dapp.url]: dapp, + }, + }, + }) +}) + +export const removeCustomHookDappAtom = atom(null, (get, set, dapp: HookDappIframe) => { + const { chainId } = get(walletInfoAtom) + const state = get(customHookDappsInner) + const currentState = { ...state[chainId] } + + delete currentState.pre[dapp.url] + delete currentState.post[dapp.url] + + set(customHookDappsInner, { + ...state, + [chainId]: currentState, + }) + + const hookDappId = getHookDappId(dapp) + + // Delete applied hooks along with the deleting hook-dapp + set(setHooksAtom, (hooksState) => ({ + preHooks: (hooksState.preHooks || []).filter((hook) => hook.dappId !== hookDappId), + postHooks: (hooksState.postHooks || []).filter((hook) => hook.dappId !== hookDappId), + })) +}) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts index 600947b893..e975b59799 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts @@ -3,13 +3,9 @@ import { atomWithStorage } from 'jotai/utils' import { getJotaiIsolatedStorage } from '@cowprotocol/core' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' -import type { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { walletInfoAtom } from '@cowprotocol/wallet' -interface CowHookDetailsSerialized { - hookDetails: CowHookDetails - dappName: string -} +import { CowHookDetailsSerialized } from '../types/hooks' export type HooksStoreState = { preHooks: CowHookDetailsSerialized[] @@ -25,7 +21,7 @@ const EMPTY_STATE: HooksStoreState = { } const hooksAtomInner = atomWithStorage( - 'hooksStoreAtom:v1', + 'hooksStoreAtom:v2', mapSupportedNetworks({}), getJotaiIsolatedStorage(), ) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts index 08c5c62063..dea723d383 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts @@ -33,6 +33,8 @@ export interface HookDappBase { walletCompatibility: HookDappWalletCompatibility[] } +export type DappId = `${HookDappType}:::${HookDappBase['name']}` + export interface HookDappInternal extends HookDappBase { type: HookDappType.INTERNAL component: (props: HookDappProps) => ReactNode @@ -48,7 +50,7 @@ export type HookDapp = HookDappInternal | HookDappIframe export interface CowHookDetailsSerialized { hookDetails: CowHookDetails - dappName: string + dappId: DappId } export type AddHook = CoWHookDappActions['addHook'] diff --git a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts index aa130a32d0..f3b14c81c5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts @@ -1,6 +1,21 @@ -import { HookDapp, HookDappIframe, HookDappType } from './types/hooks' +import { CowHookDetailsSerialized, DappId, HookDapp, HookDappBase, HookDappIframe, HookDappType } from './types/hooks' // Do a safe guard assertion that receives a HookDapp and asserts is a HookDappIframe export function isHookDappIframe(dapp: HookDapp): dapp is HookDappIframe { return dapp.type === HookDappType.IFRAME } + +export const getHookDappId = (dapp: HookDapp): DappId => `${dapp.type}:::${dapp.name}` +export function parseDappId(id: DappId): Pick { + const [type, name] = id.split(':::') + + return { type: type as HookDappType, name } +} + +export function findHookDappById(dapps: HookDapp[], hookDetails: CowHookDetailsSerialized): HookDapp | undefined { + return dapps.find((i) => { + const { type, name } = parseDappId(hookDetails.dappId) + + return i.type === type && i.name === name + }) +} diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/index.tsx index 40513080de..6abd085ae6 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/index.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { TokenLogo, getTokenListViewLink, ListState } from '@cowprotocol/tokens' import { ButtonPrimary } from '@cowprotocol/ui' -import { AlertTriangle } from 'react-feather' +import { ExternalSourceAlert } from 'common/pure/ExternalSourceAlert' import * as styledEl from './styled' @@ -39,23 +39,18 @@ export function ImportListModal(props: ImportListModalProps) {

- - -

Import at your own risk

-

- By adding this list you are implicitly trusting that the data is correct. Anyone can create a list, including - creating fake versions of existing lists and lists that claim to represent projects that do not have one. -

-

- If you purchase a token from this list, you may not be able to sell it back. -

-
- - setIsAccepted((state) => !state)} /> - I understand - -
-
+ setIsAccepted((state) => !state)}> + <> +

+ By adding this list you are implicitly trusting that the data is correct. Anyone can create a list, + including creating fake versions of existing lists and lists that claim to represent projects that do not + have one. +

+

+ If you purchase a token from this list, you may not be able to sell it back. +

+ +
onImport(list)}> Import diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/styled.ts index 1562f7e3b8..0af188bbff 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/ImportListModal/styled.ts @@ -10,29 +10,10 @@ export const Wrapper = styled.div` border-radius: 20px; ` -export const Contents = styled.div` - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - gap: 5px; - padding: 20px; - margin: 20px; - border-radius: 20px; - color: var(${UI.COLOR_DANGER_TEXT}); - background: var(${UI.COLOR_DANGER_BG}); -` - export const ActionButtonWrapper = styled.div` padding: 0 20px 20px 20px; ` -export const AcceptanceBox = styled.label` - display: flex; - gap: 6px; - cursor: pointer; -` - export const ListInfo = styled.div` display: flex; flex-direction: row; diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index 477ea91a47..cb8ef213c4 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import { BalancesState } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { UnsupportedTokensState } from '@cowprotocol/tokens' -import { BackButton } from '@cowprotocol/ui' +import { BackButton, SearchInput } from '@cowprotocol/ui' import { Edit } from 'react-feather' @@ -69,7 +69,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) {

Select a token

- e.key === 'Enter' && onInputPressEnter?.()} diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts index 6e5b26c90b..92f88d3443 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/styled.ts @@ -34,34 +34,6 @@ export const Header = styled.div` } ` -export const SearchInput = styled.input` - position: relative; - display: flex; - padding: 16px; - align-items: center; - width: 100%; - white-space: nowrap; - outline: none; - background: var(${UI.COLOR_PAPER_DARKER}); - color: inherit; - border: 1px solid var(${UI.COLOR_BORDER}); - appearance: none; - font-size: 16px; - border-radius: 12px; - - ::placeholder { - color: inherit; - opacity: 0.7; - } - - transition: border 100ms; - - :focus { - border: 1px solid var(${UI.COLOR_PRIMARY}); - outline: none; - } -` - export const ActionButton = styled.button` ${blankButtonMixin}; diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts index 79ccb97c1c..b4640608d4 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useResetRecipient.ts @@ -7,6 +7,8 @@ import { usePostHooksRecipientOverride } from 'modules/hooksStore' import { useTradeStateFromUrl } from './setupTradeState/useTradeStateFromUrl' import { useDerivedTradeState } from './useDerivedTradeState' +import { useIsHooksTradeType } from './useIsHooksTradeType' +import { useIsNativeIn } from './useIsNativeInOrOut' import { useIsAlternativeOrderModalVisible } from '../state/alternativeOrder' @@ -15,6 +17,8 @@ export function useResetRecipient(onChangeRecipient: (recipient: string | null) const tradeState = useDerivedTradeState() const tradeStateFromUrl = useTradeStateFromUrl() const postHooksRecipientOverride = usePostHooksRecipientOverride() + const isHooksTradeType = useIsHooksTradeType() + const isNativeIn = useIsNativeIn() const hasTradeState = !!tradeStateFromUrl const { chainId } = useWalletInfo() @@ -45,10 +49,21 @@ export function useResetRecipient(onChangeRecipient: (recipient: string | null) * Remove recipient override when its source hook was deleted */ useEffect(() => { - if (!postHooksRecipientOverride && recipient === prevPostHooksRecipientOverride) { + const recipientOverrideWasRemoved = !postHooksRecipientOverride && recipient === prevPostHooksRecipientOverride + + if (recipientOverrideWasRemoved) { + onChangeRecipient(null) + } + }, [recipient, postHooksRecipientOverride, prevPostHooksRecipientOverride, isNativeIn, onChangeRecipient]) + + /** + * Remove recipient when going out from hooks-store page + */ + useEffect(() => { + if (!isHooksTradeType || (isHooksTradeType && isNativeIn)) { onChangeRecipient(null) } - }, [recipient, postHooksRecipientOverride, prevPostHooksRecipientOverride, onChangeRecipient]) + }, [isHooksTradeType, isNativeIn, onChangeRecipient]) return null } diff --git a/apps/cowswap-frontend/src/modules/trade/index.ts b/apps/cowswap-frontend/src/modules/trade/index.ts index 261a9eb561..c08f839ed6 100644 --- a/apps/cowswap-frontend/src/modules/trade/index.ts +++ b/apps/cowswap-frontend/src/modules/trade/index.ts @@ -15,6 +15,7 @@ export * from './types/TradeDerivedState' export * from './hooks/useTradeNavigate' export * from './hooks/useReceiveAmountInfo' export * from './hooks/useTradeState' +export * from './hooks/useIsNativeInOrOut' export * from './hooks/useNavigateOnCurrencySelection' export * from './hooks/useSwitchTokensPlaces' export * from './hooks/useUpdateCurrencyAmount' diff --git a/apps/hook-dapp-omnibridge/public/manifest.json b/apps/hook-dapp-omnibridge/public/manifest.json index 2dbd359004..aaf2080e3a 100644 --- a/apps/hook-dapp-omnibridge/public/manifest.json +++ b/apps/hook-dapp-omnibridge/public/manifest.json @@ -21,5 +21,17 @@ "name": "Omnibridge hook", "short_name": "Omnibridge hook", "start_url": ".", - "theme_color": "#ffffff" + "theme_color": "#ffffff", + "cow_hook_dapp": { + "name": "Omnibridge", + "descriptionShort": "Bridge from Gnosis Chain to Mainnet", + "description": "The Omnibridge can be used to bridge ERC-20 tokens between Ethereum and Gnosis. The first time a token is bridged, a new ERC677 token contract is deployed on GC with an additional suffix to differentiate the token. It will say \"token name on xDai\", as this was the original chain name prior to re-branding. If a token has been bridged previously, the previously deployed contract is used. The requested token amount is minted and sent to the account initiating the transfer (or an alternative receiver account specified by the sender).", + "version": "0.0.1", + "website": "https://omni.legacy.gnosischain.com", + "image": "http://localhost:3000/hook-dapp-omnibridge/apple-touch-icon.png", + "conditions": { + "position": "post", + "smartContractWalletSupported": false + } + } } diff --git a/apps/hook-dapp-omnibridge/src/app/hook-dapp/index.tsx b/apps/hook-dapp-omnibridge/src/app/hook-dapp/index.tsx index 5c2aad7960..eebc93aacf 100644 --- a/apps/hook-dapp-omnibridge/src/app/hook-dapp/index.tsx +++ b/apps/hook-dapp-omnibridge/src/app/hook-dapp/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { CoWHookDappActions, HookDappContext, initCoWHookDapp } from '@cowprotocol/hook-dapp-lib' @@ -57,8 +57,8 @@ export function OmnibridgeApp() { setSigner(signer) }, []) - if (!orderParams) return

Please, specify valid order first

- if (!proxyAddress) return

Please, connect wallet first

+ if (!proxyAddress) return

Please connect your wallet to continue

+ if (!orderParams) return

Please specify your swap order before proceeding

return (
diff --git a/libs/hook-dapp-lib/README.md b/libs/hook-dapp-lib/README.md index 922758fe0e..a8dfe15485 100644 --- a/libs/hook-dapp-lib/README.md +++ b/libs/hook-dapp-lib/README.md @@ -3,9 +3,7 @@ >CoW Hooks allow users to call arbitrary Ethereum action before and/or after swap. >For example: before swap give a permission to CoW Protocol smart-contract and after swap bridge tokens to another chain. -Main docs: https://docs.cow.fi/cow-protocol/reference/core/intents/hooks - -Tutorial: https://v1.docs.cow.fi/overview/cow-hooks/cow-hooks-example/permit-swap-and-bridge-cow-hook +### Docs: https://docs.cow.fi/cow-protocol/tutorials/hook-dapp ![](./demo.png) diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index 78eb951415..32201d1725 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -39,4 +39,5 @@ export interface HookDappContext { hookToEdit?: CowHookDetails isSmartContract: boolean | undefined isPreHook: boolean + isDarkMode: boolean } diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index e2e277d562..403e897f4f 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -32,6 +32,7 @@ export * from './pure/ClosableBanner' export * from './pure/PercentDisplay' export * from './pure/CmsImage' export * from './pure/DismissableInlineBanner' +export * from './pure/Input' export * from './containers/CowSwapSafeAppLink' export * from './containers/InlineBanner' diff --git a/libs/ui/src/pure/Input/index.tsx b/libs/ui/src/pure/Input/index.tsx new file mode 100644 index 0000000000..95476536d4 --- /dev/null +++ b/libs/ui/src/pure/Input/index.tsx @@ -0,0 +1,31 @@ +import styled from 'styled-components/macro' + +import { UI } from '../../enum' + +export const SearchInput = styled.input` + position: relative; + display: flex; + padding: 16px; + align-items: center; + width: 100%; + white-space: nowrap; + outline: none; + background: var(${UI.COLOR_PAPER_DARKER}); + color: inherit; + border: 1px solid var(${UI.COLOR_BORDER}); + appearance: none; + font-size: 16px; + border-radius: 12px; + + ::placeholder { + color: inherit; + opacity: 0.7; + } + + transition: border 100ms; + + :focus { + border: 1px solid var(${UI.COLOR_PRIMARY}); + outline: none; + } +`