Skip to content

Commit

Permalink
feat(hooks-store): new design & refactoring (#4859)
Browse files Browse the repository at this point in the history
* chore: update eslint

* refactor: align hook dapp files structure

* refactor: fix files structure

* refactor: fix dependency inversion (hooksStore -> swap)

* refactor: separate hooks store modal from buttons

* chore: fix circular dependency

* refactor: rename HooksStoreModal to HookRegistryList

* refactor: make hook dapp context specific to dapp

* refactor: extract pure components

* refactor: remove outputTokens

* feat(hooks-store): persist hooks into localStorage

* feat(hooks-store): add styles

* feat(hooks-store): style hook dapps

* feat(hooks-store): style applied hook item

* chore: fix code style

* chore: fix circular deps

* chore: fix code style

* fix(trade): hide top content for wrap flow

* fix(hooks-store): split state by chainId

* fix(hooks-store): skip custom hooks in swap widget

* chore(hooks-store): add uid to hook item title

* fix(hooks-store): fix appData module dependencies

* fix(hooks-store): bind state to account

* fix(hooks-store): allow hooks in smart-contract wallets

* chore: fix typo

* refactor: move hooks types to the module

* chore: fix lint

* fix: permit data loading

* feat(hooks-store): general hooks widget styling and DnD (#4868)

* chore: test commit

* chore: test commit

---------

Co-authored-by: fairlight <[email protected]>
  • Loading branch information
shoom3301 and fairlighteth authored Sep 11, 2024
1 parent 8cfd70b commit 1b989fa
Show file tree
Hide file tree
Showing 81 changed files with 1,789 additions and 1,067 deletions.
3 changes: 1 addition & 2 deletions apps/cowswap-frontend/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// this is not used for now. we use "craco test", but eventually we will

/* eslint-disable */
export default {
displayName: 'cowswap',
preset: '../../jest.preset.js',
Expand All @@ -12,5 +11,5 @@ export default {
coverageDirectory: '../../coverage/cowswap',
setupFilesAfterEnv: ['../../jest.setup.ts'],
setupFiles: ['dotenv/config'],
transformIgnorePatterns: ['node_modules/(?!@ledgerhq/connect-kit-loader)'],
transformIgnorePatterns: ['/node_modules/(?!react-dnd|dnd-core|@react-dnd)'],
}
25 changes: 19 additions & 6 deletions apps/cowswap-frontend/src/common/pure/NewModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ModalInner = styled.div`
background: transparent;
padding: 0;
position: relative;
`

const Wrapper = styled.div<{
Expand Down Expand Up @@ -91,10 +92,9 @@ const BackButtonStyled = styled(BackButton)`
left: 10px;
`

const NewModalContent = styled.div<{ padding?: string }>`
const NewModalContent = styled.div<{ padding?: string, justifyContent?: string }>`
display: flex;
align-items: center;
justify-content: center;
justify-content: ${({ justifyContent }) => justifyContent || 'center'};
flex-flow: column wrap;
flex: 1;
width: 100%;
Expand Down Expand Up @@ -156,17 +156,28 @@ export const NewModalContentBottom = styled(NewModalContentTop)`
export interface NewModalProps {
maxWidth?: number
minHeight?: number
contentPadding?: string
title?: string
onDismiss?: Command
children?: React.ReactNode
modalMode?: boolean
justifyContent?: string
}

export function NewModal({ maxWidth = 450, minHeight = 350, modalMode, title, children, onDismiss }: NewModalProps) {
export function NewModal({
maxWidth = 450,
minHeight = 350,
contentPadding,
justifyContent,
modalMode,
title,
children,
onDismiss,
}: NewModalProps) {
const onDismissCallback = useCallback(() => onDismiss?.(), [onDismiss])

return (
<Wrapper maxWidth={maxWidth} minHeight={minHeight} modalMode={modalMode}>
<Wrapper maxWidth={maxWidth} minHeight={minHeight} modalMode={modalMode} >
<ModalInner>
{!modalMode && <BackButtonStyled onClick={onDismissCallback} />}
{title && (
Expand All @@ -180,7 +191,9 @@ export function NewModal({ maxWidth = 450, minHeight = 350, modalMode, title, ch
</Heading>
)}

<NewModalContent className={modalMode ? 'modalMode' : ''}>{children}</NewModalContent>
<NewModalContent className={modalMode ? 'modalMode' : ''} padding={contentPadding} justifyContent={justifyContent}>
{children}
</NewModalContent>
</ModalInner>
</Wrapper>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,76 +1,86 @@
import { useEffect, useMemo, useRef } from 'react'
import { useEffect, useRef } from 'react'

import { latest } from '@cowprotocol/app-data'
import { getIsNativeToken } from '@cowprotocol/common-utils'
import { PermitHookData } from '@cowprotocol/permit-utils'
import { useIsSmartContractWallet } from '@cowprotocol/wallet'

import { Nullish } from 'types'

import { useHooks } from 'modules/hooksStore'
import { useAccountAgnosticPermitHookData } from 'modules/permit'
import { useDerivedSwapInfo } from 'modules/swap/hooks/useSwapState'
import { useDerivedTradeState, useHasTradeEnoughAllowance, useIsHooksTradeType, useIsSellNative } from 'modules/trade'

import { useLimitHasEnoughAllowance } from '../../limitOrders/hooks/useLimitHasEnoughAllowance'
import { useSwapEnoughAllowance } from '../../swap/hooks/useSwapFlowContext'
import { useUpdateAppDataHooks } from '../hooks'
import { TypedAppDataHooks, TypedCowHook } from '../types'
import { buildAppDataHooks } from '../utils/buildAppDataHooks'
import { cowHookToTypedCowHook } from '../utils/typedHooks'

type OrderInteractionHooks = latest.OrderInteractionHooks

function useAgnosticPermitDataIfUserHasNoAllowance(): PermitHookData | undefined {
const { target, callData, gasLimit } = useAccountAgnosticPermitHookData() || {}
function useAgnosticPermitDataIfUserHasNoAllowance(): Nullish<PermitHookData> {
const hookData = useAccountAgnosticPermitHookData()

// Remove permitData if the user has enough allowance for the current trade
const swapHasEnoughAllowance = useSwapEnoughAllowance()
const limitHasEnoughAllowance = useLimitHasEnoughAllowance()
const shouldUsePermit = swapHasEnoughAllowance === false || limitHasEnoughAllowance === false
const hasTradeEnoughAllowance = useHasTradeEnoughAllowance()

return useMemo(() => {
if (!target || !callData || !gasLimit) {
return undefined
}
if (hasTradeEnoughAllowance === undefined) return undefined

return shouldUsePermit ? { target, callData, gasLimit } : undefined
}, [shouldUsePermit, target, callData, gasLimit])
const shouldUsePermit = hasTradeEnoughAllowance === false

return shouldUsePermit ? hookData : null
}

export function AppDataHooksUpdater(): null {
const { trade } = useDerivedSwapInfo()
const { preHooks, postHooks } = useHooks()
const tradeState = useDerivedTradeState()
const isHooksTradeType = useIsHooksTradeType()
const hooksStoreState = useHooks()
const preHooks = isHooksTradeType ? hooksStoreState.preHooks : null
const postHooks = isHooksTradeType ? hooksStoreState.postHooks : null
const updateAppDataHooks = useUpdateAppDataHooks()
const permitData = useAgnosticPermitDataIfUserHasNoAllowance()
const hooksPrev = useRef<OrderInteractionHooks | undefined>(undefined)
const hasTradeInfo = !!trade
const hasTradeInfo = !!tradeState
// This is already covered up the dependency chain, but it still slips through some times
// Adding this additional check here to try to prevent a race condition to ever allowing this to pass through
const isSmartContractWallet = useIsSmartContractWallet()
// Remove hooks if the order is selling native. There's no need for approval
const isNativeSell = trade?.inputAmount.currency ? getIsNativeToken(trade?.inputAmount.currency) : false
const isNativeSell = useIsSellNative()

useEffect(() => {
const preInteractionHooks = preHooks.map<TypedCowHook>((hookDetails) =>
cowHookToTypedCowHook(hookDetails.hook, 'hookStore')
const preInteractionHooks = (preHooks || []).map<TypedCowHook>((hookDetails) =>
cowHookToTypedCowHook(hookDetails.hook, 'hookStore'),
)
const postInteractionHooks = postHooks.map<TypedCowHook>((hookDetails) =>
cowHookToTypedCowHook(hookDetails.hook, 'hookStore')
const postInteractionHooks = (postHooks || []).map<TypedCowHook>((hookDetails) =>
cowHookToTypedCowHook(hookDetails.hook, 'hookStore'),
)

// Permit data is not loaded yet, wait until it's loaded
if (permitData === undefined) {
return
}

// Add permit hook
if (permitData && !isSmartContractWallet) {
preInteractionHooks.push(cowHookToTypedCowHook(permitData, 'permit'))
}

const hooks = buildAppDataHooks<TypedCowHook[], TypedAppDataHooks>({
preInteractionHooks: permitData
? preInteractionHooks.concat([cowHookToTypedCowHook(permitData, 'permit')])
: preInteractionHooks,
preInteractionHooks,
postInteractionHooks,
})

if (
const areHooksChanged = JSON.stringify(hooksPrev.current) !== JSON.stringify(hooks)

const shouldNotUpdateHooks =
!hasTradeInfo || // If there's no trade info, wait until we have one to update the hooks (i.e. missing quote)
isSmartContractWallet === undefined || // We don't know what type of wallet it is, wait until it's defined
JSON.stringify(hooksPrev.current) === JSON.stringify(hooks) // Or if the hooks has not changed
) {
isSmartContractWallet === undefined // We don't know what type of wallet it is, wait until it's defined

if (shouldNotUpdateHooks && !areHooksChanged) {
return undefined
}

if (!isSmartContractWallet && !isNativeSell && hooks) {
// Hooks are not available for eth-flow orders now
if (hooks && !isNativeSell) {
// Update the hooks
updateAppDataHooks(hooks)
hooksPrev.current = hooks
Expand All @@ -79,7 +89,16 @@ export function AppDataHooksUpdater(): null {
updateAppDataHooks(undefined)
hooksPrev.current = undefined
}
}, [updateAppDataHooks, permitData, hasTradeInfo, isSmartContractWallet, isNativeSell, preHooks, postHooks])
}, [
updateAppDataHooks,
permitData,
hasTradeInfo,
isSmartContractWallet,
isNativeSell,
preHooks,
postHooks,
isHooksTradeType,
])

return null
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { PERMIT_SIGNER } from '@cowprotocol/permit-utils'
import { CowHook } from '@cowprotocol/types'

import { OrderInteractionHooks } from '../types'
import { CowHook, OrderInteractionHooks } from '../types'

export type HooksFilter = (cowHook: CowHook) => boolean

Expand All @@ -25,7 +24,7 @@ export const filterPermitSignerPermit: HooksFilter = (cowHook: CowHook): boolean
export function filterHooks(
hooks: OrderInteractionHooks | undefined,
preHooksFilter: HooksFilter | undefined,
postHooksFilter: HooksFilter | undefined
postHooksFilter: HooksFilter | undefined,
): OrderInteractionHooks | undefined {
if (!hooks) {
return hooks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function RoutesApp() {
<Route path="profile" element={<Navigate to={RoutesEnum.ACCOUNT} />} />

{/*Swap*/}
<Route path={RoutesEnum.SWAP} element={<SwapPage hooksEnabled={false} />} />
<Route path={RoutesEnum.SWAP} element={<SwapPage />} />
<Route path={RoutesEnum.HOOKS} element={<HooksPage />} />
<Route path={RoutesEnum.SEND} element={<RedirectPathToSwapOnly />} />

Expand Down
3 changes: 0 additions & 3 deletions apps/cowswap-frontend/src/modules/hooksStore/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
export const HOOKS_TRAMPOLINE_ADDRESS = '0x01dcb88678aedd0c4cc9552b20f4718550250574'
export const SBC_DEPOSIT_CONTRACT_ADDRESS = '0x0B98057eA310F4d31F2a452B414647007d1645d9'

// Sorry Safe, you need to set up CORS policy :)
// TODO: run our own instance
export const TENDERLY_SIMULATE_ENDPOINT_URL =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useMemo } from 'react'

import { Command } from '@cowprotocol/types'
import { useWalletInfo } from '@cowprotocol/wallet'

import { useAddHook } from '../../hooks/useAddHook'
import { useEditHook } from '../../hooks/useEditHook'
import { useHookById } from '../../hooks/useHookById'
import { HookDapp, HookDappContext as HookDappContextType } from '../../types/hooks'
import { isHookDappIframe } from '../../utils'

interface HookDappContainerProps {
dapp: HookDapp
isPreHook: boolean
onDismiss: Command
onDismissModal: Command
hookToEdit?: string
}

export function HookDappContainer({ dapp, isPreHook, onDismiss, onDismissModal, hookToEdit }: HookDappContainerProps) {
const { chainId, account } = useWalletInfo()
const addHook = useAddHook(dapp, isPreHook)
const editHook = useEditHook()

const hookToEditDetails = useHookById(hookToEdit, isPreHook)

const context = useMemo<HookDappContextType>(() => {
return {
chainId,
account,
hookToEdit: hookToEditDetails,
editHook: (...args) => {
editHook(...args)
onDismiss()
},
addHook: (hookToAdd) => {
const hook = addHook(hookToAdd)
onDismiss()

return hook
},
close: onDismissModal,
}
}, [addHook, editHook, onDismiss, onDismissModal, chainId, account, hookToEditDetails])

const dappProps = useMemo(() => ({ context, dapp, isPreHook }), [context, dapp, isPreHook])

if (isHookDappIframe(dapp)) {
// TODO: Create iFrame
return <>{dapp.name}</>
}

return dapp.component(dappProps)
}
Loading

0 comments on commit 1b989fa

Please sign in to comment.