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(hooks-store): new design & refactoring #4859

Merged
merged 34 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a5bf768
chore: update eslint
shoom3301 Sep 3, 2024
abace98
refactor: align hook dapp files structure
shoom3301 Sep 3, 2024
50b780d
refactor: fix files structure
shoom3301 Sep 3, 2024
7c7a7a3
refactor: fix dependency inversion (hooksStore -> swap)
shoom3301 Sep 3, 2024
56b12a6
refactor: separate hooks store modal from buttons
shoom3301 Sep 3, 2024
e69d98c
chore: fix circular dependency
shoom3301 Sep 3, 2024
169354d
refactor: rename HooksStoreModal to HookRegistryList
shoom3301 Sep 3, 2024
9882f5c
refactor: make hook dapp context specific to dapp
shoom3301 Sep 4, 2024
b75237f
Merge branch 'develop' of https://github.com/cowprotocol/cowswap into…
shoom3301 Sep 4, 2024
2386d6c
refactor: extract pure components
shoom3301 Sep 4, 2024
61bd602
refactor: remove outputTokens
shoom3301 Sep 4, 2024
eac5dd8
feat(hooks-store): persist hooks into localStorage
shoom3301 Sep 4, 2024
9ea9781
feat(hooks-store): add styles
shoom3301 Sep 4, 2024
ee175c3
Merge branch 'develop' of https://github.com/cowprotocol/cowswap into…
shoom3301 Sep 5, 2024
67fc1f5
feat(hooks-store): style hook dapps
shoom3301 Sep 5, 2024
2899255
feat(hooks-store): style applied hook item
shoom3301 Sep 5, 2024
a1a22b3
chore: fix code style
shoom3301 Sep 6, 2024
b8f5ee0
chore: fix circular deps
shoom3301 Sep 6, 2024
1e49786
chore: fix code style
shoom3301 Sep 9, 2024
9a6b44e
fix(trade): hide top content for wrap flow
shoom3301 Sep 9, 2024
5671d04
fix(hooks-store): split state by chainId
shoom3301 Sep 9, 2024
04f8b85
fix(hooks-store): skip custom hooks in swap widget
shoom3301 Sep 9, 2024
a6d4fa9
chore(hooks-store): add uid to hook item title
shoom3301 Sep 9, 2024
c2c736e
fix(hooks-store): fix appData module dependencies
shoom3301 Sep 9, 2024
cb338c8
fix(hooks-store): bind state to account
shoom3301 Sep 9, 2024
58a68ad
fix(hooks-store): allow hooks in smart-contract wallets
shoom3301 Sep 10, 2024
074e6fd
chore: fix typo
shoom3301 Sep 10, 2024
5b99274
Merge branch 'develop' of https://github.com/cowprotocol/cowswap into…
shoom3301 Sep 11, 2024
aaea230
refactor: move hooks types to the module
shoom3301 Sep 11, 2024
75312cf
chore: fix lint
shoom3301 Sep 11, 2024
6b896cd
fix: permit data loading
shoom3301 Sep 11, 2024
f9a41b6
feat(hooks-store): general hooks widget styling and DnD (#4868)
fairlighteth Sep 11, 2024
b668136
chore: test commit
shoom3301 Sep 11, 2024
d9e11a5
chore: test commit
shoom3301 Sep 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading