diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/hooks/useOrderProgressBarProps.ts b/apps/cowswap-frontend/src/modules/orderProgressBar/hooks/useOrderProgressBarProps.ts index 5e83293f30..d87cca7872 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/hooks/useOrderProgressBarProps.ts +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/hooks/useOrderProgressBarProps.ts @@ -133,7 +133,7 @@ function useOrderBaseProgressBarProps(params: UseOrderProgressBarPropsParams): U // Do not build progress bar data when these conditions are set const disableProgressBar = widgetDisabled || isCreating || isFailed || isPresignaturePending || featureFlagDisabled - const orderId = order?.id || '' + const orderId = order?.id const getCancelOrder = useCancelOrder() const showCancellationModal = order && getCancelOrder ? getCancelOrder(order) : null @@ -254,9 +254,9 @@ function getDoNotQueryStatusEndpoint( const DEFAULT_STATE = {} -function useGetExecutingOrderState(orderId: string): OrderProgressBarState { +function useGetExecutingOrderState(orderId?: string): OrderProgressBarState { const fullState = useAtomValue(ordersProgressBarStateAtom) - const singleState = fullState[orderId] + const singleState = orderId ? fullState[orderId] : undefined return useMemo(() => singleState || DEFAULT_STATE, [singleState]) } @@ -281,7 +281,7 @@ function useSetExecutingOrderProgressBarStepNameCallback(): (orderId: string, va // local updaters function useCountdownStartUpdater( - orderId: string, + orderId: string | undefined, countdown: OrderProgressBarState['countdown'], backendApiStatus: OrderProgressBarState['backendApiStatus'], shouldDisableCountdown: boolean, @@ -289,6 +289,10 @@ function useCountdownStartUpdater( const setCountdown = useSetExecutingOrderCountdownCallback() useEffect(() => { + if (!orderId) { + return + } + if (shouldDisableCountdown) { // Loose `!= null` on purpose: both null and undefined should reset the countdown, but 0 must stay; strict `!== null` would let undefined slip through if (countdown != null) { @@ -308,17 +312,21 @@ function useCountdownStartUpdater( }, [backendApiStatus, setCountdown, countdown, orderId, shouldDisableCountdown]) } -function useCancellingOrderUpdater(orderId: string, isCancelling: boolean): void { +function useCancellingOrderUpdater(orderId: string | undefined, isCancelling: boolean): void { const setCancellationTriggered = useSetAtom(setOrderProgressBarCancellationTriggered) useEffect(() => { - if (isCancelling) setCancellationTriggered(orderId) + if (!orderId || !isCancelling) { + return + } + + setCancellationTriggered(orderId) }, [orderId, isCancelling, setCancellationTriggered]) } // TODO: Break down this large function into smaller functions function useProgressBarStepNameUpdater( - orderId: string, + orderId: string | undefined, isUnfillable: boolean, isCancelled: boolean, isExpired: boolean, @@ -352,8 +360,14 @@ function useProgressBarStepNameUpdater( // Update state with new step name useEffect(() => { + if (!orderId) { + return + } + + const ensuredOrderId = orderId + function updateStepName(name: OrderProgressBarStepName): void { - setProgressBarStepName(orderId, name || DEFAULT_STEP_NAME) + setProgressBarStepName(ensuredOrderId, name || DEFAULT_STEP_NAME) } let timer: NodeJS.Timeout | undefined @@ -487,7 +501,11 @@ const BACKEND_TYPE_TO_PROGRESS_BAR_STEP_NAME: Record { expect(store.get(ordersProgressBarStateAtom)).toBe(initialState) }) + }) describe('updateOrderProgressBarCountdown', () => { @@ -124,3 +126,16 @@ describe('updateOrderProgressBarCountdown', () => { expect(store.get(ordersProgressBarStateAtom)).toEqual({}) }) }) + +describe('cancellationTrackedOrderIdsAtom', () => { + it('returns ids with cancellationTriggered flag set', () => { + const store = createStore() + store.set(ordersProgressBarStateAtom, { + a: { cancellationTriggered: true }, + b: {}, + c: { cancellationTriggered: true }, + }) + + expect(store.get(cancellationTrackedOrderIdsAtom)).toEqual(['a', 'c']) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/state/atoms.ts b/apps/cowswap-frontend/src/modules/orderProgressBar/state/atoms.ts index 3e58de5551..8c279872e8 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/state/atoms.ts +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/state/atoms.ts @@ -32,7 +32,7 @@ export const pruneOrdersProgressBarState = atom(null, (get, set, trackedOrderIds return acc }, {}) - if (!changed && trackedOrderIds.every((orderId) => orderId in fullState)) { + if (!changed) { return } @@ -213,3 +213,11 @@ export const setOrderProgressBarCancellationTriggered = atom(null, (get, set, or set(ordersProgressBarStateAtom, { ...fullState, [orderId]: singleState }) }) + +export const cancellationTrackedOrderIdsAtom = atom((get) => { + const fullState = get(ordersProgressBarStateAtom) + + return Object.entries(fullState) + .filter(([, state]) => state?.cancellationTriggered) + .map(([orderId]) => orderId) +}) diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.test.tsx b/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.test.tsx index 90c8ca04da..11c122002d 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.test.tsx +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.test.tsx @@ -36,6 +36,7 @@ jest.mock('modules/trade', () => ({ })) const mockPruneOrders = jest.fn() +const mockCancellationIds = jest.fn() jest.mock('jotai', () => { const actual = jest.requireActual('jotai') @@ -51,6 +52,15 @@ jest.mock('jotai', () => { return actual.useSetAtom(atom) }), + useAtomValue: jest.fn((atom: PrimitiveAtom) => { + const { cancellationTrackedOrderIdsAtom } = jest.requireActual('../state/atoms') + + if (atom === cancellationTrackedOrderIdsAtom) { + return mockCancellationIds() + } + + return actual.useAtomValue(atom) + }), } }) @@ -72,6 +82,7 @@ describe('OrderProgressStateUpdater', () => { }) useSurplusQueueOrderIdsMock.mockReturnValue([]) useTradeConfirmStateMock.mockReturnValue({ transactionHash: null } as never) + mockCancellationIds.mockReturnValue([]) }) afterEach(() => { @@ -154,4 +165,17 @@ describe('OrderProgressStateUpdater', () => { expect(mockPruneOrders).toHaveBeenLastCalledWith(['1', '2', '3']) }) + + it('keeps cancellation-triggered orders even if they are not pending anymore', () => { + useWalletInfoMock.mockReturnValue({ + chainId: undefined, + account: undefined, + } as unknown as WalletInfo) + useOnlyPendingOrdersMock.mockReturnValue([]) + mockCancellationIds.mockReturnValue(['abc']) + + render() + + expect(mockPruneOrders).toHaveBeenLastCalledWith(['abc']) + }) }) diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.tsx b/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.tsx index dc1e55d402..1498667b7f 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/updaters/OrderProgressStateUpdater.tsx @@ -1,4 +1,4 @@ -import { useSetAtom } from 'jotai' +import { useAtomValue, useSetAtom } from 'jotai' import { ReactNode, useEffect, useMemo } from 'react' import { OrderClass, SupportedChainId } from '@cowprotocol/cow-sdk' @@ -12,7 +12,7 @@ import { useOnlyPendingOrders } from 'legacy/state/orders/hooks' import { useTradeConfirmState } from 'modules/trade' import { useOrderProgressBarProps } from '../hooks/useOrderProgressBarProps' -import { pruneOrdersProgressBarState } from '../state/atoms' +import { cancellationTrackedOrderIdsAtom, pruneOrdersProgressBarState } from '../state/atoms' function OrderProgressStateObserver({ chainId, order }: { chainId: SupportedChainId; order: Order }): null { useOrderProgressBarProps(chainId, order) @@ -24,6 +24,7 @@ export function OrderProgressStateUpdater(): ReactNode { const pruneProgressState = useSetAtom(pruneOrdersProgressBarState) const { transactionHash } = useTradeConfirmState() const surplusQueueOrderIds = useSurplusQueueOrderIds() + const cancellationTrackedOrderIds = useAtomValue(cancellationTrackedOrderIdsAtom) const pendingOrders = useOnlyPendingOrders(chainId as SupportedChainId, account) const marketOrders = useMemo( @@ -46,8 +47,18 @@ export function OrderProgressStateUpdater(): ReactNode { trackedIdsSet.add(transactionHash) } + cancellationTrackedOrderIds.forEach((orderId) => trackedIdsSet.add(orderId)) + pruneProgressState(Array.from(trackedIdsSet)) - }, [account, chainId, marketOrders, pruneProgressState, surplusQueueOrderIds, transactionHash]) + }, [ + account, + cancellationTrackedOrderIds, + chainId, + marketOrders, + pruneProgressState, + surplusQueueOrderIds, + transactionHash, + ]) if (!chainId || !account) { return null