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}
{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 (
-
+
@@ -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 }) => (