Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@ describe('BridgingEnabledUpdater', () => {
expect(setIsBridgingEnabled).toHaveBeenCalledWith(false)
})

it('enables bridging on swap route when the wallet is compatible', () => {
it('enables bridging on swap route for a compatible wallet', () => {
render(<BridgingEnabledUpdater />)

expect(setIsBridgingEnabled).toHaveBeenCalledWith(true)
})

it('disables bridging on non-swap routes', () => {
mockUseTradeTypeInfo.mockReturnValue({ route: Routes.LIMIT_ORDER })

render(<BridgingEnabledUpdater />)

expect(setIsBridgingEnabled).toHaveBeenCalledWith(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { MobileChainPanelPortal } from './MobileChainPanelPortal'
import { InnerWrapper, ModalContainer, WidgetCard, WidgetOverlay, Wrapper } from './styled'

import { useCloseTokenSelectWidget } from '../../hooks/useCloseTokenSelectWidget'
import { ChainPanel } from '../../pure/ChainPanel'
import { ImportListModal } from '../../pure/ImportListModal'
import { ImportTokenModal } from '../../pure/ImportTokenModal'
Expand All @@ -26,6 +27,14 @@ export function SelectTokenWidget(props: SelectTokenWidgetProps): ReactNode {
const isCompactLayout = useMediaQuery(Media.upToMedium(false))
const [isMobileChainPanelOpen, setIsMobileChainPanelOpen] = useState(false)
const isChainPanelVisible = hasChainPanel && !isCompactLayout
const closeTokenSelectWidget = useCloseTokenSelectWidget()

// Cleanup: reset widget state on unmount
useEffect(() => {
return () => {
closeTokenSelectWidget({ overrideForceLock: true })
}
}, [closeTokenSelectWidget])

useEffect(() => {
if (!shouldRender) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ import { sortChainsByDisplayOrder } from '../utils/sortChainsByDisplayOrder'
* The array depends on sell/buy token selection.
* For the sell token we return all supported chains.
* For the buy token we return current network + all bridge target networks.
*
* Note: `isBridgingEnabled` reads from a Jotai atom, controlled by BridgingEnabledUpdater
* based on runtime checks (swap route + wallet compatibility).
*/
export function useChainsToSelect(): ChainsToSelectState | undefined {
const { chainId } = useWalletInfo()
const { field, selectedTargetChainId = chainId, tradeType } = useSelectTokenWidgetState()
const { data: bridgeSupportedNetworks, isLoading } = useBridgeSupportedNetworks()
const { areUnsupportedChainsEnabled } = useFeatureFlags()
const isBridgingEnabled = useIsBridgingEnabled()
const isBridgingEnabled = useIsBridgingEnabled() // Reads from Jotai atom
const availableChains = useAvailableChains()
const isAdvancedTradeType = tradeType === TradeType.LIMIT_ORDER || tradeType === TradeType.ADVANCED_ORDERS

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { createStore, Provider } from 'jotai'
import { ReactNode } from 'react'

import { act, renderHook } from '@testing-library/react'

import { useCloseTokenSelectWidget } from './useCloseTokenSelectWidget'

import { selectTokenWidgetAtom, updateSelectTokenWidgetAtom } from '../state/selectTokenWidgetAtom'

function createTestWrapper(store: ReturnType<typeof createStore>) {
return function TestWrapper({ children }: { children: ReactNode }) {
return <Provider store={store}>{children}</Provider>
}
}

describe('useCloseTokenSelectWidget', () => {
it('returns stable reference when forceOpen toggles', () => {
const store = createStore()
const wrapper = createTestWrapper(store)

const { result, rerender } = renderHook(() => useCloseTokenSelectWidget(), { wrapper })
const firstRef = result.current

// Toggle forceOpen to true
act(() => {
store.set(updateSelectTokenWidgetAtom, { forceOpen: true })
})
rerender()
expect(result.current).toBe(firstRef) // Same reference

// Toggle forceOpen back to false
act(() => {
store.set(updateSelectTokenWidgetAtom, { forceOpen: false })
})
rerender()
expect(result.current).toBe(firstRef) // Still same reference
})

it('does NOT reset state when forceOpen is true and overrideForceLock is not set', () => {
const store = createStore()
const wrapper = createTestWrapper(store)

const { result } = renderHook(() => useCloseTokenSelectWidget(), { wrapper })

// Set forceOpen = true, open = true
act(() => {
store.set(updateSelectTokenWidgetAtom, { forceOpen: true, open: true })
})

// Call without override - should NOT reset
act(() => {
result.current()
})
expect(store.get(selectTokenWidgetAtom).open).toBe(true)
})

it('resets state when forceOpen is true but overrideForceLock is set', () => {
const store = createStore()
const wrapper = createTestWrapper(store)

const { result } = renderHook(() => useCloseTokenSelectWidget(), { wrapper })

// Set forceOpen = true, open = true
act(() => {
store.set(updateSelectTokenWidgetAtom, { forceOpen: true, open: true })
})

// Call with override - SHOULD reset
act(() => {
result.current({ overrideForceLock: true })
})
expect(store.get(selectTokenWidgetAtom).open).toBe(false)
})

it('resets state when forceOpen is false', () => {
const store = createStore()
const wrapper = createTestWrapper(store)

const { result } = renderHook(() => useCloseTokenSelectWidget(), { wrapper })

// Set open = true, forceOpen = false
act(() => {
store.set(updateSelectTokenWidgetAtom, { open: true, forceOpen: false })
})

// Call without override - should reset because forceOpen is false
act(() => {
result.current()
})
expect(store.get(selectTokenWidgetAtom).open).toBe(false)
})

it('uses latest forceOpen value immediately (no stale closure)', () => {
const store = createStore()
const wrapper = createTestWrapper(store)

const { result, rerender } = renderHook(() => useCloseTokenSelectWidget(), { wrapper })

// Set open = true, forceOpen = false
act(() => {
store.set(updateSelectTokenWidgetAtom, { open: true, forceOpen: false })
})
rerender()

// Now toggle forceOpen to true in the same test
act(() => {
store.set(updateSelectTokenWidgetAtom, { forceOpen: true })
})
rerender()

// Calling without override should NOT reset because forceOpen is now true
act(() => {
result.current()
})
expect(store.get(selectTokenWidgetAtom).open).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useRef } from 'react'

import { useSelectTokenWidgetState } from './useSelectTokenWidgetState'
import { useUpdateSelectTokenWidgetState } from './useUpdateSelectTokenWidgetState'
Expand All @@ -11,12 +11,23 @@ export function useCloseTokenSelectWidget(): CloseTokenSelectWidget {
const updateSelectTokenWidget = useUpdateSelectTokenWidgetState()
const widgetState = useSelectTokenWidgetState()

// Ref to read forceOpen at call-time, not capture-time
// This makes the returned callback referentially stable
const forceOpenRef = useRef(widgetState.forceOpen)

// Synchronous update during render is intentional here:
// - We need the latest forceOpen value available immediately when closeTokenSelectWidget is called
// - Using useEffect would create a race condition where the ref has stale value during the same render cycle
// - This is safe because we're only reading/writing a ref, not causing side effects
// eslint-disable-next-line react-hooks/refs
forceOpenRef.current = widgetState.forceOpen

return useCallback(
(options?: { overrideForceLock?: boolean }) => {
if (widgetState.forceOpen && !options?.overrideForceLock) return
if (forceOpenRef.current && !options?.overrideForceLock) return

updateSelectTokenWidget(DEFAULT_SELECT_TOKEN_WIDGET_STATE)
},
[updateSelectTokenWidget, widgetState.forceOpen],
[updateSelectTokenWidget],
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ReactNode, useCallback, useMemo } from 'react'

import ICON_ORDERS from '@cowprotocol/assets/svg/orders.svg'
import { useFeatureFlags, useTheme } from '@cowprotocol/common-hooks'
import { useFeatureFlags, useIsBridgingEnabled, useTheme } from '@cowprotocol/common-hooks'
import { isInjectedWidget, maxAmountSpend } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { ButtonOutlined, Media, MY_ORDERS_ID, SWAP_HEADER_OFFSET } from '@cowprotocol/ui'
Expand Down Expand Up @@ -69,7 +69,8 @@ export function TradeWidgetForm(props: TradeWidgetProps): ReactNode {
const isLimitOrderTrade = tradeTypeInfo?.tradeType === TradeType.LIMIT_ORDER
const shouldLockForAlternativeOrder = isAlternativeOrderModalVisible && isLimitOrderTrade
const isWrapOrUnwrap = useIsWrapOrUnwrap()
const { isLimitOrdersUpgradeBannerEnabled, isBridgingEnabled } = useFeatureFlags()
const { isLimitOrdersUpgradeBannerEnabled } = useFeatureFlags()
const isBridgingEnabled = useIsBridgingEnabled()
const isCurrentTradeBridging = useIsCurrentTradeBridging()
const { darkMode } = useTheme()

Expand Down