diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9ffe5a9970..840b20012a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { - "apps/cowswap-frontend": "1.60.0", + "apps/cowswap-frontend": "1.61.1", "apps/explorer": "2.28.0", "libs/permit-utils": "0.1.2", "libs/widget-lib": "0.5.0", diff --git a/apps/cowswap-frontend/CHANGELOG.md b/apps/cowswap-frontend/CHANGELOG.md index 0dbd0f6557..3dca8f2df1 100644 --- a/apps/cowswap-frontend/CHANGELOG.md +++ b/apps/cowswap-frontend/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [1.61.1](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.61.0...cowswap-v1.61.1) (2024-03-04) + + +### Bug Fixes + +* **eth-flow:** prevent orders with expired quotes ([#3965](https://github.com/cowprotocol/cowswap/issues/3965)) ([f965020](https://github.com/cowprotocol/cowswap/commit/f965020cec5e0901138130e050939cc912ca4fd8)) + +## [1.61.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.60.0...cowswap-v1.61.0) (2024-02-29) + + +### Features + +* **appzi:** new appzi survey for limit orders ([#3918](https://github.com/cowprotocol/cowswap/issues/3918)) ([99e004a](https://github.com/cowprotocol/cowswap/commit/99e004ad410aefacfd2090423ef2e480ed48302e)) +* **settings:** remove expert mode ([#3905](https://github.com/cowprotocol/cowswap/issues/3905)) ([4f98075](https://github.com/cowprotocol/cowswap/commit/4f98075dbcacdbf49e9f43fb2a0936411d6a7365)) + + +### Bug Fixes + +* always open hash links in new tab ([#3888](https://github.com/cowprotocol/cowswap/issues/3888)) ([0275de9](https://github.com/cowprotocol/cowswap/commit/0275de9f3ec04f697878b0507a5b09c8842aa7ba)) +* increase quote refresh interval for Swap and TWAP ([#3935](https://github.com/cowprotocol/cowswap/issues/3935)) ([55b5e22](https://github.com/cowprotocol/cowswap/commit/55b5e22b8ad0edba5e8c114fcd11f8caa39b5ab4)) +* **limit-orders:** price updated warning should not be displayed ([#3925](https://github.com/cowprotocol/cowswap/issues/3925)) ([7a5b64d](https://github.com/cowprotocol/cowswap/commit/7a5b64d9c0c0d7e44aa15502735c843d43bc1fc7)) +* **twap:** allow creating orders with 100% slippage ([#3897](https://github.com/cowprotocol/cowswap/issues/3897)) ([05a604d](https://github.com/cowprotocol/cowswap/commit/05a604d0fd92f0ca0295b1ccef0cf4c7725ab24c)) + ## [1.60.0](https://github.com/cowprotocol/cowswap/compare/cowswap-v1.59.2...cowswap-v1.60.0) (2024-02-22) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 96a4e786ec..b9feb566b2 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/cowswap", - "version": "1.60.0", + "version": "1.61.1", "description": "CoW Swap", "main": "index.js", "author": "", diff --git a/apps/cowswap-frontend/src/common/constants/common.ts b/apps/cowswap-frontend/src/common/constants/common.ts index 151381d3f4..a0bd34d461 100644 --- a/apps/cowswap-frontend/src/common/constants/common.ts +++ b/apps/cowswap-frontend/src/common/constants/common.ts @@ -5,8 +5,6 @@ import ms from 'ms.macro' export const HIGH_FEE_WARNING_PERCENTAGE = new Percent(1, 10) -export const SAFE_COW_APP_LINK = 'https://app.safe.global/share/safe-app?appUrl=https%3A%2F%2Fswap.cow.fi&chain=eth' - export const MAX_ORDER_DEADLINE = ms`182d` + ms`12h` // 6 months, matching backend's https://github.com/cowprotocol/infrastructure/blob/901ed8e2fe3ea57956585f107bdd7539c2e7d3d1/services/Pulumi.yaml#L15 // Use a 150K gas as a fallback if there's issue calculating the gas estimation (fixes some issues with some nodes failing to calculate gas costs for SC wallets) diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts b/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts index c4471bf592..9df762310d 100644 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts +++ b/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveCallback.ts @@ -45,14 +45,15 @@ export function useTradeApproveCallback(amountToApprove?: CurrencyAmount { console.error('Error setting the allowance for token', error) - if (!isRejectRequestProviderError(error)) { + if (isRejectRequestProviderError(error)) { + updateTradeApproveState({ error: 'User rejected approval transaction' }) + } else { const errorCode = error?.code && typeof error.code === 'number' ? error.code : null approvalAnalytics('Error', symbol, errorCode) + updateTradeApproveState({ error: typeof error === 'string' ? error : error.message || error.toString() }) } - updateTradeApproveState({ error: typeof error === 'string' ? error : error.message || error.toString() }) - throw error }) }, diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/index.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/index.tsx index a1584a1c6d..10431f1f9a 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/index.tsx @@ -7,24 +7,29 @@ import * as styledEl from './styled' export interface CurrencyArrowSeparatorProps { isLoading: boolean + disabled?: boolean withRecipient: boolean hasSeparatorLine?: boolean isCollapsed?: boolean onSwitchTokens(): void - border?: boolean } export function CurrencyArrowSeparator(props: CurrencyArrowSeparatorProps) { - const { isLoading, onSwitchTokens, withRecipient, isCollapsed = true, hasSeparatorLine } = props + const { isLoading, onSwitchTokens, withRecipient, isCollapsed = true, hasSeparatorLine, disabled = false } = props const isInjectedWidgetMode = isInjectedWidget() return ( - + {!isInjectedWidgetMode && isLoading ? ( ) : ( - + )} diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/styled.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/styled.tsx index b3fc2142e8..595405b418 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/styled.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencyArrowSeparator/styled.tsx @@ -5,10 +5,15 @@ import styled, { css } from 'styled-components/macro' import { loadingAnimationMixin } from './style-mixins' -export const Box = styled.div<{ withRecipient: boolean; isCollapsed: boolean; hasSeparatorLine?: boolean }>` +export const Box = styled.div<{ + withRecipient: boolean + isCollapsed: boolean + hasSeparatorLine?: boolean + disabled: boolean +}>` display: ${({ withRecipient }) => (withRecipient ? 'inline-flex' : 'block')}; margin: ${({ withRecipient, isCollapsed }) => (withRecipient ? '0' : isCollapsed ? '-13px auto' : '2px auto')}; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'inherit' : 'pointer')}; color: inherit; position: relative; z-index: 2; @@ -58,7 +63,7 @@ export const LoadingWrapper = styled.div<{ isLoading: boolean }>` ${({ isLoading }) => isLoading && loadingAnimationMixin} ` -export const ArrowDownIcon = styled(ArrowDown)` +export const ArrowDownIcon = styled(ArrowDown)<{ disabled: boolean }>` display: block; margin: auto; stroke: currentColor; @@ -66,7 +71,7 @@ export const ArrowDownIcon = styled(ArrowDown)` padding: 0; height: 100%; width: 20px; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'inherit' : 'pointer')}; color: inherit; ` diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx index 93891b65b1..578b5a37b3 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx @@ -4,8 +4,7 @@ import { setMaxSellTokensAnalytics } from '@cowprotocol/analytics' import { NATIVE_CURRENCIES } from '@cowprotocol/common-const' import { formatInputAmount, getIsNativeToken } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { TokenAmount } from '@cowprotocol/ui' -import { MouseoverTooltip } from '@cowprotocol/ui' +import { MouseoverTooltip, TokenAmount } from '@cowprotocol/ui' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -34,6 +33,7 @@ export interface CurrencyInputPanelProps extends Partial { isChainIdUnsupported: boolean disabled?: boolean inputDisabled?: boolean + tokenSelectorDisabled?: boolean inputTooltip?: string showSetMax?: boolean maxBalance?: CurrencyAmount | undefined @@ -58,6 +58,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) { showSetMax = false, maxBalance, inputDisabled = false, + tokenSelectorDisabled = false, inputTooltip, onUserInput, allowsOffchainSigning, @@ -153,6 +154,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) { } currency={disabled ? undefined : currency || undefined} loading={areCurrenciesLoading || disabled} + readonlyMode={tokenSelectorDisabled} />
diff --git a/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx b/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx index 3fde63ec5f..0a53f8d493 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencySelectButton/styled.tsx @@ -36,6 +36,7 @@ export const CurrencySelectWrapper = styled.button<{ isLoading: boolean; $stubbe pointer-events: ${({ readonlyMode }) => (readonlyMode ? 'none' : '')}; border-radius: var(${UI.BORDER_RADIUS_NORMAL}); padding: 6px; + ${({ readonlyMode }) => (readonlyMode ? 'padding-right: 10px;' : '')} transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out; max-width: 190px; diff --git a/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx b/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx index 73ae0ae1f5..e3fc5c7be9 100644 --- a/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx @@ -18,13 +18,18 @@ const ModalInner = styled.div` position: relative; ` -const Wrapper = styled.div<{ maxWidth?: number | string; minHeight?: number | string }>` +const Wrapper = styled.div<{ + maxWidth?: number | string + minHeight?: number | string + modalMode?: boolean +}>` display: flex; width: 100%; height: 100%; margin: auto; overflow-y: auto; - background: var(${UI.COLOR_PAPER}); + background: ${({ modalMode }) => (modalMode ? `var(${UI.COLOR_PAPER_DARKER})` : `var(${UI.COLOR_PAPER})`)}; + border: ${({ modalMode }) => (modalMode ? `1px solid var(${UI.COLOR_PAPER})` : 'none')}; border-radius: var(${UI.BORDER_RADIUS_NORMAL}); box-shadow: var(${UI.BOX_SHADOW}); @@ -49,11 +54,12 @@ const Wrapper = styled.div<{ maxWidth?: number | string; minHeight?: number | st const Heading = styled.h2` display: flex; + flex-flow: row wrap; justify-content: space-between; width: 100%; height: auto; margin: 0; - padding: 18px 40px; + padding: 16px 20px 3px; font-size: var(${UI.FONT_SIZE_MEDIUM}); ${({ theme }) => theme.mediaWidth.upToSmall` @@ -63,13 +69,10 @@ const Heading = styled.h2` ` const IconX = styled.div` - position: absolute; - top: 16px; - right: 10px; cursor: pointer; opacity: 0.7; transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; - margin: 0 0 0 auto; + margin: 0; > svg { width: var(${UI.ICON_SIZE_NORMAL}); @@ -88,7 +91,7 @@ const BackButtonStyled = styled(BackButton)` left: 10px; ` -const NewModalContent = styled.div<{ paddingTop?: number }>` +const NewModalContent = styled.div<{ padding?: string }>` display: flex; align-items: center; justify-content: center; @@ -96,15 +99,19 @@ const NewModalContent = styled.div<{ paddingTop?: number }>` flex: 1; width: 100%; height: 100%; - padding: 0 var(${UI.PADDING_NORMAL}) var(${UI.PADDING_NORMAL}); + padding: ${({ padding }) => padding || `0 var(${UI.PADDING_NORMAL}) var(${UI.PADDING_NORMAL})`}; + + &.modalMode { + padding: 10px; + } h1, h2, h3 { width: 100%; - font-size: var(${UI.FONT_SIZE_LARGER}); + font-size: var(${UI.FONT_SIZE_MEDIUM}); font-weight: var(${UI.FONT_WEIGHT_BOLD}); - text-align: center; + text-align: left; line-height: 1.4; margin: 0 auto; } @@ -159,17 +166,21 @@ export function NewModal({ maxWidth = 450, minHeight = 350, modalMode, title, ch const onDismissCallback = useCallback(() => onDismiss?.(), [onDismiss]) return ( - + {!modalMode && } - {title && {title}} - {modalMode && ( - - - + {title && ( + + {title}{' '} + {modalMode && ( + + + + )} + )} - {children} + {children} ) diff --git a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts index 798c0e7bd9..b6f4ee84e6 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts @@ -5,15 +5,15 @@ import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { useDispatch, useSelector } from 'react-redux' import { - updateQuote, - UpdateQuoteParams, ClearQuoteParams, getNewQuote, GetQuoteParams, refreshQuote, - SetQuoteErrorParams, - setQuoteError, RefreshQuoteParams, + setQuoteError, + SetQuoteErrorParams, + updateQuote, + UpdateQuoteParams, } from './actions' import { QuoteInformationObject, QuotesMap } from './reducer' diff --git a/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts b/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts index d9b8629bd0..c58a806b17 100644 --- a/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts +++ b/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts @@ -14,7 +14,7 @@ interface FeeInformation { amount: string } -export type FeeForTrade = { feeAsCurrency: CurrencyAmount } & Pick +export type FeeForTrade = { feeAsCurrency: CurrencyAmount } & FeeInformation type TradeExecutionPrice = CanonicalMarketParams | undefined> & { price?: PriceInformation } diff --git a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx index cf6fb72d35..be106c7004 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx @@ -6,7 +6,15 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useENS } from '@cowprotocol/ens' import { TokenLogo, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' import { UiOrderType } from '@cowprotocol/types' -import { ExternalLink, TokenAmount, UI } from '@cowprotocol/ui' +import { + ExternalLink, + TokenAmount, + UI, + Icon, + IconType, + BannerOrientation, + CustomRecipientWarningBanner, +} from '@cowprotocol/ui' import { CurrencyAmount } from '@uniswap/sdk-core' import { OrderProgressBar } from 'legacy/components/OrderProgressBar' @@ -19,8 +27,6 @@ import { EthFlowStepper } from 'modules/swap/containers/EthFlowStepper' import { useCancelOrder } from 'common/hooks/useCancelOrder' import { isPending } from 'common/hooks/useCategorizeRecentActivity' import { useGetSurplusData } from 'common/hooks/useGetSurplusFiatValue' -import { Icon, IconType } from 'common/pure/Icon' -import { BannerOrientation, CustomRecipientWarningBanner } from 'common/pure/InlineBanner/banners' import { RateInfo, RateInfoParams } from 'common/pure/RateInfo' import { SafeWalletLink } from 'common/pure/SafeWalletLink' import { diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index 9cb381b429..5f50fc025b 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -10,6 +10,7 @@ import { EthFlowDeadlineUpdater, EthFlowSlippageUpdater } from 'modules/swap/sta import { useOnTokenListAddingError } from 'modules/tokensList' import { UsdPricesUpdater } from 'modules/usdAmount' +import { useFeatureFlags } from 'common/hooks/featureFlags/useFeatureFlags' import { TotalSurplusUpdater } from 'common/state/totalSurplusState' import { ApplicationUpdater } from 'common/updaters/ApplicationUpdater' import { CancelReplaceTxUpdater } from 'common/updaters/CancelReplaceTxUpdater' @@ -34,6 +35,7 @@ export function Updaters() { const { chainId, account } = useWalletInfo() const { tokenLists, appCode } = useInjectedWidgetParams() const onTokenListAddingError = useOnTokenListAddingError() + const { isGeoBlockEnabled } = useFeatureFlags() return ( <> @@ -62,7 +64,7 @@ export function Updaters() { - + - diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx index 9f21517eca..3a84037d04 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx @@ -2,6 +2,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import React, { useCallback, useEffect } from 'react' import { isFractionFalsy } from '@cowprotocol/common-utils' +import { BundleTxApprovalBanner, BundleTxSafeWcBanner, SmallVolumeWarningBanner } from '@cowprotocol/ui' import { useIsSafeViaWc, useWalletInfo } from '@cowprotocol/wallet' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' @@ -23,11 +24,6 @@ import { useTradeQuote } from 'modules/tradeQuote' import { useShouldZeroApprove } from 'modules/zeroApproval' import { HIGH_FEE_WARNING_PERCENTAGE } from 'common/constants/common' -import { - BundleTxApprovalBanner, - BundleTxSafeWcBanner, - SmallVolumeWarningBanner, -} from 'common/pure/InlineBanner/banners' import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' import { calculatePercentageInRelationToReference } from 'utils/orderUtils/calculatePercentageInRelationToReference' diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions.ts b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions.ts index f5b3cbfcb2..9312491739 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/hooks/useLimitOrdersWidgetActions.ts @@ -1,4 +1,4 @@ -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai' import { useCallback } from 'react' import { changeSwapAmountAnalytics } from '@cowprotocol/analytics' @@ -7,8 +7,8 @@ import { OrderKind } from '@cowprotocol/cow-sdk' import { Field } from 'legacy/state/types' -import { updateLimitOrdersRawStateAtom } from 'modules/limitOrders' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' +import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' import { useUpdateCurrencyAmount } from 'modules/limitOrders/hooks/useUpdateCurrencyAmount' import { limitRateAtom } from 'modules/limitOrders/state/limitRateAtom' import { TradeWidgetActions } from 'modules/trade' @@ -22,7 +22,7 @@ export function useLimitOrdersWidgetActions(): TradeWidgetActions { const isWrapOrUnwrap = useIsWrapOrUnwrap() const updateCurrencyAmount = useUpdateCurrencyAmount() - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() const onCurrencySelection = useOnCurrencySelection() diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index 43a22b8d93..e980846232 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -1,4 +1,4 @@ -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai' import React, { useMemo } from 'react' import { isSellOrder } from '@cowprotocol/common-utils' @@ -21,9 +21,9 @@ import * as styledEl from './styled' import { useLimitOrdersDerivedState } from '../../hooks/useLimitOrdersDerivedState' import { LimitOrdersFormState, useLimitOrdersFormState } from '../../hooks/useLimitOrdersFormState' +import { useUpdateLimitOrdersRawState } from '../../hooks/useLimitOrdersRawState' import { useTradeFlowContext } from '../../hooks/useTradeFlowContext' import { InfoBanner } from '../../pure/InfoBanner' -import { updateLimitOrdersRawStateAtom } from '../../state/limitOrdersRawStateAtom' import { limitOrdersSettingsAtom } from '../../state/limitOrdersSettingsAtom' import { limitRateAtom } from '../../state/limitRateAtom' import { DeadlineInput } from '../DeadlineInput' @@ -77,7 +77,8 @@ export function LimitOrdersWidget() { const rateInfoParams = useRateInfoParams(inputCurrencyAmount, outputCurrencyAmount) const widgetActions = useLimitOrdersWidgetActions() - const { showRecipient } = settingsState + const { showRecipient: showRecipientSetting } = settingsState + const showRecipient = showRecipientSetting || !!recipient const priceImpact = useTradePriceImpact() const quoteAmount = useMemo( @@ -157,7 +158,7 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { return isRateLoading }, [isRateLoading, inputCurrency, outputCurrency]) - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() const inputCurrencyPreviewInfo = { amount: inputCurrencyInfo.amount, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx index f7cd7ede74..d89aee62c3 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx @@ -2,7 +2,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback, useEffect, useMemo, useState } from 'react' import { formatInputAmount, getAddress, isFractionFalsy } from '@cowprotocol/common-utils' -import { TokenSymbol, Loader } from '@cowprotocol/ui' +import { Loader, TokenSymbol } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' import { RefreshCw } from 'react-feather' @@ -70,6 +70,7 @@ export function RateInput() { activeRate: isFractionFalsy(marketRate) ? initialRate : marketRate, isTypedValue: false, isRateFromUrl: false, + isAlternativeOrderRate: false, }) }, [marketRate, initialRate, updateRate]) @@ -81,6 +82,7 @@ export function RateInput() { activeRate: toFraction(typedValue, isInverted), isTypedValue: true, isRateFromUrl: false, + isAlternativeOrderRate: false, }) }, [isInverted, updateRate, updateLimitRateState] diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.test.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.test.ts index 9863cc3fb4..3edeb7fc8e 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.test.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.test.ts @@ -1,5 +1,3 @@ -import { useAtomValue, useSetAtom } from 'jotai' - import { useIsBundlingSupported } from '@cowprotocol/wallet' import { renderHook } from '@testing-library/react-hooks' @@ -10,19 +8,21 @@ import { useSafeBundleFlowContext } from 'modules/limitOrders/hooks/useSafeBundl import { safeBundleFlow } from 'modules/limitOrders/services/safeBundleFlow' import { tradeFlow } from 'modules/limitOrders/services/tradeFlow' import { TradeFlowContext } from 'modules/limitOrders/services/types' +import { useNavigateToOpenOrdersTable } from 'modules/ordersTable' import { useNeedsApproval } from 'common/hooks/useNeedsApproval' import { TradeAmounts } from 'common/types' import { WithModalProvider } from 'utils/withModalProvider' import { useHandleOrderPlacement } from './useHandleOrderPlacement' +import { useLimitOrdersRawState, useUpdateLimitOrdersRawState } from './useLimitOrdersRawState' import { TradeConfirmActions } from '../../trade' -import { limitOrdersRawStateAtom, updateLimitOrdersRawStateAtom } from '../state/limitOrdersRawStateAtom' import { defaultLimitOrdersSettings } from '../state/limitOrdersSettingsAtom' jest.mock('modules/limitOrders/services/tradeFlow') jest.mock('modules/limitOrders/services/safeBundleFlow') +jest.mock('modules/ordersTable') jest.mock('modules/limitOrders/hooks/useSafeBundleFlowContext') jest.mock('common/hooks/useNeedsApproval') @@ -46,6 +46,9 @@ jest.mock('common/hooks/useAnalyticsReporter') const mockTradeFlow = tradeFlow as jest.MockedFunction const mockSafeBundleFlow = safeBundleFlow as jest.MockedFunction +const mockUseNavigateToOpenOrdersTable = useNavigateToOpenOrdersTable as jest.MockedFunction< + typeof useNavigateToOpenOrdersTable +> const mockUseSafeBundleFlowContext = useSafeBundleFlowContext as jest.MockedFunction const mockUseNeedsApproval = useNeedsApproval as jest.MockedFunction @@ -85,13 +88,14 @@ describe('useHandleOrderPlacement', () => { mockUseSafeBundleFlowContext.mockImplementation(() => null) mockUseNeedsApproval.mockImplementation(() => false) mockIsBundlingSupported.mockImplementation(() => true) + mockUseNavigateToOpenOrdersTable.mockImplementation(() => () => {}) }) it('When a limit order placed, then the recipient value should be deleted', async () => { // Arrange renderHook( () => { - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() updateLimitOrdersState({ recipient }) }, @@ -99,7 +103,7 @@ describe('useHandleOrderPlacement', () => { ) // Assert - const { result: limitOrdersStateResultBefore } = renderHook(() => useAtomValue(limitOrdersRawStateAtom), { + const { result: limitOrdersStateResultBefore } = renderHook(() => useLimitOrdersRawState(), { wrapper: WithModalProvider, }) expect(limitOrdersStateResultBefore.current.recipient).toBe(recipient) @@ -112,7 +116,7 @@ describe('useHandleOrderPlacement', () => { await result.current() // Assert - const { result: limitOrdersStateResultAfter } = renderHook(() => useAtomValue(limitOrdersRawStateAtom), { + const { result: limitOrdersStateResultAfter } = renderHook(() => useLimitOrdersRawState(), { wrapper: WithModalProvider, }) expect(limitOrdersStateResultAfter.current.recipient).toBe(null) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts index 0bdb1b6b26..6ddff194b2 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useHandleOrderPlacement.ts @@ -1,18 +1,20 @@ -import { useAtom, useSetAtom } from 'jotai' +import { useAtom } from 'jotai' import { useCallback } from 'react' import { getAddress } from '@cowprotocol/common-utils' import { PriceImpact } from 'legacy/hooks/usePriceImpact' +import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' import { useSafeBundleFlowContext } from 'modules/limitOrders/hooks/useSafeBundleFlowContext' import { safeBundleFlow } from 'modules/limitOrders/services/safeBundleFlow' import { tradeFlow } from 'modules/limitOrders/services/tradeFlow' import { PriceImpactDeclineError, TradeFlowContext } from 'modules/limitOrders/services/types' -import { updateLimitOrdersRawStateAtom } from 'modules/limitOrders/state/limitOrdersRawStateAtom' import { LimitOrdersSettingsState } from 'modules/limitOrders/state/limitOrdersSettingsAtom' import { partiallyFillableOverrideAtom } from 'modules/limitOrders/state/partiallyFillableOverride' +import { useNavigateToOpenOrdersTable } from 'modules/ordersTable' import { TradeConfirmActions } from 'modules/trade/hooks/useTradeConfirmActions' +import { useHideAlternativeOrderModal } from 'modules/trade/state/alternativeOrder' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' import OperatorError from 'api/gnosisProtocol/errors/OperatorError' @@ -27,7 +29,9 @@ export function useHandleOrderPlacement( tradeConfirmActions: TradeConfirmActions ): () => Promise { const { confirmPriceImpactWithoutFee } = useConfirmPriceImpactWithoutFee() - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() + const hideAlternativeOrderModal = useHideAlternativeOrderModal() + const navigateToOpenOrdersTable = useNavigateToOpenOrdersTable() const [partiallyFillableOverride, setPartiallyFillableOverride] = useAtom(partiallyFillableOverrideAtom) // tx bundling stuff const safeBundleFlowContext = useSafeBundleFlowContext(tradeContext) @@ -97,6 +101,10 @@ export function useHandleOrderPlacement( updateLimitOrdersState({ recipient: null }) // Reset override after successful order placement setPartiallyFillableOverride(undefined) + // Reset alternative mode if any + hideAlternativeOrderModal() + // Navigate to open orders + navigateToOpenOrdersTable() }) .catch((error) => { if (error instanceof PriceImpactDeclineError) return @@ -107,7 +115,14 @@ export function useHandleOrderPlacement( tradeConfirmActions.onError(getSwapErrorMessage(error)) } }) - }, [tradeFn, tradeConfirmActions, updateLimitOrdersState, setPartiallyFillableOverride]) + }, [ + tradeFn, + tradeConfirmActions, + updateLimitOrdersState, + setPartiallyFillableOverride, + hideAlternativeOrderModal, + navigateToOpenOrdersTable, + ]) } function buildTradeAmounts(tradeContext: TradeFlowContext): TradeAmounts { diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useIsWidgetUnlocked.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useIsWidgetUnlocked.ts index 7eb1a918b3..740ee0b211 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useIsWidgetUnlocked.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useIsWidgetUnlocked.ts @@ -1,11 +1,9 @@ -import { useAtomValue } from 'jotai/index' - import { isInjectedWidget } from '@cowprotocol/common-utils' -import { limitOrdersRawStateAtom } from '../state/limitOrdersRawStateAtom' +import { useLimitOrdersRawState } from './useLimitOrdersRawState' export function useIsWidgetUnlocked(): boolean { - const rawState = useAtomValue(limitOrdersRawStateAtom) + const rawState = useLimitOrdersRawState() return rawState.isUnlocked || isInjectedWidget() } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useRateImpact.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useRateImpact.ts index e6af3c93a3..dfb3545dc9 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useRateImpact.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useRateImpact.ts @@ -3,6 +3,8 @@ import { useMemo } from 'react' import { limitRateAtom } from 'modules/limitOrders/state/limitRateAtom' +const FRACTION_DIGITS = 15 + export function useRateImpact(): number { const { activeRate, marketRate, isLoading, isLoadingMarketRate } = useAtomValue(limitRateAtom) @@ -12,7 +14,12 @@ export function useRateImpact(): number { if (noActiveRate || noExecutionRate || isLoading || isLoadingMarketRate) return 0 - const ratePercent = +activeRate.divide(marketRate).multiply(100).subtract(100).toFixed(1) + const ar = +activeRate.toFixed(FRACTION_DIGITS) + const mr = +marketRate.toFixed(FRACTION_DIGITS) + const ratio = ar / mr + const percent = ratio * 100 - 100 + + const ratePercent = +percent.toFixed(1) return !ratePercent || !Number.isFinite(ratePercent) || Number.isNaN(ratePercent) ? 0 : ratePercent }, [activeRate, marketRate, isLoading, isLoadingMarketRate]) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useSetupLimitOrderAmountsFromUrl.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useSetupLimitOrderAmountsFromUrl.ts index a94c20fb14..1ad1ab8874 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useSetupLimitOrderAmountsFromUrl.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useSetupLimitOrderAmountsFromUrl.ts @@ -1,17 +1,15 @@ -import { useSetAtom } from 'jotai' import { useCallback, useLayoutEffect, useMemo } from 'react' -import { tryParseCurrencyAmount } from '@cowprotocol/common-utils' -import { FractionUtils } from '@cowprotocol/common-utils' -import { getIntOrFloat } from '@cowprotocol/common-utils' +import { FractionUtils, getIntOrFloat, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { OrderKind } from '@cowprotocol/cow-sdk' import { Price } from '@uniswap/sdk-core' import { useLocation, useNavigate } from 'react-router-dom' import { Writeable } from 'types' -import { LimitOrdersRawState, updateLimitOrdersRawStateAtom } from 'modules/limitOrders' +import { LimitOrdersRawState } from 'modules/limitOrders' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' +import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' import { useUpdateActiveRate } from 'modules/limitOrders/hooks/useUpdateActiveRate' import { TRADE_URL_BUY_AMOUNT_KEY, TRADE_URL_SELL_AMOUNT_KEY } from 'modules/trade/const/tradeUrl' @@ -26,7 +24,7 @@ export function useSetupLimitOrderAmountsFromUrl() { const navigate = useNavigate() const { search, pathname } = useLocation() const params = useMemo(() => new URLSearchParams(search), [search]) - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() const updateRate = useUpdateActiveRate() const { inputCurrency, outputCurrency } = useLimitOrdersDerivedState() @@ -74,7 +72,7 @@ export function useSetupLimitOrderAmountsFromUrl() { if (sellCurrencyAmount && buyCurrencyAmount) { const activeRate = new Price({ baseAmount: sellCurrencyAmount, quoteAmount: buyCurrencyAmount }) - updateRate({ activeRate, isTypedValue: false, isRateFromUrl: true }) + updateRate({ activeRate, isTypedValue: false, isRateFromUrl: true, isAlternativeOrderRate: false }) } } // Trigger only when URL or assets are changed diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateActiveRate.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateActiveRate.ts index cd6f346889..99109c686b 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateActiveRate.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateActiveRate.ts @@ -3,12 +3,12 @@ import { useCallback } from 'react' import { isSellOrder } from '@cowprotocol/common-utils' -import { updateLimitOrdersRawStateAtom } from 'modules/limitOrders' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' +import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' import { useUpdateCurrencyAmount } from 'modules/limitOrders/hooks/useUpdateCurrencyAmount' import { limitRateAtom, LimitRateState, updateLimitRateAtom } from 'modules/limitOrders/state/limitRateAtom' -type RateUpdateParams = Pick +type RateUpdateParams = Pick export interface UpdateRateCallback { (update: RateUpdateParams): void @@ -17,15 +17,15 @@ export interface UpdateRateCallback { export function useUpdateActiveRate(): UpdateRateCallback { const { inputCurrencyAmount, outputCurrencyAmount, orderKind } = useLimitOrdersDerivedState() const rateState = useAtomValue(limitRateAtom) - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() const updateCurrencyAmount = useUpdateCurrencyAmount() const updateRateState = useSetAtom(updateLimitRateAtom) - const { isRateFromUrl: currentIsRateFromUrl } = rateState + const { isRateFromUrl: currentIsRateFromUrl, isAlternativeOrderRate: currentIsAlternativeOrderRate } = rateState return useCallback( (update: RateUpdateParams) => { - const { activeRate, isRateFromUrl } = update + const { activeRate, isRateFromUrl, isAlternativeOrderRate } = update updateRateState(update) @@ -33,7 +33,8 @@ export function useUpdateActiveRate(): UpdateRateCallback { if (activeRate) { // Don't update amounts when rate is set from URL. See useSetupLimitOrderAmountsFromUrl() - if (currentIsRateFromUrl || isRateFromUrl) { + // Don't update amounts when rate is set from AlternativeOrder. See AlternativeLimitOrderUpdater + if (currentIsRateFromUrl || isRateFromUrl || currentIsAlternativeOrderRate || isAlternativeOrderRate) { return } @@ -55,11 +56,12 @@ export function useUpdateActiveRate(): UpdateRateCallback { }, [ updateRateState, + orderKind, currentIsRateFromUrl, + currentIsAlternativeOrderRate, updateCurrencyAmount, inputCurrencyAmount, outputCurrencyAmount, - orderKind, updateLimitOrdersState, ] ) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateCurrencyAmount.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateCurrencyAmount.ts index fa9e1e849d..33743d16b5 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateCurrencyAmount.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useUpdateCurrencyAmount.ts @@ -1,4 +1,3 @@ -import { useSetAtom } from 'jotai' import { useCallback } from 'react' import { FractionUtils, isSellOrder } from '@cowprotocol/common-utils' @@ -10,7 +9,8 @@ import { Writeable } from 'types' import { Field } from 'legacy/state/types' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' -import { LimitOrdersRawState, updateLimitOrdersRawStateAtom } from 'modules/limitOrders/state/limitOrdersRawStateAtom' +import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' +import { LimitOrdersRawState } from 'modules/limitOrders/state/limitOrdersRawStateAtom' import { calculateAmountForRate } from 'utils/orderUtils/calculateAmountForRate' @@ -21,7 +21,7 @@ type CurrencyAmountProps = { } export function useUpdateCurrencyAmount() { - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() const { inputCurrency, outputCurrency } = useLimitOrdersDerivedState() return useCallback( @@ -37,10 +37,17 @@ export function useUpdateCurrencyAmount() { outputCurrency, }) + const inputCurrencyAmount = FractionUtils.serializeFractionToJSON( + field === Field.INPUT ? amount : calculatedAmount + ) + const outputCurrencyAmount = FractionUtils.serializeFractionToJSON( + field === Field.OUTPUT ? amount : calculatedAmount + ) + const update: Partial> = { orderKind, - inputCurrencyAmount: FractionUtils.serializeFractionToJSON(field === Field.INPUT ? amount : calculatedAmount), - outputCurrencyAmount: FractionUtils.serializeFractionToJSON(field === Field.OUTPUT ? amount : calculatedAmount), + ...(inputCurrencyAmount ? { inputCurrencyAmount } : undefined), + ...(outputCurrencyAmount ? { outputCurrencyAmount } : undefined), } updateLimitOrdersState(update) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/index.ts index 5702afa6f9..8ac72a89fa 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/index.ts @@ -1,12 +1,13 @@ -export * from './containers/LimitOrdersWidget' +export * from './const/trade' export * from './containers/ChartWidget' -export * from './updaters/QuoteObserverUpdater' -export * from './updaters/InitialPriceUpdater' +export * from './containers/LimitOrdersWidget' +export * from './state/limitOrdersRawStateAtom' +export * from './state/limitOrdersSettingsAtom' +export * from './updaters/AlternativeLimitOrderUpdater' export * from './updaters/ExecutionPriceUpdater' export * from './updaters/FillLimitOrdersDerivedStateUpdater' +export * from './updaters/InitialPriceUpdater' +export * from './updaters/QuoteObserverUpdater' export * from './updaters/SetupLimitOrderAmountsFromUrlUpdater' -export * from './state/limitOrdersRawStateAtom' -export * from './state/limitOrdersSettingsAtom' -export { useIsWidgetUnlocked } from './hooks/useIsWidgetUnlocked' -export * from './const/trade' export * from './updaters/TriggerAppziUpdater' +export { useIsWidgetUnlocked } from './hooks/useIsWidgetUnlocked' diff --git a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersRawStateAtom.ts b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersRawStateAtom.ts index c0d8afc079..a699182d38 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersRawStateAtom.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersRawStateAtom.ts @@ -1,9 +1,14 @@ import { atom } from 'jotai' import { atomWithStorage } from 'jotai/utils' +import { atomWithPartialUpdate } from '@cowprotocol/common-utils' import { getJotaiIsolatedStorage } from '@cowprotocol/core' import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' +import { + alternativeOrderAtomSetterFactory, + alternativeOrderReadWriteAtomFactory, +} from 'modules/trade/state/alternativeOrder' import { DEFAULT_TRADE_DERIVED_STATE, TradeDerivedState } from 'modules/trade/types/TradeDerivedState' import { ExtendedTradeRawState, getDefaultTradeRawState } from 'modules/trade/types/TradeRawState' @@ -15,31 +20,61 @@ export interface LimitOrdersRawState extends ExtendedTradeRawState { readonly isUnlocked: boolean } -export function getDefaultLimitOrdersState(chainId: SupportedChainId | null): LimitOrdersRawState { +export function getDefaultLimitOrdersState( + chainId: SupportedChainId | null, + isUnlocked: boolean = false +): LimitOrdersRawState { return { ...getDefaultTradeRawState(chainId), inputCurrencyAmount: null, outputCurrencyAmount: null, orderKind: OrderKind.SELL, - isUnlocked: false, + isUnlocked, } } -export const limitOrdersRawStateAtom = atomWithStorage( +// Regular form state + +const regularRawStateAtom = atomWithStorage( 'limit-orders-atom:v4', getDefaultLimitOrdersState(null), getJotaiIsolatedStorage() ) -export const updateLimitOrdersRawStateAtom = atom(null, (get, set, nextState: Partial) => { - set(limitOrdersRawStateAtom, () => { - const prevState = get(limitOrdersRawStateAtom) +const { updateAtom: regularUpdateRawStateAtom } = atomWithPartialUpdate(regularRawStateAtom) - return { ...prevState, ...nextState } - }) +const regularDerivedStateAtom = atom({ + ...DEFAULT_TRADE_DERIVED_STATE, + isUnlocked: true, }) -export const limitOrdersDerivedStateAtom = atom({ +// Alternative state for recreating/editing existing orders + +const alternativeRawStateAtom = atom(getDefaultLimitOrdersState(null, true)) + +const { updateAtom: alternativeUpdateRawStateAtom } = atomWithPartialUpdate(alternativeRawStateAtom) + +const alternativeDerivedStateAtom = atom({ ...DEFAULT_TRADE_DERIVED_STATE, isUnlocked: true, }) + +// Pick atom according to type of form displayed + +export const limitOrdersRawStateAtom = alternativeOrderReadWriteAtomFactory( + regularRawStateAtom, + alternativeRawStateAtom +) + +export const updateLimitOrdersRawStateAtom = atom( + null, + alternativeOrderAtomSetterFactory< + null, // pass null to indicate there is no getter + Partial + >(regularUpdateRawStateAtom, alternativeUpdateRawStateAtom) +) + +export const limitOrdersDerivedStateAtom = alternativeOrderReadWriteAtomFactory( + regularDerivedStateAtom, + alternativeDerivedStateAtom +) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts index 87ee68a147..b4df5c2ef4 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts @@ -1,4 +1,4 @@ -import { atom } from 'jotai' +import { atom, Getter, Setter } from 'jotai' import { atomWithStorage } from 'jotai/utils' import { getJotaiIsolatedStorage } from '@cowprotocol/core' @@ -7,6 +7,10 @@ import { Milliseconds, Timestamp } from 'types' import { defaultLimitOrderDeadline } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' import { partiallyFillableOverrideAtom } from 'modules/limitOrders/state/partiallyFillableOverride' +import { + alternativeOrderAtomSetterFactory, + alternativeOrderReadWriteAtomFactory, +} from 'modules/trade/state/alternativeOrder' export interface LimitOrdersSettingsState { readonly showRecipient: boolean @@ -22,21 +26,49 @@ export const defaultLimitOrdersSettings: LimitOrdersSettingsState = { customDeadlineTimestamp: null, } -export const limitOrdersSettingsAtom = atomWithStorage( +// regular +const regularLimitOrdersSettingsAtom = atomWithStorage( 'limit-orders-settings-atom:v2', defaultLimitOrdersSettings, getJotaiIsolatedStorage() ) +const regularUpdateLimitOrdersSettingsAtom = atom( + null, + partialFillsOverrideSetterFactory(regularLimitOrdersSettingsAtom) +) + +// alternative +const alternativeLimitOrdersSettingsAtom = atom(defaultLimitOrdersSettings) +const alternativeUpdateLimitOrdersSettingsAtom = atom( + null, + partialFillsOverrideSetterFactory(alternativeLimitOrdersSettingsAtom) +) + +// export +export const limitOrdersSettingsAtom = alternativeOrderReadWriteAtomFactory( + regularLimitOrdersSettingsAtom, + alternativeLimitOrdersSettingsAtom +) +export const updateLimitOrdersSettingsAtom = atom( + null, + alternativeOrderAtomSetterFactory(regularUpdateLimitOrdersSettingsAtom, alternativeUpdateLimitOrdersSettingsAtom) +) -export const updateLimitOrdersSettingsAtom = atom(null, (get, set, nextState: Partial) => { - set(limitOrdersSettingsAtom, () => { - const prevState = get(limitOrdersSettingsAtom) +// utils - if (nextState.partialFillsEnabled !== prevState.partialFillsEnabled) { - // Whenever `partialFillsEnabled` changes, reset `partiallyFillableOverrideAtom` - set(partiallyFillableOverrideAtom, undefined) - } +function partialFillsOverrideSetterFactory( + atomToUpdate: typeof regularLimitOrdersSettingsAtom | typeof alternativeLimitOrdersSettingsAtom +) { + return (get: Getter, set: Setter, nextState: Partial) => { + set(atomToUpdate, () => { + const prevState = get(atomToUpdate) - return { ...prevState, ...nextState } - }) -}) + if (nextState.partialFillsEnabled !== prevState.partialFillsEnabled) { + // Whenever `partialFillsEnabled` changes, reset `partiallyFillableOverrideAtom` + set(partiallyFillableOverrideAtom, undefined) + } + + return { ...prevState, ...nextState } + }) + } +} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/state/limitRateAtom.ts b/apps/cowswap-frontend/src/modules/limitOrders/state/limitRateAtom.ts index 66f79dd58c..066797fa59 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/state/limitRateAtom.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/state/limitRateAtom.ts @@ -15,6 +15,8 @@ export interface LimitRateState { // To avoid price overriding when it's already set from useSetupLimitOrderAmountsFromUrl() readonly isRateFromUrl: boolean readonly typedValue: string | null + // Respect alternative order initial rate + readonly isAlternativeOrderRate: boolean } export const initLimitRateState = () => ({ @@ -28,6 +30,7 @@ export const initLimitRateState = () => ({ isTypedValue: false, isRateFromUrl: false, typedValue: null, + isAlternativeOrderRate: false, }) export const { atom: limitRateAtom, updateAtom: updateLimitRateAtom } = atomWithPartialUpdate( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/state/partiallyFillableOverride.ts b/apps/cowswap-frontend/src/modules/limitOrders/state/partiallyFillableOverride.ts index 698f218949..afe7c3d64a 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/state/partiallyFillableOverride.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/state/partiallyFillableOverride.ts @@ -1,9 +1,17 @@ -import { atom, SetStateAction } from 'jotai' +import { atom } from 'jotai' + +import { alternativeOrderReadWriteAtomFactory } from 'modules/trade/state/alternativeOrder' export type PartiallyFillableOverrideType = boolean | undefined export type PartiallyFillableOverrideDispatcherType = [ PartiallyFillableOverrideType, - (update?: SetStateAction) => void + (update: PartiallyFillableOverrideType) => void ] -export const partiallyFillableOverrideAtom = atom(undefined) +const regularPartiallyFillableOverrideAtom = atom(undefined) +const alternativePartiallyFillableOverrideAtom = atom(undefined) + +export const partiallyFillableOverrideAtom = alternativeOrderReadWriteAtomFactory( + regularPartiallyFillableOverrideAtom, + alternativePartiallyFillableOverrideAtom +) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts new file mode 100644 index 0000000000..c03c7820cb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts @@ -0,0 +1,195 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useState } from 'react' + +import { usePrevious } from '@cowprotocol/common-hooks' +import { useENS } from '@cowprotocol/ens' +import { useWalletInfo } from '@cowprotocol/wallet' +import { Price } from '@uniswap/sdk-core' + +import { Order } from 'legacy/state/orders/actions' + +import { + limitOrdersDerivedStateAtom, + LimitOrdersSettingsState, + updateLimitOrdersSettingsAtom, +} from 'modules/limitOrders' +import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' +import { limitOrdersDeadlines } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' +import { partiallyFillableOverrideAtom } from 'modules/limitOrders/state/partiallyFillableOverride' +import { useAlternativeOrder, useHideAlternativeOrderModal } from 'modules/trade/state/alternativeOrder' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +import { DEFAULT_TRADE_DERIVED_STATE } from '../../trade' +import { useLimitOrdersDerivedState } from '../hooks/useLimitOrdersDerivedState' +import { useUpdateActiveRate } from '../hooks/useUpdateActiveRate' +import { updateLimitRateAtom } from '../state/limitRateAtom' + +export function AlternativeLimitOrderUpdater(): null { + // Update raw state and related settings once on load + useUpdateAlternativeRawState() + + // Set rate only once on load. If the user decides to change the values manually, the rate should then be allowed to be calculated + useSetAlternativeRate() + + // Hide modal if chainId or account changes + useResetAlternativeOnChainOrAccountChange() + + return null +} + +function useUpdateAlternativeRawState(): null { + const alternativeOrder = useAlternativeOrder() + const updateRawState = useUpdateLimitOrdersRawState() + const updatePartialFillOverride = useSetAtom(partiallyFillableOverrideAtom) + const updateSettingsState = useSetAtom(updateLimitOrdersSettingsAtom) + const updateDerivedState = useSetAtom(limitOrdersDerivedStateAtom) + + const { receiver, owner } = alternativeOrder || {} + // Use custom recipient address if set and != owner + const recipientAddress = receiver && receiver !== owner ? receiver : undefined + // Load used ens name, if any + const { name: recipient } = useENS(recipientAddress) + + useEffect(() => { + if (alternativeOrder) { + // Reset existing derived state to avoid stale info + updateDerivedState({ ...DEFAULT_TRADE_DERIVED_STATE, isUnlocked: true }) + + const { + inputToken, + outputToken, + sellAmount, + feeAmount, + buyAmount: outputCurrencyAmount, + kind: orderKind, + partiallyFillable, + } = alternativeOrder + + // To account for orders created before fee=0 went live + const inputCurrencyAmount = (BigInt(sellAmount) + BigInt(feeAmount)).toString() + + updateRawState({ + inputCurrencyId: inputToken.address, + outputCurrencyId: outputToken.address, + inputCurrencyAmount, + outputCurrencyAmount, + orderKind, + // Use loaded ens name, otherwise use address, if any of them exist + recipient: recipient || recipientAddress, + recipientAddress, + }) + // Sync partially fillable override based on the order flag + updatePartialFillOverride(partiallyFillable) + + // Sync settings (custom recipient and deadline values) + updateSettingsState(getSettingsState(alternativeOrder, !!recipientAddress)) + } + // Do it once on load + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [recipient, recipientAddress]) + + return null +} + +function useSetAlternativeRate(): null { + const updateRate = useUpdateActiveRate() + const updateLimitRateState = useSetAtom(updateLimitRateAtom) + const { inputCurrencyAmount, outputCurrencyAmount } = useLimitOrdersDerivedState() + + const [hasSetRate, setHasSetRate] = useState(false) + + useEffect(() => { + // Reset state when both currencies are null to prevent stale rates + if (!inputCurrencyAmount && !outputCurrencyAmount) { + setHasSetRate(false) + } + }, [inputCurrencyAmount, outputCurrencyAmount]) + + useEffect(() => { + // Update rate when rate has not been set yet + if (!hasSetRate && inputCurrencyAmount && outputCurrencyAmount) { + setHasSetRate(true) + + // Clear existing market rate + updateLimitRateState({ marketRate: null }) + + // Set new active rate + const activeRate = new Price({ baseAmount: inputCurrencyAmount, quoteAmount: outputCurrencyAmount }) + updateRate({ activeRate, isTypedValue: false, isRateFromUrl: false, isAlternativeOrderRate: true }) + } + }, [inputCurrencyAmount, hasSetRate, outputCurrencyAmount, updateRate, updateLimitRateState]) + + return null +} + +function useResetAlternativeOnChainOrAccountChange(): null { + const { chainId, account } = useWalletInfo() + const prevChainId = usePrevious(chainId) + const prevAccount = usePrevious(account) + const hideAlternativeOrderModal = useHideAlternativeOrderModal() + + useEffect(() => { + if ((prevChainId && chainId !== prevChainId) || (prevAccount && prevAccount !== account)) { + hideAlternativeOrderModal() + } + }, [chainId, account, prevChainId, prevAccount, hideAlternativeOrderModal]) + + return null +} + +/** + * Get order creation and expiration times, as Date objs + */ +function getOrderTimes(order: Order | ParsedOrder): [Date, Date] { + if ('validTo' in order) { + // Order instance, creationTime is a ISO string, validTo is a UNIX timestamp + return [new Date(order.creationTime), new Date(+order.validTo * 1000)] + } + // ParsedOrder instance, both are Date objects + return [order.creationTime, order.expirationTime] +} + +/** + * Get order duration in milliseconds + * @param order + */ +function getDuration(order: Order | ParsedOrder): number { + const [creationTime, expirationTime] = getOrderTimes(order) + const duration = expirationTime.getTime() - creationTime.getTime() + return Math.round(duration) +} + +/** + * Get pre-defined deadline matching given duration, if any + */ +function getMatchingDeadline(duration: number) { + // Match duration with approximate time + return limitOrdersDeadlines.find(({ value }) => Math.round(value / duration) === 1) +} + +/** + * Get setting state based on existing order + * + * Will set: + * - `showCustomRecipient` + * - `partialFillsEnabled` + * - either `deadlineMilliseconds` or `customDeadlineTimestamp` + */ +function getSettingsState(order: Order | ParsedOrder, hasCustomRecipient: boolean): Partial { + const state: Partial = { + showRecipient: hasCustomRecipient, + partialFillsEnabled: order.partiallyFillable, + } + + const duration = getDuration(order) + const deadline = getMatchingDeadline(duration) + + if (deadline) { + return { ...state, deadlineMilliseconds: deadline.value } + } + + const customDeadlineTimestamp = Math.round((Date.now() + duration) / 1000) + + return { ...state, customDeadlineTimestamp } +} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx index 4a7f8d6480..fd6ccb135d 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/InitialPriceUpdater/index.tsx @@ -40,7 +40,7 @@ export function InitialPriceUpdater() { if (!price || isInitialPriceSet || isLoading || prevPrice?.equalTo(price)) return setIsInitialPriceSet(true) - updateRate({ activeRate: price, isTypedValue: false, isRateFromUrl: false }) + updateRate({ activeRate: price, isTypedValue: false, isRateFromUrl: false, isAlternativeOrderRate: false }) updateLimitRateState({ isLoading }) }, [isInitialPriceSet, updateLimitRateState, updateRate, price, isLoading, prevPrice]) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts b/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts index 2d9d2fe776..72e324a5be 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts @@ -20,3 +20,6 @@ export const HISTORY_TAB: OrderTab = { export const ORDERS_TABLE_TABS: OrderTab[] = [OPEN_TAB, HISTORY_TAB] export const ORDERS_TABLE_PAGE_SIZE = 10 + +export const ORDERS_TABLE_TAB_KEY = 'tab' +export const ORDERS_TABLE_PAGE_KEY = 'page' diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/hooks.ts b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/hooks.ts index 1ff850372f..21d4faf170 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/hooks.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/hooks.ts @@ -1,10 +1,16 @@ -import { useSetAtom, useAtomValue } from 'jotai' -import { useCallback } from 'react' +import { useAtomValue, useSetAtom } from 'jotai' +import { useCallback, useMemo } from 'react' -import { updateReceiptAtom, receiptAtom } from 'modules/ordersTable/state/orderReceiptAtom' +import { Command, UiOrderType } from '@cowprotocol/types' +import { useSetAlternativeOrder } from 'modules/trade/state/alternativeOrder' + +import { isPending } from 'common/hooks/useCategorizeRecentActivity' +import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' import { ParsedOrder } from 'utils/orderUtils/parseOrder' +import { receiptAtom, updateReceiptAtom } from '../../state/orderReceiptAtom' + export function useCloseReceiptModal() { const updateReceiptState = useSetAtom(updateReceiptAtom) return useCallback(() => updateReceiptState({ order: null }), [updateReceiptState]) @@ -20,3 +26,15 @@ export function useSelectedOrder(): ParsedOrder | null { return order } + +export function useGetShowRecreateModal(order: ParsedOrder | null): Command | null { + const setOrderToRecreate = useSetAlternativeOrder() + + return useMemo( + () => + !order || isPending(order) || getUiOrderType(order) !== UiOrderType.LIMIT + ? null + : () => setOrderToRecreate(order), + [order, setOrderToRecreate] + ) +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/index.tsx index 4cc7b9f947..98a186ee9c 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersReceiptModal/index.tsx @@ -5,14 +5,15 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import JSBI from 'jsbi' import { PendingOrdersPrices } from 'modules/orders/state/pendingOrdersPricesAtom' -import { ReceiptModal } from 'modules/ordersTable/pure/ReceiptModal' -import { useTwapOrderById, useTwapOrderByChildId } from 'modules/twap' +import { useTwapOrderByChildId, useTwapOrderById } from 'modules/twap' import { calculatePrice } from 'utils/orderUtils/calculatePrice' -import { useCloseReceiptModal, useSelectedOrder } from './hooks' +import { useCloseReceiptModal, useGetShowRecreateModal, useSelectedOrder } from './hooks' -export type OrdersReceiptModalProps = { +import { ReceiptModal } from '../../pure/ReceiptModal' + +type OrdersReceiptModalProps = { pendingOrdersPrices: PendingOrdersPrices } @@ -28,6 +29,8 @@ export function OrdersReceiptModal(props: OrdersReceiptModalProps) { const twapOrder = twapOrderById || twapOrderByChildId const isTwapPartOrder = !!twapOrderByChildId + const showRecreateModal = useGetShowRecreateModal(order) + if (!chainId || !order) { return null } @@ -73,6 +76,7 @@ export function OrdersReceiptModal(props: OrdersReceiptModalProps) { isTwapPartOrder={isTwapPartOrder} isOpen={!!order} onDismiss={closeReceiptModal} + showRecreateModal={showRecreateModal} /> ) } diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useGetOrdersToCheckPendingPermit.ts b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useGetOrdersToCheckPendingPermit.ts index 444c72662b..f62a4120e4 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useGetOrdersToCheckPendingPermit.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useGetOrdersToCheckPendingPermit.ts @@ -8,7 +8,7 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder' import { OrdersTableList } from './useOrdersTableList' -import { getOrderParams } from '../../../pure/OrdersTableContainer/utils/getOrderParams' +import { getOrderParams } from '../../../utils/getOrderParams' import { isParsedOrder } from '../../../utils/orderTableGroupUtils' export function useGetOrdersToCheckPendingPermit( diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts index d99330eb71..6f9e37bee4 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts @@ -5,7 +5,7 @@ import { Order, PENDING_STATES } from 'legacy/state/orders/actions' import { getIsComposableCowOrder } from 'utils/orderUtils/getIsComposableCowOrder' import { getIsNotComposableCowOrder } from 'utils/orderUtils/getIsNotComposableCowOrder' -import { TabOrderTypes } from '../../../pure/OrdersTableContainer' +import { TabOrderTypes } from '../../../types' import { groupOrdersTable } from '../../../utils/groupOrdersTable' import { getParsedOrderFromTableItem, isParsedOrder, OrderTableItem } from '../../../utils/orderTableGroupUtils' diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useValidatePageUrlParams.ts b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useValidatePageUrlParams.ts index 66585c4b1a..2983f2e8d9 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useValidatePageUrlParams.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useValidatePageUrlParams.ts @@ -3,7 +3,8 @@ import { useLayoutEffect } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { ORDERS_TABLE_PAGE_SIZE } from '../../../const/tabs' -import { buildOrdersTableUrl, parseOrdersTableUrl } from '../../../utils/buildOrdersTableUrl' +import { buildOrdersTableUrl } from '../../../utils/buildOrdersTableUrl' +import { parseOrdersTableUrl } from '../../../utils/parseOrdersTableUrl' // Reset page params if they are invalid export function useValidatePageUrlParams(ordersLength: number, currentTabId: string, currentPageNumber: number) { diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx index e04a35a976..1827547bd8 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx @@ -2,6 +2,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback, useEffect, useMemo } from 'react' import { useTokensAllowances, useTokensBalances } from '@cowprotocol/balances-and-allowances' +import { UiOrderType } from '@cowprotocol/types' import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { useLocation, useNavigate } from 'react-router-dom' @@ -11,18 +12,14 @@ import { Order } from 'legacy/state/orders/actions' import { pendingOrdersPricesAtom } from 'modules/orders/state/pendingOrdersPricesAtom' import { useGetSpotPrice } from 'modules/orders/state/spotPricesAtom' -import { OPEN_TAB, ORDERS_TABLE_TABS } from 'modules/ordersTable/const/tabs' -import { MultipleCancellationMenu } from 'modules/ordersTable/containers/MultipleCancellationMenu' -import { OrdersReceiptModal } from 'modules/ordersTable/containers/OrdersReceiptModal' -import { useSelectReceiptOrder } from 'modules/ordersTable/containers/OrdersReceiptModal/hooks' -import { OrderActions } from 'modules/ordersTable/pure/OrdersTableContainer/types' -import { buildOrdersTableUrl, parseOrdersTableUrl } from 'modules/ordersTable/utils/buildOrdersTableUrl' import { PendingPermitUpdater, useGetOrdersPermitStatus } from 'modules/permit' +import { useSetAlternativeOrder } from 'modules/trade/state/alternativeOrder' import { useCancelOrder } from 'common/hooks/useCancelOrder' -import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' +import { isPending, useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' import { ordersToCancelAtom, updateOrdersToCancelAtom } from 'common/hooks/useMultipleOrdersCancellation/state' import { CancellableOrder } from 'common/utils/isOrderCancellable' +import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' import { ParsedOrder } from 'utils/orderUtils/parseOrder' import { useGetOrdersToCheckPendingPermit } from './hooks/useGetOrdersToCheckPendingPermit' @@ -31,8 +28,16 @@ import { useOrdersTableTokenApprove } from './hooks/useOrdersTableTokenApprove' import { useValidatePageUrlParams } from './hooks/useValidatePageUrlParams' import { BalancesAndAllowances } from '../../../tokens' -import { OrdersTableContainer, TabOrderTypes } from '../../pure/OrdersTableContainer' +import { OPEN_TAB, ORDERS_TABLE_TABS } from '../../const/tabs' +import { OrdersTableContainer } from '../../pure/OrdersTableContainer' +import { OrderActions } from '../../pure/OrdersTableContainer/types' +import { TabOrderTypes } from '../../types' +import { buildOrdersTableUrl } from '../../utils/buildOrdersTableUrl' import { OrderTableItem, tableItemsToOrders } from '../../utils/orderTableGroupUtils' +import { parseOrdersTableUrl } from '../../utils/parseOrdersTableUrl' +import { MultipleCancellationMenu } from '../MultipleCancellationMenu' +import { OrdersReceiptModal } from '../OrdersReceiptModal' +import { useSelectReceiptOrder } from '../OrdersReceiptModal/hooks' function getOrdersListByIndex(ordersList: OrdersTableList, id: string): OrderTableItem[] { return id === OPEN_TAB.id ? ordersList.pending : ordersList.history @@ -52,7 +57,7 @@ const ContentWrapper = styled.div` width: 100%; ` -export interface OrdersTableWidgetProps { +interface OrdersTableWidgetProps { displayOrdersOnlyForSafeApp: boolean orders: Order[] orderType: TabOrderTypes @@ -136,10 +141,22 @@ export function OrdersTableWidget({ [allOrders, cancelOrder] ) + const setOrderToRecreate = useSetAlternativeOrder() + const getShowRecreateModal = useCallback( + (order: ParsedOrder) => { + if (isPending(order) || getUiOrderType(order) !== UiOrderType.LIMIT) { + return null + } + return () => setOrderToRecreate(order) + }, + [setOrderToRecreate] + ) + const approveOrderToken = useOrdersTableTokenApprove() const orderActions: OrderActions = { getShowCancellationModal, + getShowRecreateModal, selectReceiptOrder, toggleOrderForCancellation, toggleOrdersForCancellation, diff --git a/apps/cowswap-frontend/src/modules/ordersTable/hooks/useGetBuildOrdersTableUrl.ts b/apps/cowswap-frontend/src/modules/ordersTable/hooks/useGetBuildOrdersTableUrl.ts new file mode 100644 index 0000000000..31c4d30e8d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/hooks/useGetBuildOrdersTableUrl.ts @@ -0,0 +1,14 @@ +import { useCallback } from 'react' + +import { useLocation } from 'react-router-dom' + +import { buildOrdersTableUrl } from '../utils/buildOrdersTableUrl' + +export function useGetBuildOrdersTableUrl() { + const location = useLocation() + + return useCallback( + (pageInfo: Parameters[1]) => buildOrdersTableUrl(location, pageInfo), + [location] + ) +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/hooks/useNavigateToOpenOrdersTable.ts b/apps/cowswap-frontend/src/modules/ordersTable/hooks/useNavigateToOpenOrdersTable.ts new file mode 100644 index 0000000000..e0e88fa958 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/hooks/useNavigateToOpenOrdersTable.ts @@ -0,0 +1,16 @@ +import { useCallback } from 'react' + +import { useNavigate } from 'react-router-dom' + +import { useGetBuildOrdersTableUrl } from './useGetBuildOrdersTableUrl' + +import { OPEN_TAB } from '../const/tabs' + +export function useNavigateToOpenOrdersTable() { + const navigate = useNavigate() + const buildOrdersTableUrl = useGetBuildOrdersTableUrl() + + return useCallback(() => { + navigate(buildOrdersTableUrl({ tabId: OPEN_TAB.id, pageNumber: 1 })) + }, [buildOrdersTableUrl, navigate]) +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/index.tsx index 603da87305..fdaa7edcd2 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/index.tsx @@ -1 +1,6 @@ +export * from './const/tabs' export * from './containers/OrdersTableWidget' +export * from './hooks/useGetBuildOrdersTableUrl' +export * from './hooks/useNavigateToOpenOrdersTable' +export * from './types' +export * from './utils/buildOrdersTableUrl' diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx index becc3c31d2..f066c74d6b 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx @@ -45,12 +45,13 @@ const Wrapper = styled.div<{ } ` -export type OrderStatusBoxProps = { +type OrderStatusBoxProps = { order: ParsedOrder widthAuto?: boolean withWarning?: boolean onClick?: Command } + export function OrderStatusBox({ order, widthAuto, withWarning, onClick }: OrderStatusBoxProps) { const { title, color, background } = getOrderStatusTitleAndColor(order) return ( diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderContextMenu.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderContextMenu.tsx index 073ec1068e..736922eed6 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderContextMenu.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderContextMenu.tsx @@ -3,7 +3,7 @@ import { UI } from '@cowprotocol/ui' import { Menu, MenuButton, MenuItem, MenuList } from '@reach/menu-button' import { transparentize } from 'color2k' -import { FileText, Link2, MoreVertical, Trash2 } from 'react-feather' +import { FileText, Link2, MoreVertical, Repeat, Trash2 } from 'react-feather' import styled from 'styled-components/macro' export const ContextMenuButton = styled(MenuButton)` @@ -39,7 +39,6 @@ export const ContextMenuList = styled(MenuList)` border-radius: 12px; overflow: hidden; position: relative; - z-index: 2; outline: none; min-width: 240px; margin: 10px 0; @@ -70,9 +69,15 @@ export interface OrderContextMenuProps { openReceipt: Command activityUrl: string | undefined showCancellationModal: Command | null + showRecreateModal: Command | null } -export function OrderContextMenu({ openReceipt, activityUrl, showCancellationModal }: OrderContextMenuProps) { +export function OrderContextMenu({ + openReceipt, + activityUrl, + showCancellationModal, + showRecreateModal, +}: OrderContextMenuProps) { return ( @@ -90,11 +95,17 @@ export function OrderContextMenu({ openReceipt, activityUrl, showCancellationMod )} {showCancellationModal && ( - showCancellationModal()}> + Cancel order )} + {showRecreateModal && ( + + + Recreate order + + )} ) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx index 27838520c3..67fca7dd7c 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import AlertTriangle from '@cowprotocol/assets/cow-swap/alert.svg' import { ZERO_FRACTION } from '@cowprotocol/common-const' @@ -7,7 +7,7 @@ import { getAddress, getEtherscanLink } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { TokenLogo } from '@cowprotocol/tokens' import { Command, UiOrderType } from '@cowprotocol/types' -import { Loader, TokenAmount, TokenSymbol, UI } from '@cowprotocol/ui' +import { Loader, TokenAmount, TokenSymbol, UI, ButtonSecondary } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' import SVG from 'react-inlinesvg' @@ -15,20 +15,9 @@ import SVG from 'react-inlinesvg' import { CREATING_STATES, OrderStatus } from 'legacy/state/orders/actions' import { PendingOrderPrices } from 'modules/orders/state/pendingOrdersPricesAtom' -import { EstimatedExecutionPrice } from 'modules/ordersTable/pure/OrdersTableContainer/OrderRow/EstimatedExecutionPrice' -import { OrderContextMenu } from 'modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderContextMenu' -import { - CheckboxCheckmark, - TableRow, - TableRowCheckbox, - TableRowCheckboxWrapper, -} from 'modules/ordersTable/pure/OrdersTableContainer/styled' -import { OrderActions } from 'modules/ordersTable/pure/OrdersTableContainer/types' -import { OrderStatusBox } from 'modules/ordersTable/pure/OrderStatusBox' import { getIsEthFlowOrder } from 'modules/swap/containers/EthFlowStepper' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { ButtonSecondary } from 'common/pure/ButtonSecondary' import { RateInfo } from 'common/pure/RateInfo' import { getQuoteCurrency } from 'common/services/getQuoteCurrency' import { isOrderCancellable } from 'common/utils/isOrderCancellable' @@ -39,9 +28,14 @@ import { getSellAmountWithFee } from 'utils/orderUtils/getSellAmountWithFee' import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' import { ParsedOrder } from 'utils/orderUtils/parseOrder' +import { EstimatedExecutionPrice } from './EstimatedExecutionPrice' +import { OrderContextMenu } from './OrderContextMenu' import * as styledEl from './styled' -import { OrderParams } from '../utils/getOrderParams' +import { OrderParams } from '../../../utils/getOrderParams' +import { OrderStatusBox } from '../../OrderStatusBox' +import { CheckboxCheckmark, TableRow, TableRowCheckbox, TableRowCheckboxWrapper } from '../styled' +import { OrderActions } from '../types' const TIME_AGO_UPDATE_INTERVAL = 3000 @@ -173,7 +167,10 @@ export function OrderRow({ const { inputCurrencyAmount, outputCurrencyAmount } = rateInfoParams const { estimatedExecutionPrice, feeAmount } = prices || {} - const showCancellationModal = orderActions.getShowCancellationModal(order) + // eslint-disable-next-line react-hooks/exhaustive-deps + const showCancellationModal = useMemo(() => orderActions.getShowCancellationModal(order), [order.id]) + // eslint-disable-next-line react-hooks/exhaustive-deps + const showRecreateModal = useMemo(() => orderActions.getShowRecreateModal(order), [order.id]) const withAllowanceWarning = hasEnoughAllowance === false && hasValidPendingPermit === false const withWarning = @@ -384,6 +381,7 @@ export function OrderRow({ activityUrl={activityUrl} openReceipt={onClick} showCancellationModal={showCancellationModal} + showRecreateModal={showRecreateModal} /> diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx index a71ebecb59..b776b78e68 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx @@ -8,21 +8,12 @@ import { Currency, Price } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' import { X } from 'react-feather' import SVG from 'react-inlinesvg' -import { useLocation } from 'react-router-dom' import styled from 'styled-components/macro' import QuestionHelper, { QuestionWrapper } from 'legacy/components/QuestionHelper' import { PendingOrdersPrices } from 'modules/orders/state/pendingOrdersPricesAtom' import { SpotPricesKeyParams } from 'modules/orders/state/spotPricesAtom' -import { ORDERS_TABLE_PAGE_SIZE } from 'modules/ordersTable/const/tabs' -import { - CheckboxCheckmark, - TableHeader, - TableRowCheckbox, - TableRowCheckboxWrapper, -} from 'modules/ordersTable/pure/OrdersTableContainer/styled' -import { OrderActions } from 'modules/ordersTable/pure/OrdersTableContainer/types' import { OrdersPermitStatus } from 'modules/permit' import { BalancesAndAllowances } from 'modules/tokens' @@ -33,10 +24,13 @@ import { CancellableOrder } from 'common/utils/isOrderCancellable' import { isOrderOffChainCancellable } from 'common/utils/isOrderOffChainCancellable' import { OrderRow } from './OrderRow' +import { CheckboxCheckmark, TableHeader, TableRowCheckbox, TableRowCheckboxWrapper } from './styled' import { TableGroup } from './TableGroup' -import { getOrderParams } from './utils/getOrderParams' +import { OrderActions } from './types' -import { buildOrdersTableUrl } from '../../utils/buildOrdersTableUrl' +import { ORDERS_TABLE_PAGE_SIZE } from '../../const/tabs' +import { useGetBuildOrdersTableUrl } from '../../hooks/useGetBuildOrdersTableUrl' +import { getOrderParams } from '../../utils/getOrderParams' import { getParsedOrderFromTableItem, isParsedOrder, @@ -224,7 +218,7 @@ export function OrdersTable({ currentPageNumber, ordersPermitStatus, }: OrdersTableProps) { - const location = useLocation() + const buildOrdersTableUrl = useGetBuildOrdersTableUrl() const [isRateInverted, setIsRateInverted] = useState(false) const checkboxRef = useRef(null) @@ -275,7 +269,7 @@ export function OrdersTable({ return cancellableOrders.every((item) => selectedOrdersMap[getParsedOrderFromTableItem(item).id]) }, [cancellableOrders, selectedOrdersMap]) - const getPageUrl = useCallback((index: number) => buildOrdersTableUrl(location, { pageNumber: index }), [location]) + const getPageUrl = useCallback((index: number) => buildOrdersTableUrl({ pageNumber: index }), [buildOrdersTableUrl]) // React doesn't support indeterminate attribute // Because of it, we have to use element reference diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx index 419f4cc6f2..bf2a86fe76 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx @@ -1,12 +1,11 @@ import { UI } from '@cowprotocol/ui' import { Trans } from '@lingui/macro' -import { Link, useLocation } from 'react-router-dom' +import { Link } from 'react-router-dom' import styled from 'styled-components/macro' -import { buildOrdersTableUrl } from 'modules/ordersTable/utils/buildOrdersTableUrl' - import { OrderTab } from '../../const/tabs' +import { useGetBuildOrdersTableUrl } from '../../hooks/useGetBuildOrdersTableUrl' const Tabs = styled.div` display: inline-block; @@ -52,7 +51,7 @@ export interface OrdersTabsProps { } export function OrdersTabs({ tabs }: OrdersTabsProps) { - const location = useLocation() + const buildOrdersTableUrl = useGetBuildOrdersTableUrl() const activeTabIndex = Math.max( tabs.findIndex((i) => i.isActive), 0 @@ -64,7 +63,7 @@ export function OrdersTabs({ tabs }: OrdersTabsProps) { {tab.title} ({tab.count}) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx index 5aaa3f5b23..8f582d5164 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx @@ -14,9 +14,9 @@ import { BalancesAndAllowances } from 'modules/tokens' import { OrderRow } from './OrderRow' import * as styledEl from './OrderRow/styled' import { OrderActions } from './types' -import { getOrderParams } from './utils/getOrderParams' import { ORDERS_TABLE_PAGE_SIZE } from '../../const/tabs' +import { getOrderParams } from '../../utils/getOrderParams' import { OrderTableGroup } from '../../utils/orderTableGroupUtils' import { OrdersTablePagination } from '../OrdersTablePagination' diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx index 0288e07a25..0850d19048 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.cosmos.tsx @@ -1,13 +1,16 @@ -import { OrderActions } from 'modules/ordersTable/pure/OrdersTableContainer/types' +import { Command } from '@cowprotocol/types' + import { BalancesAndAllowances } from 'modules/tokens' import { ParsedOrder } from 'utils/orderUtils/parseOrder' import { ordersMock } from './orders.mock' +import { OrderActions } from './types' import { OrderTab } from '../../const/tabs' +import { TabOrderTypes } from '../../types' -import { OrdersTableContainer, TabOrderTypes } from './index' +import { OrdersTableContainer } from './index' const tabs: OrderTab[] = [ { @@ -49,6 +52,10 @@ const orderActions: OrderActions = { approveOrderToken() { console.log('approveOrderToken ') }, + getShowRecreateModal: function (_: ParsedOrder): Command | null { + console.log(`getShowRecreateModal`) + return null + }, } export default ( diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx index 10c332437b..7031d6067a 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/index.tsx @@ -4,7 +4,7 @@ import cowMeditatingV2 from '@cowprotocol/assets/cow-swap/meditating-cow-v2.svg' import imageConnectWallet from '@cowprotocol/assets/cow-swap/wallet-plus.svg' import { isInjectedWidget } from '@cowprotocol/common-utils' import { ExternalLink } from '@cowprotocol/ui' -import { UI } from '@cowprotocol/ui' +import { UI, CowSwapSafeAppLink } from '@cowprotocol/ui' import { Trans } from '@lingui/macro' import SVG from 'react-inlinesvg' @@ -12,11 +12,11 @@ import styled from 'styled-components/macro' import { Web3Status } from 'modules/wallet/containers/Web3Status' -import { CowSwapSafeAppLink } from 'common/pure/CowSwapSafeAppLink' - import { OrdersTable, OrdersTableProps } from './OrdersTable' import { OrdersTabs, OrdersTabsProps } from './OrdersTabs' +import { TabOrderTypes } from '../../types' + const OrdersBox = styled.div` background: ${({ theme }) => (theme.isInjectedWidgetMode ? `var(${UI.COLOR_PAPER})` : 'transparent')}; color: inherit; @@ -162,7 +162,7 @@ const ExternalArrow = styled.span` font-size: 11px; } ` -export interface OrdersProps extends OrdersTabsProps, OrdersTableProps { +interface OrdersProps extends OrdersTabsProps, OrdersTableProps { isWalletConnected: boolean isOpenOrdersTab: boolean isSafeViaWc: boolean @@ -172,11 +172,6 @@ export interface OrdersProps extends OrdersTabsProps, OrdersTableProps { orderType: TabOrderTypes } -export enum TabOrderTypes { - LIMIT = 'limit', - ADVANCED = 'advanced', -} - export function OrdersTableContainer({ chainId, orders, diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/types.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/types.ts index 6353201fac..d11738effd 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/types.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/types.ts @@ -1,3 +1,4 @@ +import { Command } from '@cowprotocol/types' import { Token } from '@uniswap/sdk-core' import { UseCancelOrderReturn } from 'common/hooks/useCancelOrder' @@ -5,6 +6,7 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder' export interface OrderActions { getShowCancellationModal: (order: ParsedOrder) => UseCancelOrderReturn + getShowRecreateModal: (order: ParsedOrder) => Command | null selectReceiptOrder(order: ParsedOrder): void toggleOrderForCancellation(order: ParsedOrder): void toggleOrdersForCancellation(orders: ParsedOrder[]): void diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx index 866e6c7e93..733b9679bd 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx @@ -2,7 +2,8 @@ import { TokenAmount } from '@cowprotocol/ui' -import { ProgressBarWrapper, ProgressBar } from 'modules/ordersTable/pure/OrdersTableContainer/OrderRow/styled' +// TODO: bad import +import { ProgressBar, ProgressBarWrapper } from 'modules/ordersTable/pure/OrdersTableContainer/OrderRow/styled' import { getFilledAmounts } from 'utils/orderUtils/getFilledAmounts' import { ParsedOrder } from 'utils/orderUtils/parseOrder' diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/StatusField.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/StatusField.tsx index 000e1be7c1..3ebba7b736 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/StatusField.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/StatusField.tsx @@ -1,9 +1,9 @@ -import { OrderStatusBox } from 'modules/ordersTable/pure/OrderStatusBox' - import { ParsedOrder } from 'utils/orderUtils/parseOrder' import * as styledEl from './styled' +import { OrderStatusBox } from '../OrderStatusBox' + export type Props = { order: ParsedOrder } diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/index.tsx index 67f2ee62ef..55916b50ff 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/index.tsx @@ -1,7 +1,16 @@ import { ExplorerDataType, getExplorerLink, isSellOrder, shortenAddress } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' -import { ExternalLink, UI } from '@cowprotocol/ui' +import { + UI, + Icon, + IconType, + ExternalLink, + InlineBanner, + ButtonSecondary, + BannerOrientation, + CustomRecipientWarningBanner, +} from '@cowprotocol/ui' import { CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core' import { OrderStatus } from 'legacy/state/orders/actions' @@ -10,9 +19,6 @@ import { CloseIcon } from 'legacy/theme' import { TwapOrderItem } from 'modules/twap/types' import { isPending } from 'common/hooks/useCategorizeRecentActivity' -import { Icon, IconType } from 'common/pure/Icon' -import { InlineBanner } from 'common/pure/InlineBanner' -import { BannerOrientation, CustomRecipientWarningBanner } from 'common/pure/InlineBanner/banners' import { CowModal } from 'common/pure/Modal' import { useHideReceiverWalletBanner, @@ -33,6 +39,7 @@ import { PriceField } from './PriceField' import { StatusField } from './StatusField' import * as styledEl from './styled' import { SurplusField } from './SurplusField' + interface ReceiptProps { isOpen: boolean order: ParsedOrder @@ -46,6 +53,7 @@ interface ReceiptProps { limitPrice: Fraction | null executionPrice: Fraction | null estimatedExecutionPrice: Fraction | null + showRecreateModal: Command | null } const FILLED_COMMON_TOOLTIP = 'How much of the order has been filled.' @@ -97,6 +105,7 @@ export function ReceiptModal({ executionPrice, estimatedExecutionPrice, receiverEnsName, + showRecreateModal, }: ReceiptProps) { // Check if Custom Recipient Warning Banner should be visible const isCustomRecipientWarningBannerVisible = !useIsReceiverWalletBannerHidden(order.id) @@ -123,6 +132,11 @@ export function ReceiptModal({ Order Receipt + {showRecreateModal && ( + + Recreate this order + + )} onDismiss()} /> diff --git a/apps/cowswap-frontend/src/modules/ordersTable/types.ts b/apps/cowswap-frontend/src/modules/ordersTable/types.ts new file mode 100644 index 0000000000..230d73bb87 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/types.ts @@ -0,0 +1,9 @@ +export interface OrdersTablePageParams { + tabId: string + pageNumber: number +} + +export enum TabOrderTypes { + LIMIT = 'limit', + ADVANCED = 'advanced', +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/utils/buildOrdersTableUrl.ts b/apps/cowswap-frontend/src/modules/ordersTable/utils/buildOrdersTableUrl.ts index 67c3bc4a0b..de0140556d 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/utils/buildOrdersTableUrl.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/utils/buildOrdersTableUrl.ts @@ -1,16 +1,7 @@ import { Location } from 'history' -import { ORDERS_TABLE_TABS } from 'modules/ordersTable/const/tabs' - -const ORDERS_TABLE_TAB_KEY = 'tab' -const ORDERS_TABLE_PAGE_KEY = 'page' - -const ordersTableTabsIds = ORDERS_TABLE_TABS.map((item) => item.id) - -export interface OrdersTablePageParams { - tabId: string - pageNumber: number -} +import { ORDERS_TABLE_PAGE_KEY, ORDERS_TABLE_TAB_KEY } from '../const/tabs' +import { OrdersTablePageParams } from '../types' export function buildOrdersTableUrl( { pathname, search }: Pick, @@ -28,15 +19,3 @@ export function buildOrdersTableUrl( return { pathname, search: query.toString() } } - -export function parseOrdersTableUrl(search: string): Partial { - const params = new URLSearchParams(search) - - const tabIdRaw = params.get(ORDERS_TABLE_TAB_KEY) || '' - const tabId = ordersTableTabsIds.includes(tabIdRaw) ? tabIdRaw : undefined - - const pageNumberRaw = params.get(ORDERS_TABLE_PAGE_KEY) || undefined - const pageNumber = pageNumberRaw && /^\d+$/.test(pageNumberRaw) ? +pageNumberRaw : undefined - - return { tabId, pageNumber } -} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.test.ts b/apps/cowswap-frontend/src/modules/ordersTable/utils/getOrderParams.test.ts similarity index 98% rename from apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.test.ts rename to apps/cowswap-frontend/src/modules/ordersTable/utils/getOrderParams.test.ts index 55dc4c7b55..6fc63a2df1 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.test.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/utils/getOrderParams.test.ts @@ -4,7 +4,7 @@ import { BalancesAndAllowances } from 'modules/tokens' import { getOrderParams } from './getOrderParams' -import { ordersMock } from '../orders.mock' +import { ordersMock } from '../pure/OrdersTableContainer/orders.mock' describe('getOrderParams', () => { const BASE_ORDER = ordersMock[0] diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts b/apps/cowswap-frontend/src/modules/ordersTable/utils/getOrderParams.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts rename to apps/cowswap-frontend/src/modules/ordersTable/utils/getOrderParams.ts diff --git a/apps/cowswap-frontend/src/modules/ordersTable/utils/parseOrdersTableUrl.ts b/apps/cowswap-frontend/src/modules/ordersTable/utils/parseOrdersTableUrl.ts new file mode 100644 index 0000000000..109cdd5d89 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/utils/parseOrdersTableUrl.ts @@ -0,0 +1,16 @@ +import { ORDERS_TABLE_PAGE_KEY, ORDERS_TABLE_TAB_KEY, ORDERS_TABLE_TABS } from '../const/tabs' +import { OrdersTablePageParams } from '../types' + +const ordersTableTabsIds = ORDERS_TABLE_TABS.map((item) => item.id) + +export function parseOrdersTableUrl(search: string): Partial { + const params = new URLSearchParams(search) + + const tabIdRaw = params.get(ORDERS_TABLE_TAB_KEY) || '' + const tabId = ordersTableTabsIds.includes(tabIdRaw) ? tabIdRaw : undefined + + const pageNumberRaw = params.get(ORDERS_TABLE_PAGE_KEY) || undefined + const pageNumber = pageNumberRaw && /^\d+$/.test(pageNumberRaw) ? +pageNumberRaw : undefined + + return { tabId, pageNumber } +} diff --git a/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts b/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts index d1f818c03b..8d25351217 100644 --- a/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts +++ b/apps/cowswap-frontend/src/modules/swap/helpers/getSwapButtonState.ts @@ -1,9 +1,11 @@ import { Token, TradeType } from '@uniswap/sdk-core' import { QuoteError } from 'legacy/state/price/actions' +import { QuoteInformationObject } from 'legacy/state/price/reducer' import TradeGp from 'legacy/state/swap/TradeGp' import { getEthFlowEnabled } from 'modules/swap/helpers/getEthFlowEnabled' +import { isQuoteExpired } from 'modules/tradeQuote/utils/isQuoteExpired' import { ApprovalState } from 'common/hooks/useApproveState' @@ -16,6 +18,7 @@ export enum SwapButtonState { TransferToSmartContract = 'TransferToSmartContract', UnsupportedToken = 'UnsupportedToken', FetchQuoteError = 'FetchQuoteError', + QuoteExpired = 'QuoteExpired', OfflineBrowser = 'OfflineBrowser', Loading = 'Loading', WalletIsNotConnected = 'WalletIsNotConnected', @@ -39,6 +42,7 @@ export interface SwapButtonStateParams { isReadonlyGnosisSafeUser: boolean isSwapUnsupported: boolean isBundlingSupported: boolean + quote: QuoteInformationObject | undefined | null inputError?: string approvalState: ApprovalState feeWarningAccepted: boolean @@ -63,7 +67,8 @@ const quoteErrorToSwapButtonState: { [key in QuoteError]: SwapButtonState | null } export function getSwapButtonState(input: SwapButtonStateParams): SwapButtonState { - const { trade, quoteError, approvalState, isPermitSupported } = input + const { trade, quote, approvalState, isPermitSupported, amountsForSignature } = input + const quoteError = quote?.error // show approve flow when: no error on inputs, not approved or pending, or approved in current session // never show if price impact is above threshold @@ -104,6 +109,10 @@ export function getSwapButtonState(input: SwapButtonStateParams): SwapButtonStat return SwapButtonState.ReadonlyGnosisSafeUser } + if (isQuoteExpired(quote?.fee?.expirationDate) === true) { + return SwapButtonState.QuoteExpired + } + if (!input.isNativeIn && showApproveFlow) { if (input.isBundlingSupported) { return SwapButtonState.ApproveAndSwap diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts index b6f43b2d65..2f57651d86 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts @@ -85,7 +85,7 @@ export function useBaseFlowContextSetup(): BaseFlowContextSetup { const { allowsOffchainSigning } = useWalletDetails() const gnosisSafeInfo = useGnosisSafeInfo() const { recipient } = useSwapState() - const { trade } = useDerivedSwapInfo() + const { trade, allowedSlippage } = useDerivedSwapInfo() const appData = useAppData() const closeModals = useCloseModals() @@ -102,7 +102,6 @@ export function useBaseFlowContextSetup(): BaseFlowContextSetup { const isSafeEthFlow = useIsSafeEthFlow() const getCachedPermit = useGetCachedPermit() - const { allowedSlippage } = useDerivedSwapInfo() const [inputAmountWithSlippage, outputAmountWithSlippage] = useSwapAmountsWithSlippage() const sellTokenContract = useTokenContract(getAddress(inputAmountWithSlippage?.currency) || undefined, true) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index a8a1171006..93c37b28d2 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -103,7 +103,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext isSwapUnsupported, isNativeIn: isNativeInSwap, wrappedToken, - quoteError: quote?.error, + quote, inputError: swapInputError, approvalState, feeWarningAccepted, @@ -136,6 +136,6 @@ function useHasEnoughWrappedBalanceForSwap(inputAmount?: CurrencyAmountError loading price. Try again later. ), + [SwapButtonState.QuoteExpired]: () => ( + + Quote expired. Refreshing... + + ), [SwapButtonState.UnsupportedToken]: () => ( Unsupported token diff --git a/apps/cowswap-frontend/src/modules/swap/pure/TradeRates/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/swap/pure/TradeRates/index.cosmos.tsx index ec8d4439dd..ab79eacb11 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/TradeRates/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/TradeRates/index.cosmos.tsx @@ -22,8 +22,11 @@ const trade = new TradeGp({ outputAmount: CurrencyAmount.fromRawAmount(currency, output * 10 ** 18), outputAmountWithoutFee: CurrencyAmount.fromRawAmount(currency, (output - 3) * 10 ** 18), outputAmountAfterFees: CurrencyAmount.fromRawAmount(currency, (output - 3) * 10 ** 18), - fee: { feeAsCurrency: CurrencyAmount.fromRawAmount(currency, 3 * 10 ** 18), amount: '50' }, - executionPrice: new Price(currency, currencyOut, 1, 4), + fee: { + feeAsCurrency: CurrencyAmount.fromRawAmount(currency, 3 * 10 ** 18), + amount: '50', + expirationDate: new Date().toISOString(), + },executionPrice: new Price(currency, currencyOut, 1, 4), tradeType: TradeType.EXACT_INPUT, quoteId: 10000, partnerFee: { bps: 35, recipient: '0x1234567890123456789012345678901234567890' }, diff --git a/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx b/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx index c5a3b550fa..e37bf9c095 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx @@ -1,4 +1,5 @@ import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' +import { InlineBanner } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { NavLink } from 'react-router-dom' @@ -9,7 +10,6 @@ import { parameterizeTradeRoute } from 'modules/trade/utils/parameterizeTradeRou import { parameterizeTradeSearch } from 'modules/trade/utils/parameterizeTradeSearch' import { Routes } from 'common/constants/routes' -import { InlineBanner } from 'common/pure/InlineBanner' const StyledNavLink = styled(NavLink)` color: inherit; diff --git a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx index 1b59bd5436..354e617927 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx @@ -2,6 +2,7 @@ import React from 'react' import { genericPropsChecker } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { BundleTxApprovalBanner, BundleTxSafeWcBanner, BundleTxWrapBanner } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import styled from 'styled-components/macro' @@ -13,7 +14,6 @@ import { CompatibilityIssuesWarning } from 'modules/trade/pure/CompatibilityIssu import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { TradeUrlParams } from 'modules/trade/types/TradeRawState' -import { BundleTxApprovalBanner, BundleTxSafeWcBanner, BundleTxWrapBanner } from 'common/pure/InlineBanner/banners' import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' import { TwapSuggestionBanner } from './banners/TwapSuggestionBanner' diff --git a/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts b/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts index 9df2d06cb3..7d67dc4ae9 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/ethFlow/index.ts @@ -1,4 +1,4 @@ -import { reportAppDataWithHooks } from '@cowprotocol/common-utils' +import { reportAppDataWithHooks, reportPlaceOrderWithExpiredQuote } from '@cowprotocol/common-utils' import { CowEvents } from '@cowprotocol/events' import { Percent } from '@uniswap/sdk-core' @@ -14,6 +14,7 @@ import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep' import { tradeFlowAnalytics } from 'modules/trade/utils/analytics' import { logTradeFlow } from 'modules/trade/utils/logger' import { getSwapErrorMessage } from 'modules/trade/utils/swapErrorHelper' +import { isQuoteExpired } from 'modules/tradeQuote/utils/isQuoteExpired' import { calculateUniqueOrderId } from './steps/calculateUniqueOrderId' @@ -35,7 +36,7 @@ export async function ethFlow( addInFlightOrderId, } = ethFlowContext const { - trade: { inputAmount, outputAmount }, + trade: { inputAmount, outputAmount, fee }, } = context const tradeAmounts = { inputAmount, outputAmount } @@ -60,6 +61,12 @@ export async function ethFlow( const { orderId, orderParams } = await calculateUniqueOrderId(orderParamsOriginal, contract, checkEthFlowOrderExists) try { + // Do not proceed if fee is expired + if (isQuoteExpired(fee.expirationDate)) { + reportPlaceOrderWithExpiredQuote({ ...orderParamsOriginal, fee }) + throw new Error('Quote expired. Please refresh.') + } + logTradeFlow('ETH FLOW', 'STEP 4: sign order') const { order, txReceipt } = await signEthFlowOrderStep(orderId, orderParams, contract, addInFlightOrderId).finally( () => { diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 59e409900b..f730facf1c 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo, useRef } from 'react' import { TokenWithLogo } from '@cowprotocol/common-const' +import { BannerOrientation, ExternalLink, InlineBanner, LINK_GUIDE_ADD_CUSTOM_TOKEN } from '@cowprotocol/ui' import { useVirtualizer } from '@tanstack/react-virtual' import ms from 'ms.macro' @@ -63,6 +64,19 @@ export function TokensVirtualList(props: TokensVirtualListProps) { + +

+ Can't find your token on the list?{' '} + Read our guide on how to add custom + tokens. +

+
+ {items.map((virtualRow) => { const token = sortedTokens[virtualRow.index] const addressLowerCase = token.address.toLowerCase() diff --git a/apps/cowswap-frontend/src/modules/trade/containers/SellNativeWarningBanner/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/SellNativeWarningBanner/index.tsx index b8794a2dd9..fb562c967a 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/SellNativeWarningBanner/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/SellNativeWarningBanner/index.tsx @@ -1,8 +1,8 @@ import { OrderKind } from '@cowprotocol/cow-sdk' +import { SellNativeWarningBanner as Pure } from '@cowprotocol/ui' import { Field } from 'legacy/state/types' -import { SellNativeWarningBanner as Pure } from 'common/pure/InlineBanner/banners' import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { useDerivedTradeState } from '../../hooks/useDerivedTradeState' diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx index 4d070cb32d..a8ecc79e41 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeConfirmModal/index.tsx @@ -23,6 +23,10 @@ const Container = styled.div` background: var(${UI.COLOR_PAPER}); border-radius: var(${UI.BORDER_RADIUS_NORMAL}); box-shadow: ${({ theme }) => theme.boxShadow1}; + + .modalMode & { + box-shadow: none; + } ` type CustomSubmittedContent = (order: Order | undefined, onDismiss: Command) => JSX.Element diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index d47299c5b2..8d5cb656aa 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -9,6 +9,7 @@ import { AccountElement } from 'legacy/components/Header/AccountElement' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useOpenTokenSelectWidget } from 'modules/tokensList' +import { useIsAlternativeOrderModalVisible } from 'modules/trade/state/alternativeOrder' import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' @@ -29,6 +30,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const isInjectedWidgetMode = isInjectedWidget() const injectedWidgetParams = useInjectedWidgetParams() + const isAlternativeOrderModalVisible = useIsAlternativeOrderModalVisible() const { pendingActivity } = useCategorizeRecentActivity() const { slots, inputCurrencyInfo, outputCurrencyInfo, actions, params, disableOutput } = props @@ -61,6 +63,8 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const maxBalance = maxAmountSpend(inputCurrencyInfo.balance || undefined, isSafeWallet) const showSetMax = maxBalance?.greaterThan(0) && !inputCurrencyInfo.amount?.equalTo(maxBalance) + const alternativeOrderModalVisible = useIsAlternativeOrderModalVisible() + // Disable too frequent tokens switching const throttledOnSwitchTokens = useThrottleFn(onSwitchTokens, 500) @@ -73,13 +77,14 @@ export function TradeWidgetForm(props: TradeWidgetProps) { onUserInput, allowsOffchainSigning, openTokenSelectWidget, + tokenSelectorDisabled: alternativeOrderModalVisible, } /** * Reset recipient value only once at App start if it's not set in URL */ useEffect(() => { - if (!hasRecipientInUrl) { + if (!hasRecipientInUrl && !isAlternativeOrderModalVisible) { onChangeRecipient(null) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -89,7 +94,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { <> - + {isAlternativeOrderModalVisible ?
: } {isInjectedWidgetMode && !injectedWidgetParams.hideConnectButton && ( )} @@ -115,10 +120,10 @@ export function TradeWidgetForm(props: TradeWidgetProps) { void 0 : throttledOnSwitchTokens} withRecipient={withRecipient} isLoading={isTradePriceUpdating} + disabled={isAlternativeOrderModalVisible} />
diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx index 89b9248c73..7f48ff25eb 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx @@ -22,6 +22,10 @@ export const ContainerBox = styled.div` box-shadow: ${({ theme }) => theme.boxShadow1}; padding: 10px; position: relative; + + .modalMode & { + box-shadow: none; + } ` export const Header = styled.div` diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts index a3d7404a81..7e899e43f6 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts @@ -3,10 +3,11 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { usePrevious } from '@cowprotocol/common-hooks' import { getRawCurrentChainIdFromUrl } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { useWalletInfo, switchChain } from '@cowprotocol/wallet' +import { switchChain, useWalletInfo } from '@cowprotocol/wallet' import { useWeb3React } from '@web3-react/core' import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' +import { useIsAlternativeOrderModalVisible } from 'modules/trade/state/alternativeOrder' import { getDefaultTradeRawState, TradeRawState } from 'modules/trade/types/TradeRawState' import { useResetStateWithSymbolDuplication } from './useResetStateWithSymbolDuplication' @@ -37,6 +38,8 @@ export function useSetupTradeState(): void { const currentChainId = !urlChainId ? prevProviderChainId || SupportedChainId.MAINNET : urlChainId + const isAlternativeModalVisible = useIsAlternativeOrderModalVisible() + const switchNetworkInWallet = useCallback( (targetChainId: SupportedChainId) => { switchChain(connector, targetChainId).catch((error: Error) => { @@ -99,6 +102,12 @@ export function useSetupTradeState(): void { * - apply the URL changes only if user accepted network changes in the wallet */ useEffect(() => { + // Do nothing when in alternative modal + // App should already be loaded by then + if (isAlternativeModalVisible) { + return + } + const { inputCurrencyId, outputCurrencyId } = tradeStateFromUrl const providerAndUrlChainIdMismatch = currentChainId !== prevProviderChainId diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts index 3c2db8980a..e3f634dcdb 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts @@ -3,6 +3,9 @@ import { Atom, useAtomValue } from 'jotai' import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { tryParseFractionalAmount } from '@cowprotocol/common-utils' import { useTokenBySymbolOrAddress } from '@cowprotocol/tokens' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import { Nullish } from 'types' import { ExtendedTradeRawState } from 'modules/trade/types/TradeRawState' import { useTradeUsdAmounts } from 'modules/usdAmount' @@ -18,8 +21,8 @@ export function useBuildTradeDerivedState(stateAtom: Atom const inputCurrency = useTokenBySymbolOrAddress(rawState.inputCurrencyId) const outputCurrency = useTokenBySymbolOrAddress(rawState.outputCurrencyId) - const inputCurrencyAmount = tryParseFractionalAmount(inputCurrency, rawState.inputCurrencyAmount) - const outputCurrencyAmount = tryParseFractionalAmount(outputCurrency, rawState.outputCurrencyAmount) + const inputCurrencyAmount = getCurrencyAmount(inputCurrency, rawState.inputCurrencyAmount) + const outputCurrencyAmount = getCurrencyAmount(outputCurrency, rawState.outputCurrencyAmount) const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) || null const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) || null @@ -47,3 +50,15 @@ export function useBuildTradeDerivedState(stateAtom: Atom outputCurrencyFiatAmount, }) } + +function getCurrencyAmount( + currency: Nullish | null, + currencyAmount: Nullish +): CurrencyAmount | null { + if (!currency || !currencyAmount) { + return null + } + // State can be stored as a full string in atoms rather than a json with numerator/denominator + // Thus we try just that in case the first option fails + return tryParseFractionalAmount(currency, currencyAmount) || CurrencyAmount.fromRawAmount(currency, currencyAmount) +} diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useOnCurrencySelection.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useOnCurrencySelection.ts index 872793f7a4..c79764628b 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useOnCurrencySelection.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useOnCurrencySelection.ts @@ -1,4 +1,3 @@ -import { useSetAtom } from 'jotai' import { useCallback } from 'react' import { FractionUtils } from '@cowprotocol/common-utils' @@ -6,8 +5,8 @@ import { Currency } from '@uniswap/sdk-core' import { Field } from 'legacy/state/types' -import { updateLimitOrdersRawStateAtom } from 'modules/limitOrders' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' +import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection' import { convertAmountToCurrency } from 'utils/orderUtils/calculateExecutionPrice' @@ -15,7 +14,7 @@ import { convertAmountToCurrency } from 'utils/orderUtils/calculateExecutionPric export function useOnCurrencySelection(): (field: Field, currency: Currency | null) => void { const { inputCurrencyAmount, outputCurrencyAmount } = useLimitOrdersDerivedState() const navigateOnCurrencySelection = useNavigateOnCurrencySelection() - const updateLimitOrdersState = useSetAtom(updateLimitOrdersRawStateAtom) + const updateLimitOrdersState = useUpdateLimitOrdersRawState() return useCallback( (field: Field, currency: Currency | null) => { diff --git a/apps/cowswap-frontend/src/modules/trade/pure/PriceUpdatedBanner/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/PriceUpdatedBanner/index.tsx index 65fe8d9243..81448f4f5f 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/PriceUpdatedBanner/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/PriceUpdatedBanner/index.tsx @@ -1,9 +1,9 @@ import { UI } from '@cowprotocol/ui' +import { InlineBanner } from '@cowprotocol/ui' import { Trans } from '@lingui/macro' import styled from 'styled-components/macro' -import { InlineBanner } from 'common/pure/InlineBanner' const Wrapper = styled.div` display: flex; diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx index 94afa698dc..f41d9f18ca 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useRef, useState } from 'react' -import { ButtonSize, ButtonPrimary, BackButton, CenteredDots, LongLoadText } from '@cowprotocol/ui' +import { + ButtonSize, + ButtonPrimary, + BackButton, + CenteredDots, + LongLoadText, + BannerOrientation, + CustomRecipientWarningBanner, +} from '@cowprotocol/ui' import { Trans } from '@lingui/macro' import ms from 'ms.macro' @@ -9,7 +17,6 @@ import { useMediaQuery, upToMedium } from 'legacy/hooks/useMediaQuery' import { PriceImpact } from 'legacy/hooks/usePriceImpact' import { CurrencyAmountPreview, CurrencyPreviewInfo } from 'common/pure/CurrencyInputPanel' -import { BannerOrientation, CustomRecipientWarningBanner } from 'common/pure/InlineBanner/banners' import { QuoteCountdown } from './CountDown' import { useIsPriceChanged } from './hooks/useIsPriceChanged' diff --git a/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/atomFactories.ts b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/atomFactories.ts new file mode 100644 index 0000000000..d71070c4d3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/atomFactories.ts @@ -0,0 +1,35 @@ +import { atom, Getter, PrimitiveAtom, SetStateAction, Setter, WritableAtom } from 'jotai' + +import { isAlternativeOrderModalVisibleAtom } from './atoms' + +function alternativeOrderAtomGetterFactory( + regular: PrimitiveAtom, + alternative: PrimitiveAtom +) { + return (get: Getter) => get(get(isAlternativeOrderModalVisibleAtom) ? alternative : regular) +} + +type WritableWithOptionalSetterValue = WritableAtom + +export function alternativeOrderAtomSetterFactory( + regular: WritableWithOptionalSetterValue, + alternative: WritableWithOptionalSetterValue +) { + return (get: Getter, set: Setter, value: AtomWriterParamValue) => { + if (get(isAlternativeOrderModalVisibleAtom)) { + set(alternative, value) + } else { + set(regular, value) + } + } +} + +export function alternativeOrderReadWriteAtomFactory( + regular: WritableWithOptionalSetterValue>, + alternative: WritableWithOptionalSetterValue> +) { + return atom( + alternativeOrderAtomGetterFactory(regular, alternative), + alternativeOrderAtomSetterFactory(regular, alternative) + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/atoms.ts b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/atoms.ts new file mode 100644 index 0000000000..da9981a2b8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/atoms.ts @@ -0,0 +1,16 @@ +import { atom } from 'jotai' + +import { Order } from 'legacy/state/orders/actions' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +/** + * Main atom to control the alternative order modal/form + * When it's set, the alternative flow should be displayed + */ +export const alternativeOrderAtom = atom(null) + +/** + * Derived atom that controls the alternative modal visibility + */ +export const isAlternativeOrderModalVisibleAtom = atom((get) => !!get(alternativeOrderAtom)) diff --git a/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/hooks.ts b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/hooks.ts new file mode 100644 index 0000000000..2788bbf359 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/hooks.ts @@ -0,0 +1,28 @@ +import { useAtomValue, useSetAtom } from 'jotai' +import { useCallback } from 'react' + +import { Order } from 'legacy/state/orders/actions' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +import { alternativeOrderAtom, isAlternativeOrderModalVisibleAtom } from './atoms' + +export function useIsAlternativeOrderModalVisible() { + return useAtomValue(isAlternativeOrderModalVisibleAtom) +} + +export function useHideAlternativeOrderModal() { + const setAlternativeOrderAtom = useSetAtom(alternativeOrderAtom) + + return useCallback(() => setAlternativeOrderAtom(null), [setAlternativeOrderAtom]) +} + +export function useAlternativeOrder() { + return useAtomValue(alternativeOrderAtom) +} + +export function useSetAlternativeOrder() { + const setAlternativeOrder = useSetAtom(alternativeOrderAtom) + + return useCallback((order: Order | ParsedOrder) => setAlternativeOrder(order), [setAlternativeOrder]) +} diff --git a/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/index.ts b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/index.ts new file mode 100644 index 0000000000..96df2d7471 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/state/alternativeOrder/index.ts @@ -0,0 +1,2 @@ +export * from './atomFactories' +export * from './hooks' diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx index 19dbfa1523..84f624b44e 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx @@ -96,6 +96,7 @@ export const tradeButtonsMap: Record ) }, + [TradeFormValidation.QuoteExpired]: { text: 'Quote expired. Refreshing...' }, [TradeFormValidation.WalletNotConnected]: (context) => { return ( diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts index 9fe854ae6f..2767e3e935 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts @@ -1,5 +1,8 @@ import { getIsNativeToken, isAddress, isFractionFalsy } from '@cowprotocol/common-utils' +import { TradeType } from 'modules/trade' +import { isQuoteExpired } from 'modules/tradeQuote/utils/isQuoteExpired' + import { ApprovalState } from 'common/hooks/useApproveState' import { TradeFormValidation, TradeFormValidationContext } from '../types' @@ -88,5 +91,14 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor return TradeFormValidation.ApproveRequired } + if ( + !isWrapUnwrap && + derivedTradeState.tradeType !== TradeType.LIMIT_ORDER && + !tradeQuote.isLoading && + isQuoteExpired(tradeQuote.response?.expiration) === true + ) { + return TradeFormValidation.QuoteExpired + } + return null } diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts index d2cbfad2b9..ea3cc9364c 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts @@ -25,6 +25,7 @@ export enum TradeFormValidation { // Quote loading indicator QuoteLoading, + QuoteExpired, // Balances BalancesNotLoaded, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/utils/isQuoteExpired.ts b/apps/cowswap-frontend/src/modules/tradeQuote/utils/isQuoteExpired.ts new file mode 100644 index 0000000000..8b7c8bc4ff --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeQuote/utils/isQuoteExpired.ts @@ -0,0 +1,14 @@ +import ms from 'ms.macro' +import { Nullish } from 'types' + +const EXPIRATION_TIME_DELTA = ms`5s` + +export function isQuoteExpired(expirationDate: Nullish): boolean | undefined { + if (!expirationDate) { + return undefined + } + + const expirationTime = new Date(expirationDate).getTime() + + return Date.now() > expirationTime - EXPIRATION_TIME_DELTA +} diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx index 1e226b389b..fb8da5f031 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx @@ -2,6 +2,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' import { modifySafeHandlerAnalytics } from '@cowprotocol/analytics' +import { BundleTxApprovalBanner } from '@cowprotocol/ui' import { useIsSafeViaWc, useWalletInfo } from '@cowprotocol/wallet' import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' @@ -12,7 +13,6 @@ import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFor import { useTradeQuoteFeeFiatAmount } from 'modules/tradeQuote' import { useShouldZeroApprove } from 'modules/zeroApproval' -import { BundleTxApprovalBanner } from 'common/pure/InlineBanner/banners' import { ZeroApprovalWarning } from 'common/pure/ZeroApprovalWarning' import { diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/BigPartTimeWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/BigPartTimeWarning.tsx index c209b5ca65..53ce08df83 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/BigPartTimeWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/BigPartTimeWarning.tsx @@ -1,4 +1,4 @@ -import { InlineBanner } from 'common/pure/InlineBanner' +import { InlineBanner } from '@cowprotocol/ui' import { MAX_PART_TIME } from '../../../const' import { deadlinePartsDisplay } from '../../../utils/deadlinePartsDisplay' diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx index 76be392d47..0fcd691ba8 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx @@ -1,11 +1,9 @@ -import { ExternalLink } from '@cowprotocol/ui' +import { ExternalLink, InlineBanner } from '@cowprotocol/ui' import styled from 'styled-components/macro' import { UNSUPPORTED_SAFE_LINK } from 'modules/twap/const' -import { InlineBanner } from 'common/pure/InlineBanner' - const Wrapper = styled.div` display: flex; flex-flow: column wrap; diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx index 9f7d177e55..7b8143a16a 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartTimeWarning.tsx @@ -1,4 +1,4 @@ -import { InlineBanner } from 'common/pure/InlineBanner' +import { InlineBanner } from '@cowprotocol/ui' import { MINIMUM_PART_TIME } from '../../../const' import { deadlinePartsDisplay } from '../../../utils/deadlinePartsDisplay' diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx index a72afcfb3b..13d9c31f14 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPartVolumeWarning.tsx @@ -1,7 +1,5 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { TokenAmount } from '@cowprotocol/ui' - -import { InlineBanner } from 'common/pure/InlineBanner' +import { TokenAmount, InlineBanner } from '@cowprotocol/ui' import { MINIMUM_PART_SELL_AMOUNT_FIAT } from '../../../const' diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPriceProtectionWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPriceProtectionWarning.tsx index 8aa300dc92..77d3b126c7 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPriceProtectionWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SmallPriceProtectionWarning.tsx @@ -1,4 +1,4 @@ -import { InlineBanner } from 'common/pure/InlineBanner' +import { InlineBanner } from '@cowprotocol/ui' export function SmallPriceProtectionWarning() { return ( diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SwapPriceDifferenceWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SwapPriceDifferenceWarning.tsx index 5500199b33..adf96b976d 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SwapPriceDifferenceWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SwapPriceDifferenceWarning.tsx @@ -1,4 +1,4 @@ -import { FiatAmount, TokenAmount } from '@cowprotocol/ui' +import { FiatAmount, TokenAmount, InlineBanner } from '@cowprotocol/ui' import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { NavLink } from 'react-router-dom' @@ -8,7 +8,6 @@ import { TradeUrlParams } from 'modules/trade/types/TradeRawState' import { parameterizeTradeRoute } from 'modules/trade/utils/parameterizeTradeRoute' import { Routes } from 'common/constants/routes' -import { InlineBanner } from 'common/pure/InlineBanner' import { SwapAmountDifference } from '../../../state/swapAmountDifferenceAtom' diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx index 80d87b6fac..37d1457dad 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/UnsupportedWalletWarning.tsx @@ -1,10 +1,7 @@ -import { ExternalLink } from '@cowprotocol/ui' +import { ExternalLink, InlineBanner, CowSwapSafeAppLink } from '@cowprotocol/ui' import { UNSUPPORTED_WALLET_LINK } from 'modules/twap/const' -import { CowSwapSafeAppLink } from 'common/pure/CowSwapSafeAppLink' -import { InlineBanner } from 'common/pure/InlineBanner' - export function UnsupportedWalletWarning({ isSafeViaWc }: { isSafeViaWc: boolean }) { if (isSafeViaWc) { return ( diff --git a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx index 89d8809b09..3607502257 100644 --- a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx @@ -1,11 +1,10 @@ import { useAtomValue } from 'jotai' import { advancedOrdersAtom, AdvancedOrdersWidget, FillAdvancedOrdersDerivedStateUpdater } from 'modules/advancedOrders' -import { OrdersTableWidget } from 'modules/ordersTable' -import { TabOrderTypes } from 'modules/ordersTable/pure/OrdersTableContainer' +import { OrdersTableWidget, TabOrderTypes } from 'modules/ordersTable' import * as styledEl from 'modules/trade/pure/TradePageLayout' import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' -import { TwapFormWidget, TwapUpdaters, useAllEmulatedOrders, TwapConfirmModal } from 'modules/twap' +import { TwapConfirmModal, TwapFormWidget, TwapUpdaters, useAllEmulatedOrders } from 'modules/twap' import { useTwapFormState } from 'modules/twap/hooks/useTwapFormState' import { TwapFormState } from 'modules/twap/pure/PrimaryActionButton/getTwapFormState' diff --git a/apps/cowswap-frontend/src/pages/Claim/CanUserClaimMessage.tsx b/apps/cowswap-frontend/src/pages/Claim/CanUserClaimMessage.tsx index 7605a44820..0761aca016 100644 --- a/apps/cowswap-frontend/src/pages/Claim/CanUserClaimMessage.tsx +++ b/apps/cowswap-frontend/src/pages/Claim/CanUserClaimMessage.tsx @@ -8,10 +8,10 @@ import { Trans } from '@lingui/macro' import SVG from 'react-inlinesvg' import { ClaimStatus } from 'legacy/state/claim/actions' -import { useClaimState, useClaimTimeInfo, useClaimLinks } from 'legacy/state/claim/hooks' +import { useClaimLinks, useClaimState, useClaimTimeInfo } from 'legacy/state/claim/hooks' import { ClaimCommonTypes } from 'legacy/state/claim/types' -import { IntroDescription, BannerExplainer } from './styled' +import { BannerExplainer, IntroDescription } from './styled' type ClaimIntroductionProps = Pick< ClaimCommonTypes, diff --git a/apps/cowswap-frontend/src/pages/Claim/ClaimAddress.tsx b/apps/cowswap-frontend/src/pages/Claim/ClaimAddress.tsx index 954752f675..2723fe677e 100644 --- a/apps/cowswap-frontend/src/pages/Claim/ClaimAddress.tsx +++ b/apps/cowswap-frontend/src/pages/Claim/ClaimAddress.tsx @@ -12,7 +12,7 @@ import { useClaimDispatchers, useClaimState } from 'legacy/state/claim/hooks' import { ClaimCommonTypes } from 'legacy/state/claim/types' import { CustomLightSpinner, ThemedText } from 'legacy/theme' -import { CheckAddress, InputField, InputFieldTitle, InputErrorText } from './styled' +import { CheckAddress, InputErrorText, InputField, InputFieldTitle } from './styled' export type ClaimAddressProps = Pick & { toggleWalletModal: Command | null diff --git a/apps/cowswap-frontend/src/pages/Claim/ClaimNav.tsx b/apps/cowswap-frontend/src/pages/Claim/ClaimNav.tsx index 356e317e2f..0f3dde7dce 100644 --- a/apps/cowswap-frontend/src/pages/Claim/ClaimNav.tsx +++ b/apps/cowswap-frontend/src/pages/Claim/ClaimNav.tsx @@ -7,7 +7,7 @@ import { ClaimStatus } from 'legacy/state/claim/actions' import { useClaimDispatchers, useClaimState } from 'legacy/state/claim/hooks' import { ClaimCommonTypes } from 'legacy/state/claim/types' -import { TopNav, ClaimAccount, ClaimAccountButtons } from './styled' +import { ClaimAccount, ClaimAccountButtons, TopNav } from './styled' type ClaimNavProps = Pick diff --git a/apps/cowswap-frontend/src/pages/Claim/ClaimingStatus.tsx b/apps/cowswap-frontend/src/pages/Claim/ClaimingStatus.tsx index 35b1c9692a..123b3a58a9 100644 --- a/apps/cowswap-frontend/src/pages/Claim/ClaimingStatus.tsx +++ b/apps/cowswap-frontend/src/pages/Claim/ClaimingStatus.tsx @@ -5,8 +5,7 @@ import discordImage from '@cowprotocol/assets/cow-swap/discord.svg' import twitterImage from '@cowprotocol/assets/cow-swap/twitter.svg' import { V_COW } from '@cowprotocol/common-const' import { shortenAddress } from '@cowprotocol/common-utils' -import { TokenAmount, ButtonSecondary } from '@cowprotocol/ui' -import { ExternalLink } from '@cowprotocol/ui' +import { ExternalLink, TokenAmount, ButtonSecondary } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' import { CurrencyAmount } from '@uniswap/sdk-core' diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/AlternativeLimitOrder.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/AlternativeLimitOrder.tsx new file mode 100644 index 0000000000..d081474f39 --- /dev/null +++ b/apps/cowswap-frontend/src/pages/LimitOrders/AlternativeLimitOrder.tsx @@ -0,0 +1,31 @@ +import { useCallback } from 'react' + +import styled from 'styled-components/macro' + +import { LimitOrdersWidget } from 'modules/limitOrders' +import { useHideAlternativeOrderModal } from 'modules/trade/state/alternativeOrder' + +import { NewModal } from 'common/pure/NewModal' + +const MODAL_MAX_WIDTH = 450 + +const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; + width: 100%; + max-width: ${MODAL_MAX_WIDTH}px; +` + +export function AlternativeLimitOrder() { + const hideAlternativeOrderModal = useHideAlternativeOrderModal() + + const onDismiss = useCallback(() => hideAlternativeOrderModal(), [hideAlternativeOrderModal]) + + return ( + + + + + + ) +} diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx new file mode 100644 index 0000000000..e90cde808a --- /dev/null +++ b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx @@ -0,0 +1,30 @@ +import { UiOrderType } from '@cowprotocol/types' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useOrders } from 'legacy/state/orders/hooks' + +import { LimitOrdersWidget, useIsWidgetUnlocked } from 'modules/limitOrders' +import { OrdersTableWidget, TabOrderTypes } from 'modules/ordersTable' +import * as styledEl from 'modules/trade/pure/TradePageLayout' + +export function RegularLimitOrders() { + const isUnlocked = useIsWidgetUnlocked() + const { chainId, account } = useWalletInfo() + const allLimitOrders = useOrders(chainId, account, UiOrderType.LIMIT) + + return ( + + + + + + + + + + ) +} diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx index 8b77d2362a..0f0848b6f1 100644 --- a/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx @@ -1,52 +1,41 @@ -import { UiOrderType } from '@cowprotocol/types' -import { useWalletInfo } from '@cowprotocol/wallet' - -import { useOrders } from 'legacy/state/orders/hooks' - import { AppDataUpdater } from 'modules/appData' import { + AlternativeLimitOrderUpdater, ExecutionPriceUpdater, FillLimitOrdersDerivedStateUpdater, InitialPriceUpdater, LIMIT_ORDER_SLIPPAGE, - LimitOrdersWidget, QuoteObserverUpdater, SetupLimitOrderAmountsFromUrlUpdater, TriggerAppziLimitOrdersSurveyUpdater, - useIsWidgetUnlocked, } from 'modules/limitOrders' -import { OrdersTableWidget } from 'modules/ordersTable' -import { TabOrderTypes } from 'modules/ordersTable/pure/OrdersTableContainer' -import * as styledEl from 'modules/trade/pure/TradePageLayout' +import { useIsAlternativeOrderModalVisible } from 'modules/trade/state/alternativeOrder' -export default function LimitOrderPage() { - const { chainId, account } = useWalletInfo() - const allLimitOrders = useOrders(chainId, account, UiOrderType.LIMIT) +import { AlternativeLimitOrder } from './AlternativeLimitOrder' +import { RegularLimitOrders } from './RegularLimitOrders' - const isUnlocked = useIsWidgetUnlocked() +export default function LimitOrderPage() { + const isAlternative = useIsAlternativeOrderModalVisible() return ( <> - - - - - - - - - - - - + {isAlternative ? ( + <> + + + + ) : ( + <> + + + + + + )} ) } diff --git a/apps/explorer/src/components/orders/OrderNotFound/index.tsx b/apps/explorer/src/components/orders/OrderNotFound/index.tsx index f119276e95..fd9af31c58 100644 --- a/apps/explorer/src/components/orders/OrderNotFound/index.tsx +++ b/apps/explorer/src/components/orders/OrderNotFound/index.tsx @@ -84,7 +84,7 @@ export const OrderAddressNotFound: React.FC = (): JSX.Element => { const location = useLocation() const navigate = useNavigate() const { referrer, data } = location.state || { referrer: null, data: null } - const wasRedirected = referrer ? true : false + const wasRedirected = !!referrer const showLinkData = referrer === 'tx' && data // used after refresh by remove referrer state if was redirected useEffect(() => { @@ -120,7 +120,7 @@ export const OrderAddressNotFound: React.FC = (): JSX.Element => { or Get Support - + Support icon diff --git a/apps/explorer/src/explorer/components/OrderWidget/index.tsx b/apps/explorer/src/explorer/components/OrderWidget/index.tsx index 53e95f98e1..9dd1cc1dff 100644 --- a/apps/explorer/src/explorer/components/OrderWidget/index.tsx +++ b/apps/explorer/src/explorer/components/OrderWidget/index.tsx @@ -25,7 +25,7 @@ export const OrderWidget: React.FC = () => { errors['trades'] = error } - if (errorOrderPresentInNetworkId && networkId != errorOrderPresentInNetworkId) { + if (errorOrderPresentInNetworkId && networkId !== errorOrderPresentInNetworkId) { return } diff --git a/apps/explorer/src/explorer/components/TransactionsTableWidget/index.tsx b/apps/explorer/src/explorer/components/TransactionsTableWidget/index.tsx index 7b296660c4..b65cbdc733 100644 --- a/apps/explorer/src/explorer/components/TransactionsTableWidget/index.tsx +++ b/apps/explorer/src/explorer/components/TransactionsTableWidget/index.tsx @@ -86,7 +86,7 @@ export const TransactionsTableWidget: React.FC = ({ txHash }) => { [tabViewSelected, updateQueryString] ) - if (errorTxPresentInNetworkId && networkId != errorTxPresentInNetworkId) { + if (errorTxPresentInNetworkId && networkId !== errorTxPresentInNetworkId) { return } if (redirectTo) { diff --git a/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx b/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx index 08196d184b..265ac8c0ef 100644 --- a/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx +++ b/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx @@ -198,12 +198,15 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha onError={(): void => toggleInvalid({ appData: true })} schema={schema} uiSchema={uiSchema} - > - <> - + />
-

💅 AppData prettified

+

+ + 💅 + {' '} + AppData prettified +

This is the generated and prettified file based on the input you provided on the form.

@@ -211,7 +214,12 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha {fullAppData && ( <> -

ℹ️ AppData string

+

+ + ℹ️ + + AppData string +

This is the actual content that is hashed using keccak-256 to get the{' '} AppData hex. @@ -229,7 +237,12 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha )} {!!ipfsHashInfo && ( <> -

🐮 AppData hex

+

+ + 🐮 + {' '} + AppData hex +

This is the keccak-256 hash of the above document represented in hexadecimal format.

@@ -249,7 +262,12 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha textToCopy={ipfsHashInfo.appDataHex} contentsToDisplay={ipfsHashInfo.appDataHex} /> -

🌍 IPFS CiD

+

+ + 🌍 + {' '} + IPFS CiD +

This is the{' '} diff --git a/apps/explorer/src/explorer/pages/Home/index.tsx b/apps/explorer/src/explorer/pages/Home/index.tsx index 0e78da4445..77389569bc 100644 --- a/apps/explorer/src/explorer/pages/Home/index.tsx +++ b/apps/explorer/src/explorer/pages/Home/index.tsx @@ -9,6 +9,7 @@ import { TokensTableWidget } from '../../components/TokensTableWidget' import { Helmet } from 'react-helmet' import { APP_TITLE } from '../../const' import { SUBGRAPH_URLS } from '../../../consts/subgraphUrls' +import { SupportedChainId } from '@cowprotocol/cow-sdk' const Wrapper = styled(WrapperMod)` max-width: 140rem; @@ -47,10 +48,17 @@ const SummaryWrapper = styled.section` } ` +const SHOW_TOKENS_TABLE: Record = { + [SupportedChainId.MAINNET]: true, + [SupportedChainId.GNOSIS_CHAIN]: false, // Gchain data is not reliable + [SupportedChainId.SEPOLIA]: false, // No data for Sepolia +} + export const Home: React.FC = () => { const networkId = useNetworkId() || undefined const showCharts = !!networkId && SUBGRAPH_URLS[networkId] !== null + const showTokensTable = !!networkId && SHOW_TOKENS_TABLE[networkId] return ( @@ -63,7 +71,7 @@ export const Home: React.FC = () => { {showCharts && ( <> - + {showTokensTable && } )} diff --git a/libs/common-utils/src/misc.ts b/libs/common-utils/src/misc.ts index d323ef1e6a..2a608e712d 100644 --- a/libs/common-utils/src/misc.ts +++ b/libs/common-utils/src/misc.ts @@ -11,6 +11,8 @@ const PROVIDER_REJECT_REQUEST_CODES = [4001, -32000] // See https://eips.ethereu const PROVIDER_REJECT_REQUEST_ERROR_MESSAGES = [ 'User denied message signature', 'User rejected', + 'User denied', + 'rejected transaction', 'Transaction was rejected', ] diff --git a/libs/common-utils/src/sentry.ts b/libs/common-utils/src/sentry.ts index 90745bce86..06636f5d01 100644 --- a/libs/common-utils/src/sentry.ts +++ b/libs/common-utils/src/sentry.ts @@ -15,3 +15,10 @@ export function reportAppDataWithHooks(params: Record): void { contexts: { params }, }) } + +export function reportPlaceOrderWithExpiredQuote(params: Record): void { + Sentry.captureException('Attempt to place order with expired quote', { + tags: { errorType: 'placeOrderWithExpiredQuote' }, + contexts: { params }, + }) +} diff --git a/libs/tokens/src/containers/OnlyUniswapListAvailableBanner/index.tsx b/libs/tokens/src/containers/OnlyUniswapListAvailableBanner/index.tsx deleted file mode 100644 index 5edcf93dfd..0000000000 --- a/libs/tokens/src/containers/OnlyUniswapListAvailableBanner/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useIsCuratedListAvailable } from '../../hooks/lists/useIsCuratedListAvailable' -import styled from 'styled-components/macro' -import { UI } from '@cowprotocol/ui' -import { atomWithStorage } from 'jotai/utils' -import { useAtom } from 'jotai' -import { X } from 'react-feather' - -const Wrapper = styled.div` - width: 100%; - padding: 6px 0; - font-size: 14px; - text-align: center; - background-color: var(${UI.COLOR_PRIMARY}); - color: var(${UI.COLOR_WHITE}); - position: relative; -` - -const CloseIcon = styled(X)` - height: 28px; - width: 28px; - opacity: 0.6; - transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out; - position: absolute; - right: 10px; - top: 50%; - margin-top: -15px; - - &:hover { - cursor: pointer; - opacity: 1; - } - - > line { - stroke: var(${UI.COLOR_WHITE}); - } -` - -const bannerClosedAtom = atomWithStorage('OnlyUniswapListAvailableBanner', false) - -/** - * TODO: get review from Michel - */ -export function OnlyUniswapListAvailableBanner() { - const [isBannerClose, setBannerClosed] = useAtom(bannerClosedAtom) - const isOnlyUniswapListAvailable = useIsCuratedListAvailable() - - if (!isOnlyUniswapListAvailable || isBannerClose) return null - - return ( - -

The list of assets for trading is restricted due to your geographical location

- setBannerClosed(true)} /> - - ) -} diff --git a/libs/tokens/src/index.ts b/libs/tokens/src/index.ts index 7759e144c8..c114893645 100644 --- a/libs/tokens/src/index.ts +++ b/libs/tokens/src/index.ts @@ -1,6 +1,5 @@ // Containers import { userAddedTokenListsAtomv2Migration } from './migrations/userAddedTokenListsAtomv2Migration' -export { OnlyUniswapListAvailableBanner } from './containers/OnlyUniswapListAvailableBanner' // Run migrations first of all // TODO: remove it after 01.04.2024 diff --git a/libs/tokens/src/updaters/TokensListsUpdater/index.ts b/libs/tokens/src/updaters/TokensListsUpdater/index.ts index 9d537d7ed4..0c883e7aa2 100644 --- a/libs/tokens/src/updaters/TokensListsUpdater/index.ts +++ b/libs/tokens/src/updaters/TokensListsUpdater/index.ts @@ -13,6 +13,7 @@ import { upsertListsAtom } from '../../state/tokenLists/tokenListsActionsAtom' import { atomWithStorage } from 'jotai/utils' import { atomWithPartialUpdate, isInjectedWidget } from '@cowprotocol/common-utils' import { getJotaiMergerStorage } from '@cowprotocol/core' +import * as Sentry from '@sentry/browser' const { atom: lastUpdateTimeAtom, updateAtom: updateLastUpdateTimeAtom } = atomWithPartialUpdate( atomWithStorage>( @@ -31,9 +32,10 @@ const NETWORKS_WITHOUT_RESTRICTIONS = [SupportedChainId.SEPOLIA, SupportedChainI interface TokensListsUpdaterProps { chainId: SupportedChainId + isGeoBlockEnabled: boolean } -export function TokensListsUpdater({ chainId: currentChainId }: TokensListsUpdaterProps) { +export function TokensListsUpdater({ chainId: currentChainId, isGeoBlockEnabled }: TokensListsUpdaterProps) { const { chainId } = useAtomValue(environmentAtom) const setEnvironment = useSetAtom(updateEnvironmentAtom) const allTokensLists = useAtomValue(allListsSourcesAtom) @@ -71,7 +73,7 @@ export function TokensListsUpdater({ chainId: currentChainId }: TokensListsUpdat // Check if a user is from US and use Uniswap list, because of the SEC regulations useEffect(() => { - if (isInjectedWidget()) return + if (!isGeoBlockEnabled || isInjectedWidget()) return if (NETWORKS_WITHOUT_RESTRICTIONS.includes(chainId)) { setEnvironment({ useCuratedListOnly: false }) @@ -88,8 +90,19 @@ export function TokensListsUpdater({ chainId: currentChainId }: TokensListsUpdat updateLastUpdateTime({ [chainId]: 0 }) } }) + .catch((error) => { + const sentryError = Object.assign(error, { + name: 'GeoBlockingError', + }) + + Sentry.captureException(sentryError, { + tags: { + errorType: 'GeoBlockingError', + }, + }) + }) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chainId]) + }, [chainId, isGeoBlockEnabled]) return null } diff --git a/libs/ui/src/consts.ts b/libs/ui/src/consts.ts index 2ff084b7c6..bc5e196cf6 100644 --- a/libs/ui/src/consts.ts +++ b/libs/ui/src/consts.ts @@ -1 +1,3 @@ export const AMOUNTS_FORMATTING_FEATURE_FLAG = 'highlight-amounts-formatting' +export const SAFE_COW_APP_LINK = 'https://app.safe.global/share/safe-app?appUrl=https%3A%2F%2Fswap.cow.fi&chain=eth' +export const LINK_GUIDE_ADD_CUSTOM_TOKEN = 'https://blog.cow.fi/how-to-add-custom-tokens-on-cow-swap-a72d677c78c0' diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index fc52b7318e..18a7a12f08 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -1,6 +1,12 @@ export * from './pure/Button' +export * from './pure/ButtonSecondaryAlt' +export * from './pure/CowSwapSafeAppLink' export * from './pure/CenteredDots' export * from './pure/LongLoadText' +export * from './pure/InlineBanner' +export * from './pure/InlineBanner/banners' +export * from './pure/Icon' +export * from './pure/LinkStyledButton' export * from './pure/Loader' export { loadingOpacityMixin, LoadingRows } from './pure/Loader/styled' export * from './pure/Row' @@ -15,3 +21,4 @@ export * from './pure/ExternalLink' export * from './pure/BackButton' export * from './enum' export * from './types' +export * from './consts' diff --git a/libs/ui/src/pure/Button/index.tsx b/libs/ui/src/pure/Button/index.tsx index b8ec23a0c2..8710a9655b 100644 --- a/libs/ui/src/pure/Button/index.tsx +++ b/libs/ui/src/pure/Button/index.tsx @@ -5,7 +5,6 @@ import { ButtonProps } from 'rebass/styled-components' import styled from 'styled-components' import { RowBetween } from '../Row' -import { ButtonSize, UI } from '../../enum' import { ButtonConfirmedStyle as ButtonConfirmedStyleMod, @@ -14,6 +13,7 @@ import { ButtonOutlined as ButtonOutlinedMod, ButtonPrimary as ButtonPrimaryMod, } from './ButtonMod' +import { ButtonSize, UI } from '../../enum' export * from './ButtonMod' diff --git a/apps/cowswap-frontend/src/common/pure/ButtonSecondary/index.tsx b/libs/ui/src/pure/ButtonSecondaryAlt/index.tsx similarity index 80% rename from apps/cowswap-frontend/src/common/pure/ButtonSecondary/index.tsx rename to libs/ui/src/pure/ButtonSecondaryAlt/index.tsx index fbc6374c73..16f56ab8df 100644 --- a/apps/cowswap-frontend/src/common/pure/ButtonSecondary/index.tsx +++ b/libs/ui/src/pure/ButtonSecondaryAlt/index.tsx @@ -1,8 +1,8 @@ -import { UI } from '@cowprotocol/ui' +import { UI } from '../../enum' import styled from 'styled-components/macro' -export const ButtonSecondary = styled.button<{ padding?: string; minHeight?: string }>` +export const ButtonSecondaryAlt = styled.button<{ padding?: string; minHeight?: string }>` background: var(${UI.COLOR_PRIMARY}); color: var(${UI.COLOR_BUTTON_TEXT}); font-size: 12px; diff --git a/apps/cowswap-frontend/src/common/pure/CowSwapSafeAppLink/index.tsx b/libs/ui/src/pure/CowSwapSafeAppLink/index.tsx similarity index 53% rename from apps/cowswap-frontend/src/common/pure/CowSwapSafeAppLink/index.tsx rename to libs/ui/src/pure/CowSwapSafeAppLink/index.tsx index 3b6ca0db35..87b09bb1fc 100644 --- a/apps/cowswap-frontend/src/common/pure/CowSwapSafeAppLink/index.tsx +++ b/libs/ui/src/pure/CowSwapSafeAppLink/index.tsx @@ -1,6 +1,5 @@ -import { ExternalLink } from '@cowprotocol/ui' - -import { SAFE_COW_APP_LINK } from 'common/constants/common' +import { ExternalLink } from '../ExternalLink' +import { SAFE_COW_APP_LINK } from '../../consts' export function CowSwapSafeAppLink() { return CoW Swap Safe App↗ diff --git a/apps/cowswap-frontend/src/common/pure/Icon/index.cosmos.tsx b/libs/ui/src/pure/Icon/index.cosmos.tsx similarity index 84% rename from apps/cowswap-frontend/src/common/pure/Icon/index.cosmos.tsx rename to libs/ui/src/pure/Icon/index.cosmos.tsx index 377bc4204d..7415703ce6 100644 --- a/apps/cowswap-frontend/src/common/pure/Icon/index.cosmos.tsx +++ b/libs/ui/src/pure/Icon/index.cosmos.tsx @@ -1,8 +1,6 @@ -import { UI } from '@cowprotocol/ui' - import styled from 'styled-components/macro' - -import { Icon, IconType } from './index' +import { UI } from '../../enum' +import { Icon, IconType } from '.' const Wrapper = styled.div` width: 400px; diff --git a/apps/cowswap-frontend/src/common/pure/Icon/index.tsx b/libs/ui/src/pure/Icon/index.tsx similarity index 98% rename from apps/cowswap-frontend/src/common/pure/Icon/index.tsx rename to libs/ui/src/pure/Icon/index.tsx index fe70dc4fe4..88ddb0f484 100644 --- a/apps/cowswap-frontend/src/common/pure/Icon/index.tsx +++ b/libs/ui/src/pure/Icon/index.tsx @@ -2,7 +2,8 @@ import iconInformation from '@cowprotocol/assets/cow-swap/alert-circle.svg' import iconAlert from '@cowprotocol/assets/cow-swap/alert.svg' import iconDanger from '@cowprotocol/assets/cow-swap/alert.svg' import iconSuccess from '@cowprotocol/assets/cow-swap/check.svg' -import { UI } from '@cowprotocol/ui' + +import { UI } from '../../enum' import SVG from 'react-inlinesvg' import styled from 'styled-components/macro' diff --git a/apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx b/libs/ui/src/pure/InlineBanner/banners.tsx similarity index 96% rename from apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx rename to libs/ui/src/pure/InlineBanner/banners.tsx index 6cee55e2e0..9dbecf329b 100644 --- a/apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx +++ b/libs/ui/src/pure/InlineBanner/banners.tsx @@ -1,13 +1,11 @@ import { Command } from '@cowprotocol/types' -import { TokenAmount } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import styled from 'styled-components/macro' -import { Nullish } from 'types' - -import { LinkStyledButton } from 'legacy/theme' - -import { ButtonSecondary } from '../ButtonSecondary' +import { TokenAmount } from '../TokenAmount' +import { Nullish } from '../../types' +import { LinkStyledButton } from '../LinkStyledButton' +import { ButtonSecondary } from '../Button' import { CowSwapSafeAppLink } from '../CowSwapSafeAppLink' import { InlineBanner, InlineBannerProps } from './index' diff --git a/apps/cowswap-frontend/src/common/pure/InlineBanner/index.cosmos.tsx b/libs/ui/src/pure/InlineBanner/index.cosmos.tsx similarity index 100% rename from apps/cowswap-frontend/src/common/pure/InlineBanner/index.cosmos.tsx rename to libs/ui/src/pure/InlineBanner/index.cosmos.tsx diff --git a/apps/cowswap-frontend/src/common/pure/InlineBanner/index.tsx b/libs/ui/src/pure/InlineBanner/index.tsx similarity index 80% rename from apps/cowswap-frontend/src/common/pure/InlineBanner/index.tsx rename to libs/ui/src/pure/InlineBanner/index.tsx index a3550e72c2..e5a0b5abb7 100644 --- a/apps/cowswap-frontend/src/common/pure/InlineBanner/index.tsx +++ b/libs/ui/src/pure/InlineBanner/index.tsx @@ -1,12 +1,11 @@ import { ReactNode } from 'react' -import { UI } from '@cowprotocol/ui' +import { UI } from '../../enum' +import { Icon, IconType } from '../Icon' +import { BannerOrientation } from './banners' import styled from 'styled-components/macro' -import { Icon, IconType } from 'common/pure/Icon' -import { BannerOrientation } from 'common/pure/InlineBanner/banners' - export type BannerType = 'alert' | 'information' | 'success' | 'danger' | 'savings' interface ColorEnums { @@ -60,6 +59,8 @@ const Wrapper = styled.span<{ orientation?: BannerOrientation iconSize?: number padding?: string + margin?: string + width?: string }>` display: flex; align-items: center; @@ -68,13 +69,14 @@ const Wrapper = styled.span<{ color: ${({ colorEnums }) => `var(${colorEnums.text})`}; gap: 24px 10px; border-radius: ${({ borderRadius = '16px' }) => borderRadius}; - margin: auto; + margin: ${({ margin = 'auto' }) => margin}; padding: ${({ padding = '16px' }) => padding}; font-size: 14px; font-weight: 400; line-height: 1.2; - width: 100%; + width: ${({ width = '100%' }) => width}; + // Icon + Text content wrapper > span { display: flex; justify-content: center; @@ -83,21 +85,31 @@ const Wrapper = styled.span<{ orientation === BannerOrientation.Horizontal ? 'row' : 'column wrap'}; gap: 10px; width: 100%; + } + + // Text content + > span > span { + display: flex; + flex-flow: row wrap; + align-items: center; + gap: 10px; + justify-content: ${({ orientation = BannerOrientation.Vertical }) => + orientation === BannerOrientation.Horizontal ? 'flex-start' : 'center'}; + } - ${({ theme }) => theme.mediaWidth.upToSmall` - flex-flow: column wrap; - gap: 16px; - `}; + > span > span a { + color: inherit; + text-decoration: underline; } - > span > strong { + > span > span > strong { display: flex; align-items: center; gap: 6px; color: ${({ colorEnums }) => `var(${colorEnums.text})`}; } - > span > p { + > span > span > p { line-height: 1.4; margin: auto; padding: 0; @@ -106,7 +118,7 @@ const Wrapper = styled.span<{ orientation === BannerOrientation.Horizontal ? 'left' : 'center'}; } - > span > i { + > span > span > i { font-style: normal; font-size: 32px; line-height: 1; @@ -123,6 +135,8 @@ export type InlineBannerProps = { iconSize?: number iconPadding?: string padding?: string + margin?: string + width?: string } export function InlineBanner({ @@ -133,8 +147,10 @@ export function InlineBanner({ borderRadius, orientation, iconSize, - iconPadding, + iconPadding = '0', padding, + margin, + width, }: InlineBannerProps) { const effectiveBannerType = bannerType || 'alert' const colorEnums = getColorEnums(effectiveBannerType) @@ -146,6 +162,8 @@ export function InlineBanner({ borderRadius={borderRadius} orientation={orientation} padding={padding} + margin={margin} + width={width} > {!hideIcon && colorEnums.icon && ( @@ -158,7 +176,7 @@ export function InlineBanner({ /> )} {!hideIcon && colorEnums.iconText && {colorEnums.iconText}} - {children} + {children} ) diff --git a/libs/ui/src/pure/LinkStyledButton/index.tsx b/libs/ui/src/pure/LinkStyledButton/index.tsx new file mode 100644 index 0000000000..ef1169bacd --- /dev/null +++ b/libs/ui/src/pure/LinkStyledButton/index.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components/macro' + +// A button that triggers some onClick result, but looks like a link. +export const LinkStyledButton = styled.button<{ disabled?: boolean; bg?: boolean; isCopied?: boolean }>` + border: none; + text-decoration: none; + background: none; + cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; + color: inherit; + font-weight: 500; + opacity: ${({ disabled }) => (disabled ? 0.7 : 1)}; + + :hover { + text-decoration: ${({ disabled }) => (disabled ? null : 'underline')}; + } + + :focus { + outline: none; + text-decoration: ${({ disabled }) => (disabled ? null : 'underline')}; + } + + :active { + text-decoration: none; + } +` diff --git a/libs/ui/src/pure/Popover/index.tsx b/libs/ui/src/pure/Popover/index.tsx index b239845868..f7186f6472 100644 --- a/libs/ui/src/pure/Popover/index.tsx +++ b/libs/ui/src/pure/Popover/index.tsx @@ -1,8 +1,7 @@ import { UI } from '../../enum' import styled from 'styled-components' -import PopoverMod, { Arrow as ArrowMod, PopoverContainer as PopoverContainerMod } from './PopoverMod' -import { PopoverProps } from './PopoverMod' +import PopoverMod, { Arrow as ArrowMod, PopoverContainer as PopoverContainerMod, PopoverProps } from './PopoverMod' export * from './PopoverMod'