From e60e9bf1ca5df77dc434722759fbd29055dc3538 Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:53:00 +0100 Subject: [PATCH 1/2] feat: prevent adding unsupported hooks for your wallet --- .../containers/HookRegistryList/index.tsx | 142 ++++++++---------- .../hooksStore/pure/HookDappDetails/index.tsx | 5 +- .../pure/HookDetailHeader/index.tsx | 22 ++- .../pure/HookDetailHeader/styled.ts | 10 +- .../hooksStore/pure/HookListItem/index.tsx | 25 ++- .../hooksStore/pure/HookListItem/styled.tsx | 26 +++- 6 files changed, 128 insertions(+), 102 deletions(-) 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 8953a4b5ce..6d2dc2ce34 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { Command } from '@cowprotocol/types' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' import { useIsSmartContractWallet } from '@cowprotocol/wallet' @@ -33,62 +34,48 @@ interface HookStoreModal { export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStoreModal) { const [selectedDapp, setSelectedDapp] = useState(null) const [dappDetails, setDappDetails] = useState(null) - const [isAllHooksTab, setIsAllHooksTab] = useState(true) + const [searchQuery, setSearchQuery] = useState('') const isSmartContractWallet = useIsSmartContractWallet() + const walletType = isSmartContractWallet + ? HookDappWalletCompatibility.SMART_CONTRACT + : HookDappWalletCompatibility.EOA const addCustomHookDapp = useAddCustomHookDapp(isPreHook) const removeCustomHookDapp = useRemoveCustomHookDapp() const customHookDapps = useCustomHookDapps(isPreHook) const hookToEditDetails = useHookById(hookToEdit, isPreHook) - - // State for Search Input - const [searchQuery, setSearchQuery] = useState('') - - // Clear search input handler - const handleClearSearch = useCallback(() => { - setSearchQuery('') - }, []) - const internalHookDapps = useInternalHookDapps(isPreHook) - const currentDapps = useMemo(() => { - return isAllHooksTab ? internalHookDapps.concat(customHookDapps) : customHookDapps - }, [isAllHooksTab, internalHookDapps, customHookDapps]) + const currentDapps = useMemo( + () => (isAllHooksTab ? [...internalHookDapps, ...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) - }) + return currentDapps.filter(({ name = '', descriptionShort = '' }) => + [name, descriptionShort].some((text) => text.toLowerCase().includes(lowerQuery)), + ) }, [currentDapps, searchQuery]) + const sortedFilteredDapps = useMemo(() => { + const isCompatible = (dapp: HookDapp) => + !dapp.conditions?.walletCompatibility || dapp.conditions.walletCompatibility.includes(walletType) + return filteredDapps.sort((a, b) => (isCompatible(a) === isCompatible(b) ? 0 : isCompatible(a) ? -1 : 1)) + }, [filteredDapps, isSmartContractWallet]) + 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]) + const title = selectedDapp?.name || (dappDetails ? 'Hook description' : 'Hook Store') - // Handle modal dismiss const onDismissModal = useCallback(() => { if (hookToEdit) { setSelectedDapp(null) onDismiss() - return - } - - if (dappDetails) { + } else if (dappDetails) { setDappDetails(null) } else if (selectedDapp) { setSelectedDapp(null) @@ -97,33 +84,27 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore } }, [onDismiss, selectedDapp, dappDetails, hookToEdit]) - // Handle hookToEditDetails useEffect(() => { - if (!hookToEditDetails) { - setSelectedDapp(null) + if (hookToEditDetails) { + const foundDapp = findHookDappById(currentDapps, hookToEditDetails) + setSelectedDapp(foundDapp || null) } else { - setSelectedDapp(findHookDappById(currentDapps, hookToEditDetails) || null) + setSelectedDapp(null) } }, [hookToEditDetails, currentDapps]) - // Reset dappDetails when tab changes useEffect(() => { setDappDetails(null) }, [isAllHooksTab]) - // Handle add custom hook button - const handleAddCustomHook = useCallback(() => { - setIsAllHooksTab(false) - }, [setIsAllHooksTab]) + const handleAddCustomHook = () => setIsAllHooksTab(false) + const handleClearSearch = () => setSearchQuery('') - // 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 emptyListMessage = isAllHooksTab + ? searchQuery + ? 'No hooks match your search.' + : 'No hooks available.' + : "You haven't added any custom hooks yet. Add a custom hook to get started." const DappsListContent = ( <> @@ -153,13 +134,16 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore onClear={handleClearSearch} /> - {filteredDapps.length > 0 ? ( + {sortedFilteredDapps.length > 0 ? ( - {filteredDapps.map((dapp) => ( + {sortedFilteredDapps.map((dapp) => ( removeCustomHookDapp(dapp as HookDappIframe)} + walletType={ + isSmartContractWallet ? HookDappWalletCompatibility.SMART_CONTRACT : HookDappWalletCompatibility.EOA + } + onRemove={!isAllHooksTab ? () => removeCustomHookDapp(dapp as HookDappIframe) : undefined} onSelect={() => setSelectedDapp(dapp)} onOpenDetails={() => setDappDetails(dapp)} /> @@ -189,37 +173,29 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore onAddCustomHook={handleAddCustomHook} /> )} - {(() => { - if (selectedDapp) { - return ( - <> - - - - ) - } - - if (dappDetails) { - return setSelectedDapp(dappDetails)} /> - } - - return isAllHooksTab ? ( - DappsListContent - ) : ( - + + - {DappsListContent} - - ) - })()} + onDismiss={onDismiss} + dapp={selectedDapp} + hookToEdit={hookToEdit} + /> + + ) : dappDetails ? ( + setSelectedDapp(dappDetails)} walletType={walletType} /> + ) : isAllHooksTab ? ( + DappsListContent + ) : ( + + {DappsListContent} + + )} ) 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 c292d66ed9..9e9a787ca0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx @@ -12,9 +12,10 @@ import { HookDetailHeader } from '../HookDetailHeader' interface HookDappDetailsProps { dapp: HookDapp onSelect: Command + walletType: HookDappWalletCompatibility } -export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { +export function HookDappDetails({ dapp, onSelect, walletType }: HookDappDetailsProps) { const tags = useMemo(() => { const { version, website, type, conditions } = dapp const walletCompatibility = conditions?.walletCompatibility || [] @@ -60,7 +61,7 @@ export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { return ( - +

