Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ee3d7ff
fix: enhance progress bar logic with completion delays and step staging
fairlighteth Apr 8, 2026
0b7b1e4
fix: refactor order progress bar logic to improve step handling and c…
fairlighteth Apr 8, 2026
f5bcbb9
Merge branch 'develop' into fix/progressbar-states
fairlighteth Apr 8, 2026
ff5695b
fix: add logic to persist bridging step names in order progress bar
fairlighteth Apr 8, 2026
91be91d
fix: update order progress bar logic to restage executing step after …
fairlighteth Apr 8, 2026
fb82f69
fix: add shouldApplyStepNameImmediately function to enhance order pro…
fairlighteth Apr 8, 2026
09a2653
fix: enhance order progress bar logic to track execution attempts and…
fairlighteth Apr 8, 2026
b7b2cdc
feat: orderprogressbar playground
fairlighteth Apr 9, 2026
2829aed
Merge branch 'develop' into fix/progressbar-states
fairlighteth Apr 9, 2026
120c1df
feat: implement debug progress bar route functionality
fairlighteth Apr 9, 2026
a4b04b6
feat: enhance order progress bar functionality with new hooks and tests
fairlighteth Apr 9, 2026
8937fd8
refactor: update OrderProgressBarPlayground styles to use UI variable…
fairlighteth Apr 9, 2026
98c31e7
feat: add tests for FullSizeLottie component and enhance scaling func…
fairlighteth Apr 9, 2026
d685cdc
feat: add tests for cancelling scenarios in OrderProgressBar and enha…
fairlighteth Apr 9, 2026
d571590
feat: add disableAnalytics prop to OrderProgressBar and enhance order…
fairlighteth Apr 9, 2026
03c4eba
fix: bypass 5s min bridge state
fairlighteth Apr 10, 2026
4b82ba7
feat: add bridging status test
fairlighteth Apr 10, 2026
90db86a
fix: bridge progress bar crash
fairlighteth Apr 10, 2026
6107c30
Merge branch 'develop' into fix/progressbar-states
elena-zh Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const Routes = {
FAQ_ETH_FLOW: '/faq/sell-native',
PLAY_COWRUNNER: '/play/cow-runner',
PLAY_MEVSLICER: '/play/mev-slicer',
DEBUG_PROGRESS_BAR: '/debug/progress-bar',
ANYSWAP_AFFECTED: '/anyswap-affected-users',
CHAT: '/chat',
DOCS: '/docs',
Expand Down
5 changes: 5 additions & 0 deletions apps/cowswap-frontend/src/legacy/state/orders/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,18 @@ export interface Order extends BaseOrder {
bridgeOutputAmount?: CurrencyAmount<Currency>
}

export interface SerializedBridgeOutputAmount {
amount: string
}

/**
* Order used for persisting it in the state.
* The only difference with Order is that all it's fields are serializable
*/
export interface SerializedOrder extends BaseOrder {
inputToken: SerializedToken // for dapp use only, readable by user
outputToken: SerializedToken // for dapp use only, readable by user
bridgeOutputAmount?: SerializedBridgeOutputAmount
}
export type SetOrderCancellationHashParams = ChangeOrderStatusParams & { hash: string }

Expand Down
5 changes: 4 additions & 1 deletion apps/cowswap-frontend/src/legacy/state/orders/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UiOrderType } from '@cowprotocol/types'
import { useDispatch, useSelector } from 'react-redux'
import useSWR from 'swr'

import { addPendingOrderStep } from 'modules/trade/utils/addPendingOrderStep'
import { addPendingOrderStep } from 'modules/trade'

import { getUiOrderType } from 'utils/orderUtils/getUiOrderType'

