Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prevent adding unsupported hooks for your wallet #5020

Merged
merged 8 commits into from
Oct 28, 2024
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -33,62 +34,48 @@ interface HookStoreModal {
export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStoreModal) {
const [selectedDapp, setSelectedDapp] = useState<HookDapp | null>(null)
const [dappDetails, setDappDetails] = useState<HookDapp | null>(null)

const [isAllHooksTab, setIsAllHooksTab] = useState<boolean>(true)
const [searchQuery, setSearchQuery] = useState<string>('')

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<string>('')

// 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])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

walletType is not present in useMemo() deps

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And isSmartContractWallet is not needed there


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)
Expand All @@ -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 = (
<>
Expand Down Expand Up @@ -153,13 +134,16 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore
onClear={handleClearSearch}
/>

{filteredDapps.length > 0 ? (
{sortedFilteredDapps.length > 0 ? (
<HookDappsList>
{filteredDapps.map((dapp) => (
{sortedFilteredDapps.map((dapp) => (
<HookListItem
key={isHookDappIframe(dapp) ? dapp.url : dapp.name}
dapp={dapp}
onRemove={isAllHooksTab ? undefined : () => removeCustomHookDapp(dapp as HookDappIframe)}
walletType={
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

walletType={walletType}

isSmartContractWallet ? HookDappWalletCompatibility.SMART_CONTRACT : HookDappWalletCompatibility.EOA
}
onRemove={!isAllHooksTab ? () => removeCustomHookDapp(dapp as HookDappIframe) : undefined}
onSelect={() => setSelectedDapp(dapp)}
onOpenDetails={() => setDappDetails(dapp)}
/>
Expand Down Expand Up @@ -189,37 +173,29 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore
onAddCustomHook={handleAddCustomHook}
/>
)}
{(() => {
if (selectedDapp) {
return (
<>
<HookDetailHeader dapp={selectedDapp} iconSize={58} gap={12} padding="24px 10px" />
<HookDappContainer
isPreHook={isPreHook}
onDismiss={onDismiss}
dapp={selectedDapp}
hookToEdit={hookToEdit}
/>
</>
)
}

if (dappDetails) {
return <HookDappDetails dapp={dappDetails} onSelect={() => setSelectedDapp(dappDetails)} />
}

return isAllHooksTab ? (
DappsListContent
) : (
<AddCustomHookForm
{selectedDapp ? (
<>
<HookDetailHeader dapp={selectedDapp} iconSize={58} gap={12} padding="24px 10px" walletType={walletType} />
<HookDappContainer
isPreHook={isPreHook}
isSmartContractWallet={isSmartContractWallet}
addHookDapp={addCustomHookDapp}
>
{DappsListContent}
</AddCustomHookForm>
)
})()}
onDismiss={onDismiss}
dapp={selectedDapp}
hookToEdit={hookToEdit}
/>
</>
) : dappDetails ? (
<HookDappDetails dapp={dappDetails} onSelect={() => setSelectedDapp(dappDetails)} walletType={walletType} />
) : isAllHooksTab ? (
DappsListContent
) : (
<AddCustomHookForm
isPreHook={isPreHook}
isSmartContractWallet={isSmartContractWallet}
addHookDapp={addCustomHookDapp}
>
{DappsListContent}
</AddCustomHookForm>
)}
</NewModal>
</Wrapper>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 || []
Expand Down Expand Up @@ -60,7 +61,7 @@ export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) {

return (
<styled.Wrapper>
<HookDetailHeader dapp={dapp} onSelect={onSelect} />
<HookDetailHeader dapp={dapp} onSelect={onSelect} walletType={walletType} />
<styled.Body>
<p>{dapp.description}</p>
</styled.Body>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is duplicated in HookListItem component, I suggest to move it in a util function

!dapp.conditions?.walletCompatibility ||
dapp.conditions.walletCompatibility.includes(
walletType === HookDappWalletCompatibility.EOA
? HookDappWalletCompatibility.EOA
: HookDappWalletCompatibility.SMART_CONTRACT,
)

return (
<styled.Header iconSize={iconSize} gap={gap} padding={padding}>
<img src={image} alt={name} />
<styled.Content>
<h3>{name}</h3>
<styled.Description>{descriptionShort}</styled.Description>
{onSelect && <styled.AddButton onClick={onSelect}>Add</styled.AddButton>}
{onSelect &&
(isCompatible ? (
<styled.AddButton onClick={onSelect}>Add</styled.AddButton>
) : (
<styled.AddButton disabled title="Not compatible with current wallet type">
n/a
</styled.AddButton>
))}
</styled.Content>
</styled.Header>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})`};
}
`
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<HTMLLIElement>) => {
Expand All @@ -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 (
<styled.HookDappListItem onClick={handleItemClick}>
<styled.HookDappListItem onClick={handleItemClick} isCompatible={isCompatible}>
<img src={image} alt={name} />

<styled.HookDappDetails onClick={onOpenDetails}>
Expand All @@ -37,9 +46,15 @@ export function HookListItem({ dapp, onSelect, onOpenDetails, onRemove }: HookLi
</p>
</styled.HookDappDetails>
<span>
<styled.LinkButton onClick={onSelect} className="link-button">
Add
</styled.LinkButton>
{isCompatible ? (
<styled.LinkButton onClick={onSelect} className="link-button">
Add
</styled.LinkButton>
) : (
<styled.LinkButton disabled title="Not compatible with current wallet type">
n/a
</styled.LinkButton>
)}
{onRemove ? (
<styled.RemoveButton onClick={onRemove} className="remove-button">
Remove
Expand Down
Loading
Loading