{dapp.description}

diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx index e5b56af885..9bc2878f4e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/index.tsx @@ -1,25 +1,43 @@ +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' + import * as styled from './styled' import { HookDapp } from '../../types/hooks' interface HookDetailHeaderProps { dapp: HookDapp + walletType: HookDappWalletCompatibility onSelect?: () => void iconSize?: number gap?: number padding?: string } -export function HookDetailHeader({ dapp, onSelect, iconSize, gap, padding }: HookDetailHeaderProps) { +export function HookDetailHeader({ dapp, walletType, onSelect, iconSize, gap, padding }: HookDetailHeaderProps) { const { name, image, descriptionShort } = dapp + const isCompatible = + !dapp.conditions?.walletCompatibility || + dapp.conditions.walletCompatibility.includes( + walletType === HookDappWalletCompatibility.EOA + ? HookDappWalletCompatibility.EOA + : HookDappWalletCompatibility.SMART_CONTRACT, + ) + return ( {name}

{name}

{descriptionShort} - {onSelect && Add} + {onSelect && + (isCompatible ? ( + Add + ) : ( + + n/a + + ))}
) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts index be49d95476..a87608ecf3 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDetailHeader/styled.ts @@ -57,20 +57,20 @@ export const Description = styled.span` } ` -export const AddButton = styled.button` - background: var(${UI.COLOR_PRIMARY}); - color: var(${UI.COLOR_PAPER}); +export const AddButton = styled.button<{ disabled?: boolean }>` + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY})`}; + color: ${({ disabled }) => `var(${disabled ? UI.COLOR_TEXT_OPACITY_50 : UI.COLOR_PAPER})`}; border: none; outline: none; font-weight: 600; font-size: 16px; padding: 11px; border-radius: 21px; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; transition: background 0.2s ease-in-out; margin: 16px 0 0; &:hover { - background: var(${UI.COLOR_PRIMARY_DARKEST}); + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY_DARKEST})`}; } ` 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 fb9293bdff..08a784ea2a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx @@ -1,4 +1,5 @@ import ICON_INFO from '@cowprotocol/assets/cow-swap/info.svg' +import { HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { Command } from '@cowprotocol/types' import SVG from 'react-inlinesvg' @@ -9,12 +10,13 @@ import { HookDapp } from '../../types/hooks' interface HookListItemProps { dapp: HookDapp + walletType: HookDappWalletCompatibility onSelect: Command onOpenDetails: Command onRemove?: Command } -export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookListItemProps) { +export function HookListItem({ dapp, walletType, onSelect, onOpenDetails, onRemove }: HookListItemProps) { const { name, descriptionShort, image, version } = dapp const handleItemClick = (event: React.MouseEvent) => { @@ -25,8 +27,15 @@ export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookLi } } + // If walletCompatibility is not defined, the hook is compatible with any wallet type + const isCompatible = + !dapp.conditions?.walletCompatibility || + dapp.conditions.walletCompatibility.includes( + walletType === 'EOA' ? HookDappWalletCompatibility.EOA : HookDappWalletCompatibility.SMART_CONTRACT, + ) + return ( - + {name} @@ -37,9 +46,15 @@ export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookLi

- - Add - + {isCompatible ? ( + + Add + + ) : ( + + n/a + + )} {onRemove ? ( Remove 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 a009113cac..ae02584cdb 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/styled.tsx @@ -17,13 +17,14 @@ const BaseButton = css` transition: all 0.2s ease-in-out; ` -export const LinkButton = styled.button` +export const LinkButton = styled.button<{ disabled?: boolean }>` ${BaseButton} - background: var(${UI.COLOR_PRIMARY}); - color: var(${UI.COLOR_PAPER}); + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY})`}; + color: ${({ disabled }) => `var(${disabled ? UI.COLOR_TEXT_OPACITY_50 : UI.COLOR_PAPER})`}; border: none; font-weight: 600; font-size: 16px; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; ${Media.upToSmall()} { width: 100%; @@ -31,7 +32,7 @@ export const LinkButton = styled.button` } &:hover { - background: var(${UI.COLOR_PRIMARY_DARKEST}); + background: ${({ disabled }) => `var(${disabled ? UI.COLOR_PRIMARY_OPACITY_10 : UI.COLOR_PRIMARY_DARKEST})`}; } ` @@ -48,7 +49,7 @@ export const RemoveButton = styled.button` } ` -export const HookDappListItem = styled.li<{ isDescriptionView?: boolean }>` +export const HookDappListItem = styled.li<{ isDescriptionView?: boolean; isCompatible?: boolean }>` width: 100%; background: transparent; display: flex; @@ -63,6 +64,21 @@ export const HookDappListItem = styled.li<{ isDescriptionView?: boolean }>` transition: all 0.2s ease-in-out; margin: 0; cursor: pointer; + background: ${({ isCompatible }) => (isCompatible ? `var(${UI.COLOR_PAPER})` : `var(${UI.COLOR_PAPER_DARKER})`)}; + + &::after { + content: ${({ isCompatible }) => (isCompatible ? 'none' : '"This hook is not compatible with your wallet"')}; + color: var(${UI.COLOR_ALERT_TEXT}); + font-size: 12px; + background-color: var(${UI.COLOR_ALERT_BG}); + padding: 4px 8px; + border-radius: 12px; + display: ${({ isCompatible }) => (isCompatible ? 'none' : 'block')}; + width: 100%; + text-align: center; + margin: 0 0 -8px; + } + &:hover { background: ${({ isDescriptionView }) => isDescriptionView ? 'transparent' : `var(${UI.COLOR_PRIMARY_OPACITY_10})`}; From 14a7ac1b9a43bc8e1bcceafe6ac7019871d4ae61 Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:04:29 +0100 Subject: [PATCH 2/2] feat: fix learn path on learn/articles --- apps/cow-fi/components/ArticlesList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cow-fi/components/ArticlesList.tsx b/apps/cow-fi/components/ArticlesList.tsx index 542c9ede40..7d0520b3c4 100644 --- a/apps/cow-fi/components/ArticlesList.tsx +++ b/apps/cow-fi/components/ArticlesList.tsx @@ -7,7 +7,7 @@ interface ArticlesListProps { articles: Article[] } -const ARTICLES_PATH = '/learn/articles/' +const ARTICLES_PATH = '/learn/' export const ArticlesList: React.FC = ({ articles }) => (