diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index 3fe1cfb624b..7aa64c5e993 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -84,6 +84,8 @@ msgstr "Because you are using a smart contract wallet, you will pay a separate g #~ msgid "cancelled. Too many order cancellations" #~ msgstr "cancelled. Too many order cancellations" +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/twap/containers/SetupFallbackHandlerWarning/index.tsx msgid "Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not getting created because of that. Please, update the fallback handler in order to make the orders work again." msgstr "Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not getting created because of that. Please, update the fallback handler in order to make the orders work again." @@ -1181,6 +1183,7 @@ msgstr "Switch Network" #~ msgid "Invalid code" #~ msgstr "Invalid code" +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTabs/OrdersTabs.pure.tsx msgid "warning" @@ -1741,6 +1744,8 @@ msgstr "Deprecated Network" #~ msgid "Limit price (incl.costs)" #~ msgstr "Limit price (incl.costs)" +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/twap/containers/SetupFallbackHandlerWarning/index.tsx msgid "Update fallback handler" msgstr "Update fallback handler" diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx index 46be608facd..06af53b6239 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx @@ -14,6 +14,7 @@ import { OrderStatus } from 'legacy/state/orders/actions' import { getEstimatedExecutionPrice } from 'legacy/state/orders/utils' import { PendingOrderPrices } from 'modules/orders' +import { useIsFallbackHandlerRequired } from 'modules/twap' import { useIsProviderNetworkDeprecated } from 'common/hooks/useIsProviderNetworkDeprecated' import { useSafeMemo } from 'common/hooks/useSafeMemo' @@ -30,6 +31,7 @@ import { TableRow } from './OrderRow.styled' import { usePricesDifference } from '../../hooks/usePricesDifference' import { OrderContextMenu } from '../../pure/ContextMenu/OrderContextMenu.pure' import { CurrencyAmountItem } from '../../pure/CurrencyAmountItem/CurrencyAmountItem.pure' +import { WarningReason } from '../../pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants' import { OrderFillsAt } from '../../pure/OrderFillsAt/OrderFillsAt.pure' import { OrderFillsAtWithDistance } from '../../pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure' import { OrderMarketPrice } from '../../pure/OrderMarketPrice/OrderMarketPrice.pure' @@ -39,9 +41,13 @@ import { TableRowCheckboxWrapper, } from '../../pure/OrdersTable/Row/Checkbox/Checkbox.styled' import { OrderRowWarningEstimatedPrice } from '../../pure/OrdersTable/Row/WarningEstimatedPrice/OrderRowWarningEstimatedPrice.pure' -import { WarningTooltip } from '../../pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure' +import { + FallbackHandlerWarningTooltip, + WarningTooltip, +} from '../../pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure' import { OrderStatusBox } from '../../pure/OrderStatusBox/OrderStatusBox.pure' import { OrderActions } from '../../state/ordersTable.types' +import { getIsFallbackHandlerUnfillable } from '../../utils/getIsFallbackHandlerUnfillable' import { OrderParams } from '../../utils/getOrderParams' import { shouldShowDashForExpiration } from '../../utils/shouldShowDashForExpiration' import { getActivityUrl } from '../../utils/url/getActivityUrl' @@ -148,17 +154,27 @@ export function OrderRow({ const isExecutedPriceZero = executedPriceInverted !== undefined && executedPriceInverted?.equalTo(ZERO_FRACTION) - const isUnfillable = !percentIsAlmostHundred(filledPercentDisplay) && (isExecutedPriceZero || withWarning) + // The Safe ComposableCoW fallback handler being reset/removed is a per-account condition, so it is + // resolved here in the view (not persisted onto the order) and combined with the per-order status. + // It only applies to composable (TWAP) orders; surface it with the same danger design as the + // balance/allowance warnings (see issue #5426). + const isFallbackHandlerRequired = useIsFallbackHandlerRequired() + const isFallbackHandlerUnfillable = + isTwapTable === true && getIsFallbackHandlerUnfillable(status, isFallbackHandlerRequired) + + const isUnfillable = + isFallbackHandlerUnfillable || + (!percentIsAlmostHundred(filledPercentDisplay) && (isExecutedPriceZero || withWarning)) const inputTokenSymbol = order.inputToken.symbol || '' - // NOTE: Don't internationalize this, the text is being used as a flag... - const warningText = - hasEnoughBalance === false - ? `Insufficient balance` + const warningReason = isFallbackHandlerUnfillable + ? WarningReason.FallbackHandler + : hasEnoughBalance === false + ? WarningReason.Balance : hasEnoughAllowance === false - ? `Insufficient allowance` - : `Unfillable` + ? WarningReason.Allowance + : undefined const onApprove = withAllowanceWarning ? () => orderActions.approveOrderToken(order.inputToken) : undefined @@ -182,6 +198,7 @@ export function OrderRow({ estimatedExecutionPrice={estimatedExecutionPrice} estimatedPriceWarning={estimatedPriceWarning} isChild={isChild} + isFallbackHandlerRequired={isFallbackHandlerRequired} isInverted={isInverted} isSafeWallet={isSafeWallet} isTwapTable={isTwapTable} @@ -192,7 +209,7 @@ export function OrderRow({ prices={prices} rateInfoParams={rateInfoParams} spotPrice={spotPrice} - warningText={warningText} + warningReason={warningReason} withWarning={withWarning} /> ) @@ -252,10 +269,11 @@ export function OrderRow({ orderActions.approveOrderToken(order.inputToken)} - /> + isFallbackHandlerUnfillable ? ( + + ) : ( + orderActions.approveOrderToken(order.inputToken)} + /> + ) } /> diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx new file mode 100644 index 00000000000..04f62797f61 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx @@ -0,0 +1,66 @@ +import { ReactNode } from 'react' + +import { i18n } from '@lingui/core' +import { I18nProvider } from '@lingui/react' + +import { fireEvent, render, screen } from '@testing-library/react' + +import { WarningReason } from './orderEstimatedExecutionPrice.constants' +import { OrderEstimatedExecutionPrice } from './OrderEstimatedExecutionPrice.pure' + +jest.mock('react-inlinesvg', () => { + return function MockSvg() { + return + } +}) + +function renderWithI18n(ui: ReactNode): ReturnType { + return render({ui}) +} + +beforeAll(() => { + window.matchMedia = + window.matchMedia || + (() => + ({ + matches: false, + addEventListener: () => undefined, + removeEventListener: () => undefined, + addListener: () => undefined, + removeListener: () => undefined, + }) as unknown as MediaQueryList) +}) + +describe('OrderEstimatedExecutionPrice() – fallback handler warning', () => { + it('renders the "Update fallback handler" reason label', () => { + renderWithI18n( + , + ) + + expect(screen.getByText('Update fallback handler')).not.toBeNull() + }) + + it('reveals the fallback handler explanation on hover', async () => { + renderWithI18n( + , + ) + + fireEvent.mouseEnter(screen.getByText('Update fallback handler')) + + expect(await screen.findByText(/Your Safe fallback handler was changed/i)).not.toBeNull() + }) +}) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx index b7a54e3bf5c..875f9a90965 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx @@ -14,6 +14,7 @@ import { Nullish } from 'types' import { HIGH_FEE_WARNING_PERCENTAGE, PENDING_EXECUTION_THRESHOLD_PERCENTAGE } from 'common/constants/common' +import { WarningReason } from './orderEstimatedExecutionPrice.constants' import * as styledEl from './OrderEstimatedExecutionPrice.styled' import * as orderRowEl from '../../containers/OrderRow/OrderRow.styled' @@ -32,7 +33,7 @@ export interface OrderEstimatedExecutionPriceProps extends TokenAmountProps { onApprove?: Command percentageDifference?: Percent percentageFee?: Percent - warningText?: string + warningReason?: WarningReason } type UnlikelyToExecuteWarningProps = { @@ -57,7 +58,7 @@ export function OrderEstimatedExecutionPrice({ percentageDifference, percentageFee, tokenSymbol, - warningText, + warningReason, ...rest }: OrderEstimatedExecutionPriceProps) { const [approveClicked, setApproveClicked] = useState(true) @@ -101,27 +102,55 @@ export function OrderEstimatedExecutionPrice({ return () => clearTimeout(timeout) }, [approveClicked]) - const internationalizedWarningText = - warningText === 'Insufficient balance' + const isFallbackHandlerWarning = warningReason === WarningReason.FallbackHandler + const isAllowanceWarning = warningReason === WarningReason.Allowance + const isBalanceWarning = warningReason === WarningReason.Balance + + const warningLabel = isFallbackHandlerWarning + ? t`Update fallback handler` + : isBalanceWarning ? t`Insufficient balance` - : warningText === 'Insufficient allowance' + : isAllowanceWarning ? t`Insufficient allowance` : t`Unfillable` const unfillableLabel = ( - {(warningText === 'Insufficient allowance' || warningText === 'Insufficient balance') && ( + {isFallbackHandlerWarning && ( + +