Expand Down Expand Up @@ -305,6 +305,9 @@ export const useAddOrUpdateOrders = (): AddOrUpdateOrdersCallback => {
(params: AddOrUpdateUnserialisedOrdersParams) => {
const orders = params.orders.map((order) => ({
...order,
bridgeOutputAmount: order.bridgeOutputAmount
? { amount: order.bridgeOutputAmount.quotient.toString() }
: undefined,
inputToken: serializeToken(order.inputToken),
outputToken: serializeToken(order.outputToken),
}))
Expand Down
3 changes: 2 additions & 1 deletion apps/cowswap-frontend/src/legacy/state/orders/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UiOrderType } from '@cowprotocol/types'
import BigNumber from 'bignumber.js'
import JSBI from 'jsbi'

import { decodeAppData } from 'modules/appData/utils/decodeAppData'
import { decodeAppData } from 'modules/appData'

import { GenericOrder } from 'common/types'
import { getIsComposableCowParentOrder } from 'utils/orderUtils/getIsComposableCowParentOrder'
Expand Down Expand Up @@ -464,6 +464,7 @@ export function partialOrderUpdate({ chainId, order, isSafeWallet }: UpdateOrder
chainId,
order: {
...order,
...(order.bridgeOutputAmount && { bridgeOutputAmount: { amount: order.bridgeOutputAmount.quotient.toString() } }),
...(order.inputToken && { inputToken: serializeToken(order.inputToken) }),
...(order.outputToken && { outputToken: serializeToken(order.outputToken) }),
} as UpdateOrderParamsAction['order'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { USDC_MAINNET as USDC, USDT } from '@cowprotocol/common-const'
import { getCurrencyAddress } from '@cowprotocol/common-utils'
import { CurrencyAmount } from '@cowprotocol/currency'

import { deserializeOrder } from './deserializeOrder'

import { serializeToken } from '../../user/hooks'
import { SerializedOrder } from '../actions'
import { generateOrder } from '../mocks'
import { OrderObject } from '../reducer'

const CHAIN_ID = USDT.chainId
const OWNER = '0x0000000000000000000000000000000000000001'

function createSerializedOrder(overrides?: Partial<SerializedOrder>): OrderObject {
const order = generateOrder({ owner: OWNER, sellToken: USDT, buyToken: USDC })
const { bridgeOutputAmount: _bridgeOutputAmount, ...serializableOrder } = order

return {
id: order.id,
isSafeWallet: false,
order: {
...serializableOrder,
inputToken: serializeToken(order.inputToken),
outputToken: serializeToken(order.outputToken),
...overrides,
},
}
}

describe('deserializeOrder', () => {
it('rehydrates serialized bridge output amounts', () => {
const bridgeOutputAmount = CurrencyAmount.fromRawAmount(USDC, '1234500')
const serializedOrder = createSerializedOrder({
bridgeOutputAmount: { amount: bridgeOutputAmount.quotient.toString() },
})

const result = deserializeOrder(serializedOrder)
const resultBridgeOutputAmount = result?.bridgeOutputAmount

expect(resultBridgeOutputAmount?.quotient.toString()).toBe('1234500')
expect(resultBridgeOutputAmount?.currency.chainId).toBe(CHAIN_ID)

if (!resultBridgeOutputAmount) {
throw new Error('Expected bridge output amount to be rehydrated')
}

expect(getCurrencyAddress(resultBridgeOutputAmount.currency)).toBe(USDC.address)
})

it('drops malformed cached bridge output amounts instead of crashing', () => {
const serializedOrder = createSerializedOrder({
bridgeOutputAmount: { numerator: { bad: true } } as unknown as SerializedOrder['bridgeOutputAmount'],
})

const result = deserializeOrder(serializedOrder)

expect(result?.bridgeOutputAmount).toBeUndefined()
expect(result?.outputToken.address).toBe(USDC.address)
})
})
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { TokenWithLogo } from '@cowprotocol/common-const'
import { Currency, CurrencyAmount } from '@cowprotocol/currency'

import { SerializedToken } from '../../user/types'
import { Order, OrderStatus } from '../actions'
import { Order, OrderStatus, SerializedBridgeOutputAmount } from '../actions'
import { OrderObject, V2OrderObject } from '../reducer'
import { isOrderExpired } from '../utils'

// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function deserializeOrder(orderObject: OrderObject | V2OrderObject | undefined) {
export function deserializeOrder(orderObject: OrderObject | V2OrderObject | undefined): Order | undefined {
let order: Order | undefined
// we need to make sure the incoming order is a valid
// V3 typed order as users can have stale data from V2
Expand All @@ -16,8 +15,14 @@ export function deserializeOrder(orderObject: OrderObject | V2OrderObject | unde

const deserialisedInputToken = deserializeToken(serialisedOrder.inputToken)
const deserialisedOutputToken = deserializeToken(serialisedOrder.outputToken)
const deserialisedBridgeOutputAmount = deserializeBridgeOutputAmount(
serialisedOrder.bridgeOutputAmount,
deserialisedOutputToken,
)

order = {
...serialisedOrder,
bridgeOutputAmount: deserialisedBridgeOutputAmount,
inputToken: deserialisedInputToken,
outputToken: deserialisedOutputToken,
}
Expand All @@ -38,6 +43,30 @@ function deserializeToken(serializedToken: SerializedToken): TokenWithLogo {
return TokenWithLogo.fromToken(serializedToken, serializedToken.logoURI)
}

function deserializeBridgeOutputAmount(
serializedAmount: SerializedBridgeOutputAmount | unknown,
outputToken: TokenWithLogo,
): CurrencyAmount<Currency> | undefined {
if (typeof serializedAmount === 'string') {
return CurrencyAmount.fromRawAmount(outputToken, serializedAmount)
}

if (isSerializedBridgeOutputAmount(serializedAmount)) {
return CurrencyAmount.fromRawAmount(outputToken, serializedAmount.amount)
}

return undefined
}

function isSerializedBridgeOutputAmount(value: unknown): value is SerializedBridgeOutputAmount {
return (
typeof value === 'object' &&
value !== null &&
'amount' in value &&
typeof (value as { amount: unknown }).amount === 'string'
)
}

// TODO: Replace any with proper type definitions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isV3Order(orderObject: any): orderObject is OrderObject {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@cowprotocol/common-const'
import { useFeatureFlags } from '@cowprotocol/common-hooks'

import { Navigate, Route, Routes } from 'react-router'
import { Navigate, Route, Routes, useLocation } from 'react-router'

import { Loading } from 'legacy/components/FlashingLoading'
import { RedirectPathToSwapOnly, RedirectToPath } from 'legacy/pages/Swap/redirects'
Expand All @@ -33,10 +33,17 @@ import { LimitOrdersPage } from 'pages/LimitOrders/LimitOrders.page'
import { SwapPage } from 'pages/Swap'
import YieldPage from 'pages/Yield'

import { isDebugProgressBarRouteEnabled } from './RoutesApp.utils'

// Async routes
const NotFound = lazy(() => import(/* webpackChunkName: "not_found" */ 'pages/error/NotFound'))
const CowRunner = lazy(() => import(/* webpackChunkName: "cow_runner" */ 'pages/games/CowRunner'))
const MevSlicer = lazy(() => import(/* webpackChunkName: "mev_slicer" */ 'pages/games/MevSlicer'))
const OrderProgressBarPlaygroundRoute = lazy(() =>
import(/* webpackChunkName: "order_progress_bar_playground" */ 'pages/debug/OrderProgressBarPlayground.page').then(
({ OrderProgressBarPlaygroundPage }) => ({ default: OrderProgressBarPlaygroundPage }),
),
)

// External routes
const LegalExternal = <ExternalRedirect url={COWDAO_LEGAL_LINK} />
Expand Down Expand Up @@ -83,6 +90,8 @@ const lazyRoutes: LazyRouteProps[] = [

export function RoutesApp(): ReactNode {
const { isAffiliateProgramEnabled } = useFeatureFlags()
const { search } = useLocation()
const isDebugProgressBarEnabled = isDebugProgressBarRouteEnabled(search, process.env.NODE_ENV)

return (
<Routes>
Expand Down Expand Up @@ -114,6 +123,16 @@ export function RoutesApp(): ReactNode {
<Route path={RoutesEnum.ADVANCED_ORDERS} element={<AdvancedOrdersPage />} />
<Route path={RoutesEnum.HOOKS} element={<HooksPage />} />
<Route path={RoutesEnum.SEND} element={<RedirectPathToSwapOnly />} />
{isDebugProgressBarEnabled && (
<Route
path={RoutesEnum.DEBUG_PROGRESS_BAR}
element={
<Suspense fallback={<Loading />}>
<OrderProgressBarPlaygroundRoute />
</Suspense>
}
/>
)}

{lazyRoutes.map((item, key) => LazyRoute({ ...item, key }))}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { DEBUG_PROGRESS_BAR_QUERY_FLAG, isDebugProgressBarRouteEnabled } from './RoutesApp.utils'

describe('isDebugProgressBarRouteEnabled', () => {
it('always enables the route in development', () => {
expect(isDebugProgressBarRouteEnabled('', 'development')).toBe(true)
expect(isDebugProgressBarRouteEnabled(`?${DEBUG_PROGRESS_BAR_QUERY_FLAG}=0`, 'development')).toBe(true)
})

it('enables the route in non-development only when the query flag is set', () => {
expect(isDebugProgressBarRouteEnabled(`?${DEBUG_PROGRESS_BAR_QUERY_FLAG}=1`, 'preview')).toBe(true)
expect(isDebugProgressBarRouteEnabled(`?${DEBUG_PROGRESS_BAR_QUERY_FLAG}=true`, 'preview')).toBe(false)
expect(isDebugProgressBarRouteEnabled('', 'preview')).toBe(false)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const DEBUG_PROGRESS_BAR_QUERY_FLAG = 'debugProgressBar'

export function isDebugProgressBarRouteEnabled(search: string, environment: string | undefined): boolean {
if (environment === 'development') return true

const searchParams = new URLSearchParams(search)

return searchParams.get(DEBUG_PROGRESS_BAR_QUERY_FLAG) === '1'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getProgressBarStepName } from './useOrderProgressBarProps'

import { OrderProgressBarStepName } from '../constants'
import { OrderProgressBarState } from '../types'

const OPEN_STATUS = 'open' as OrderProgressBarState['backendApiStatus']

describe('getProgressBarStepName cancelling regressions', () => {
it('prioritizes cancelling over the price change step for open unfillable orders', () => {
const result = getProgressBarStepName(
true, // isUnfillable
false, // isCancelled
false, // isExpired
true, // isCancelling
undefined, // cancellationTriggered
false, // isConfirmed
null, // countdown
OrderProgressBarStepName.UNFILLABLE,
OPEN_STATUS,
OPEN_STATUS,
OrderProgressBarStepName.SOLVING,
undefined,
undefined,
false,
)

expect(result).toBe(OrderProgressBarStepName.CANCELLING)
})
})
Loading
Loading