{warningLabel}

+

+ + Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not + getting created because of that. Please, update the fallback handler in order to make the orders work + again. + +

+ + } + bgColor={`var(${UI.COLOR_DANGER_BG})`} + color={`var(${UI.COLOR_DANGER_TEXT})`} + > + + + {warningLabel} + +
+ )} + {(isAllowanceWarning || isBalanceWarning) && ( <> -

{internationalizedWarningText}

+

{warningLabel}

- {warningText === 'Insufficient allowance' + {isAllowanceWarning ? t`The order remains open. Execution requires adequate allowance. Approve the token to proceed.` : t`The order remains open. Execution requires sufficient balance.`}

- {warningText === 'Insufficient allowance' && handleApproveClick && ( + {isAllowanceWarning && handleApproveClick && ( {approveClicked ? ( @@ -141,10 +170,10 @@ export function OrderEstimatedExecutionPrice({ > - {internationalizedWarningText} + {warningLabel}
- {warningText === 'Insufficient allowance' && + {isAllowanceWarning && handleApproveClick && (approveClicked ? ( diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants.ts new file mode 100644 index 00000000000..b248b8f87b3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants.ts @@ -0,0 +1,12 @@ +/** + * Reason an open order is surfaced as unfillable. Used as a flag to pick the danger tooltip/label; + * the human-readable, internationalized text is derived from it in `OrderEstimatedExecutionPrice`. + * + * `FallbackHandler`: an open TWAP order whose Safe ComposableCoW fallback handler was reset can no + * longer be created (see issue #5426). + */ +export enum WarningReason { + Balance = 'balance', + Allowance = 'allowance', + FallbackHandler = 'fallback-handler', +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAt/OrderFillsAt.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAt/OrderFillsAt.pure.tsx index 517eb7f0f2f..b5ad1f79795 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAt/OrderFillsAt.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAt/OrderFillsAt.pure.tsx @@ -24,6 +24,7 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder' import * as styledEl from '../../containers/OrderRow/OrderRow.styled' import { useFeeAmountDifference } from '../../hooks/useFeeAmountDifference' +import { WarningReason } from '../OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants' import { OrderEstimatedExecutionPrice } from '../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' import { TwapOrderStatus } from '../TwapOrderStatus/TwapOrderStatus.pure' @@ -33,6 +34,7 @@ export interface OrderFillsAtProps { isTwapTable?: boolean isChild?: boolean isUnfillable: boolean + isFallbackHandlerRequired?: boolean isInverted: boolean withWarning: boolean estimatedPriceWarning: ReactNode | undefined @@ -42,7 +44,7 @@ export interface OrderFillsAtProps { rateInfoParams: RateInfoParams prices: PendingOrderPrices | undefined | null spotPrice: Nullish> - warningText: string + warningReason?: WarningReason onApprove?: Command } @@ -57,6 +59,7 @@ export function OrderFillsAt({ isTwapTable, isChild, isUnfillable, + isFallbackHandlerRequired, estimatedPriceWarning, childOrders, estimatedExecutionPrice, @@ -64,7 +67,7 @@ export function OrderFillsAt({ rateInfoParams, prices, spotPrice, - warningText, + warningReason, withWarning, onApprove, }: OrderFillsAtProps) { @@ -103,7 +106,11 @@ export function OrderFillsAt({ return ( estimatedPriceWarning || ( - + - @@ -199,7 +206,7 @@ export function OrderFillsAt({ amountFee={feeAmount} canShowWarning={getUiOrderType(order) !== UiOrderType.SWAP && !isUnfillable} isUnfillable={withWarning} - warningText={warningText} + warningReason={warningReason} onApprove={onApprove} /> )} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure.tsx index 7ad1915a7f5..fe42b9228db 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure.tsx @@ -14,6 +14,7 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder' import * as styledEl from '../../containers/OrderRow/OrderRow.styled' import { getDistanceColor } from '../../utils/getDistanceColor' +import { WarningReason } from '../OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants' import { OrderEstimatedExecutionPrice } from '../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' import { TwapOrderStatus } from '../TwapOrderStatus/TwapOrderStatus.pure' import { TwapScheduledOrderStatus } from '../TwapScheduledOrderStatus/TwapScheduledOrderStatus.pure' @@ -28,8 +29,9 @@ export interface OrderFillsAtWithDistanceProps { order: ParsedOrder isInverted: boolean isUnfillable: boolean + isFallbackHandlerRequired?: boolean withWarning: boolean - warningText: string + warningReason?: WarningReason onApprove?: Command priceDiffs: PriceDifference orderFillsAt: ReactNode @@ -49,8 +51,9 @@ export function OrderFillsAtWithDistance({ order, isInverted, isUnfillable, + isFallbackHandlerRequired, withWarning, - warningText, + warningReason, onApprove, priceDiffs, orderFillsAt, @@ -60,7 +63,11 @@ export function OrderFillsAtWithDistance({ return ( estimatedPriceWarning || ( - + diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/Group/OrdersTableRowGroup.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/Group/OrdersTableRowGroup.pure.tsx index 3bc59248627..c85bc0cf643 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/Group/OrdersTableRowGroup.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/Group/OrdersTableRowGroup.pure.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components/macro' import { OrderStatus } from 'legacy/state/orders/actions' import type { PendingOrderPrices } from 'modules/orders' +import { useIsFallbackHandlerRequired } from 'modules/twap' import { OrderRow } from '../../../../containers/OrderRow/OrderRow.container' import { OrderActions, OrderTableGroup } from '../../../../state/ordersTable.types' @@ -57,6 +58,10 @@ export function OrdersTableRowGroup({ }: OrdersTableRowGroupProps): ReactNode { const { parent, children } = item + // Per-account condition (the Safe's ComposableCoW fallback handler was reset); resolved here in the + // view and passed to the status badge rather than persisted onto the order (see issue #5426). + const isFallbackHandlerRequired = useIsFallbackHandlerRequired() + const [isCollapsed, setIsCollapsed] = useState(true) const [currentPage, setCurrentPage] = useState(1) @@ -102,6 +107,7 @@ export function OrdersTableRowGroup({ parent={parent} childrenLength={childrenLength} isCollapsed={isCollapsed} + isFallbackHandlerRequired={isFallbackHandlerRequired} onToggle={() => setIsCollapsed((state) => !state)} onClick={() => orderActions.selectReceiptOrder(parent)} childOrders={childrenWithParams} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningEstimatedPrice/OrderRowWarningEstimatedPrice.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningEstimatedPrice/OrderRowWarningEstimatedPrice.pure.tsx index ce6858cedd3..5d2841da2cd 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningEstimatedPrice/OrderRowWarningEstimatedPrice.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningEstimatedPrice/OrderRowWarningEstimatedPrice.pure.tsx @@ -4,13 +4,12 @@ import { BalancesAndAllowances } from '@cowprotocol/balances-and-allowances' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Token } from '@cowprotocol/currency' -import { useLingui } from '@lingui/react/macro' - import { OrderStatus } from 'legacy/state/orders/actions' import { ParsedOrder } from 'utils/orderUtils/parseOrder' import { getOrderParams } from '../../../../utils/getOrderParams' +import { WarningReason } from '../../../OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants' import { OrderEstimatedExecutionPrice } from '../../../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' interface OrderRowWarningEstimatedPriceProps { @@ -31,7 +30,6 @@ interface OrderRowWarningEstimatedPriceProps { export function OrderRowWarningEstimatedPrice(props: OrderRowWarningEstimatedPriceProps) { const { order, isInverted, withAllowanceWarning, approveOrderToken } = props const warningChildWithParams = findWarningChildWithParams(props) - const { t } = useLingui() if (!warningChildWithParams?.params) return null @@ -42,12 +40,12 @@ export function OrderRowWarningEstimatedPrice(props: OrderRowWarningEstimatedPri isInverted={isInverted} isUnfillable={true} canShowWarning={true} - warningText={ + warningReason={ warningChildWithParams.params.hasEnoughAllowance === false - ? t`Insufficient allowance` + ? WarningReason.Allowance : warningChildWithParams.params.hasEnoughBalance === false - ? t`Insufficient balance` - : t`Unfillable` + ? WarningReason.Balance + : undefined } onApprove={ warningChildWithParams.params.hasEnoughAllowance === false diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx index ac4fc3b2828..fa5d3a98370 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx @@ -145,3 +145,38 @@ function BalanceWarning({ symbol, isScheduled }: WarningProps) { ) } + +// Shown on the status badge of a TWAP order (and its open parts) whose Safe ComposableCoW fallback +// handler was reset, so open orders can no longer be created (see issue #5426). +// TODO: Add proper return type annotation +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function FallbackHandlerWarningTooltip({ children }: { children?: ReactNode }) { + const tooltipContent = ( + + +

+ Update fallback handler +

+

+ + Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not getting + created because of that. Please, update the fallback handler in order to make the orders work again. + +

+
+
+ ) + + return ( + + } + /> + {children} + + ) +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.test.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.test.tsx new file mode 100644 index 00000000000..e37b48c9e39 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react' + +import { OrderStatus } from 'legacy/state/orders/actions' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +import { TwapOrderStatus } from './TwapOrderStatus.pure' + +jest.mock('react-inlinesvg', () => { + return function MockSvg() { + return + } +}) + +beforeAll(() => { + window.matchMedia = + window.matchMedia || + (() => + ({ + matches: false, + addEventListener: () => undefined, + removeEventListener: () => undefined, + addListener: () => undefined, + removeListener: () => undefined, + }) as unknown as MediaQueryList) +}) + +function childOrder(overrides: Partial): ParsedOrder { + return { + status: OrderStatus.SCHEDULED, + executionData: { filledPercentDisplay: '0' }, + ...overrides, + } as unknown as ParsedOrder +} + +describe('TwapOrderStatus()', () => { + it('shows the "Update fallback handler" reason when the handler is broken and a part is open', () => { + render( + + fallback children + , + ) + + expect(screen.getByText('Update fallback handler')).not.toBeNull() + expect(screen.queryByText('fallback children')).toBeNull() + }) + + it('renders the passed children when the fallback handler is not broken', () => { + render( + + fallback children + , + ) + + expect(screen.queryByText('Update fallback handler')).toBeNull() + expect(screen.getByText('fallback children')).not.toBeNull() + }) +}) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx index 49aeedc2e03..934de82cd9e 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx @@ -9,17 +9,21 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder' // TODO: make CancelledDisplay, FilledDisplay, ExpiredDisplay common import * as styledEl from '../../containers/OrderRow/OrderRow.styled' +import { getIsFallbackHandlerUnfillable } from '../../utils/getIsFallbackHandlerUnfillable' +import { WarningReason } from '../OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants' +import { OrderEstimatedExecutionPrice } from '../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' export interface FillsAtStatusProps { childOrders?: ParsedOrder[] orderStatus: OrderStatus + isFallbackHandlerRequired?: boolean children: ReactNode } // TODO: Break down this large function into smaller functions // TODO: Add proper return type annotation // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function TwapOrderStatus({ childOrders, orderStatus, children }: FillsAtStatusProps) { +export function TwapOrderStatus({ childOrders, orderStatus, isFallbackHandlerRequired, children }: FillsAtStatusProps) { if (!childOrders) return null const areAllChildOrdersCancelled = childOrders.every((order) => order.status === OrderStatus.CANCELLED) @@ -39,6 +43,30 @@ export function TwapOrderStatus({ childOrders, orderStatus, children }: FillsAtS ) } + // An open order is unfillable when it can no longer be executed (e.g. the Safe's ComposableCoW + // fallback handler was reset). Surface it instead of the default "pending execution" display. + const isUnfillable = childOrders.some((childOrder) => + getIsFallbackHandlerUnfillable(childOrder.status, !!isFallbackHandlerRequired), + ) + + if (isUnfillable) { + return ( + <> + + + + + + ) + } + // Third priority: Check for scheduled orders const hasScheduledOrder = childOrders.some((childOrder) => childOrder.status === OrderStatus.SCHEDULED) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx index fd98836a17f..dd697dd002d 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx @@ -10,7 +10,8 @@ import type { ParsedOrder } from 'utils/orderUtils/parseOrder' import * as styledEl from './TwapStatusAndToggle.styled' -import { WarningTooltip } from '../OrdersTable/Row/WarningTooltip/WarningTooltip.pure' +import { getIsFallbackHandlerUnfillable } from '../../utils/getIsFallbackHandlerUnfillable' +import { FallbackHandlerWarningTooltip, WarningTooltip } from '../OrdersTable/Row/WarningTooltip/WarningTooltip.pure' import { OrderStatusBox } from '../OrderStatusBox/OrderStatusBox.pure' import type { OrderParams } from '../../utils/getOrderParams' @@ -24,6 +25,7 @@ interface TwapStatusAndToggleProps { parent: ParsedOrder childrenLength: number isCollapsed: boolean + isFallbackHandlerRequired?: boolean onToggle: () => void onClick: () => void childOrders: ChildOrderItems[] @@ -36,6 +38,7 @@ export function TwapStatusAndToggle({ parent, childrenLength, isCollapsed, + isFallbackHandlerRequired, onToggle, onClick, childOrders, @@ -56,14 +59,22 @@ export function TwapStatusAndToggle({ const warningChild = childWithAllowanceWarning || childWithBalanceWarning + // A reset Safe ComposableCoW fallback handler blocks a still-open order (see issue #5426). This is + // a per-account state (resolved in the view, not persisted onto the order); the parent status + // already reflects whether the TWAP is still open, so checking it is enough — surface the same + // danger design on the parent badge as the Fills-at column and the parts. + const isFallbackHandlerBlocked = getIsFallbackHandlerUnfillable(parent.status, !!isFallbackHandlerRequired) + return ( <> + ) : warningChild ? (