diff --git a/apps/cowswap-frontend/public/audio/receipt-printer.wav b/apps/cowswap-frontend/public/audio/receipt-printer.wav new file mode 100644 index 00000000000..525e0acbd3d Binary files /dev/null and b/apps/cowswap-frontend/public/audio/receipt-printer.wav differ diff --git a/apps/cowswap-frontend/public/static/StudioFeixenMono-Bold.woff2 b/apps/cowswap-frontend/public/static/StudioFeixenMono-Bold.woff2 new file mode 100644 index 00000000000..2431af65dcc Binary files /dev/null and b/apps/cowswap-frontend/public/static/StudioFeixenMono-Bold.woff2 differ diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index 73e30ecc6cd..e4dd2088f34 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -35,6 +35,7 @@ import { Updaters, WithLDProvider, } from 'modules/application' +import { setupCowSoundUnlock } from 'modules/sounds' import { hashHistory } from 'common/constants/routes' import { loadActiveLocaleMessages } from 'lib/localeMessages' @@ -61,6 +62,7 @@ function HydrateQueryClient({ children }: { children: ReactNode }): ReactNode { // Node removeChild hackaround // based on: https://github.com/facebook/react/issues/11538#issuecomment-417504600 nodeRemoveChildFix() +setupCowSoundUnlock() // Disable MetaMask network auto-refresh; ignore when window.ethereum is read-only (e.g. another extension set it with a getter). try { diff --git a/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.test.ts b/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.test.ts index 0693eaf3f70..86a8febe6e8 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.test.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.test.ts @@ -1,7 +1,12 @@ +import { UiOrderType } from '@cowprotocol/types' + import { AnyAction, Dispatch, MiddlewareAPI } from 'redux' import { instance, mock, resetCalls, when } from 'ts-mockito' -import { getCowSoundError, getCowSoundSend, getCowSoundSuccess } from 'modules/sounds' +import { getCowSoundError, getCowSoundReceiptBundle, getCowSoundSend, getCowSoundSuccess } from 'modules/sounds' + +import { getIsBridgeOrder } from 'common/utils/getIsBridgeOrder' +import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' import { soundMiddleware } from './soundMiddleware' @@ -12,6 +17,17 @@ const nextMock = jest.fn() const actionMock = mock() jest.mock('modules/sounds') +jest.mock('common/utils/getIsBridgeOrder') +jest.mock('utils/orderUtils/getUiOrderType') + +const getIsBridgeOrderMock = jest.mocked(getIsBridgeOrder) +const getUiOrderTypeMock = jest.mocked(getUiOrderType) +const receiptPlayMock = jest.fn().mockResolvedValue(undefined) +const successPlayMock = jest.fn().mockResolvedValue(undefined) +const otherPlayMock = jest.fn().mockResolvedValue(undefined) +const receiptSoundMock = { currentTime: 1, play: receiptPlayMock } as unknown as HTMLAudioElement +const successSoundMock = { currentTime: 1, play: successPlayMock } as unknown as HTMLAudioElement +const otherSoundMock = { currentTime: 1, play: otherPlayMock } as unknown as HTMLAudioElement // TODO: Break down this large function into smaller functions @@ -27,6 +43,15 @@ describe('soundMiddleware', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any) jest.clearAllMocks() + getIsBridgeOrderMock.mockReturnValue(false) + getUiOrderTypeMock.mockReturnValue(UiOrderType.SWAP) + receiptSoundMock.currentTime = 1 + successSoundMock.currentTime = 1 + otherSoundMock.currentTime = 1 + jest.mocked(getCowSoundReceiptBundle).mockReturnValue([successSoundMock, receiptSoundMock]) + jest.mocked(getCowSoundError).mockReturnValue(otherSoundMock) + jest.mocked(getCowSoundSend).mockReturnValue(otherSoundMock) + jest.mocked(getCowSoundSuccess).mockReturnValue(successSoundMock) }) describe('batch order action', () => { @@ -92,16 +117,47 @@ describe('soundMiddleware', () => { }) }) describe('fulfill order action', () => { - it('should play a sound', () => { - when(actionMock.payload).thenReturn({ chainId: 1, orders: ['some data'] }) + it('layers the success and receipt sounds for a fulfilled, non-bridge swap', () => { + when(actionMock.payload).thenReturn({ chainId: 1, orders: [{}] }) when(actionMock.type).thenReturn('order/fullfillOrdersBatch') soundMiddleware(instance(mockStore))(nextMock)(instance(actionMock)) - expect(getCowSoundSuccess).toHaveBeenCalledTimes(1) + expect(getCowSoundReceiptBundle).toHaveBeenCalledTimes(1) + expect(getCowSoundSuccess).toHaveBeenCalledTimes(0) + expect(receiptSoundMock.currentTime).toBe(0) + expect(successSoundMock.currentTime).toBe(0) + expect(receiptPlayMock).toHaveBeenCalledTimes(1) + expect(successPlayMock).toHaveBeenCalledTimes(1) expect(getCowSoundError).toHaveBeenCalledTimes(0) expect(getCowSoundSend).toHaveBeenCalledTimes(0) }) + + it('keeps the existing success sound for a bridge swap leg', () => { + getIsBridgeOrderMock.mockReturnValue(true) + when(actionMock.payload).thenReturn({ chainId: 1, orders: [{}] }) + when(actionMock.type).thenReturn('order/fullfillOrdersBatch') + + soundMiddleware(instance(mockStore))(nextMock)(instance(actionMock)) + + expect(getCowSoundReceiptBundle).toHaveBeenCalledTimes(0) + expect(getCowSoundSuccess).toHaveBeenCalledTimes(1) + expect(receiptPlayMock).toHaveBeenCalledTimes(0) + expect(successPlayMock).toHaveBeenCalledTimes(1) + }) + + it('keeps the existing success sound for non-swap orders', () => { + getUiOrderTypeMock.mockReturnValue(UiOrderType.LIMIT) + when(actionMock.payload).thenReturn({ chainId: 1, orders: [{}] }) + when(actionMock.type).thenReturn('order/fullfillOrdersBatch') + + soundMiddleware(instance(mockStore))(nextMock)(instance(actionMock)) + + expect(getCowSoundReceiptBundle).toHaveBeenCalledTimes(0) + expect(getCowSoundSuccess).toHaveBeenCalledTimes(1) + expect(receiptPlayMock).toHaveBeenCalledTimes(0) + expect(successPlayMock).toHaveBeenCalledTimes(1) + }) }) describe('batch expire order action', () => { it('should play a sound when order is not hidden', () => { diff --git a/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.ts b/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.ts index 892753e8070..fb539aaebde 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/middleware/soundMiddleware.ts @@ -1,12 +1,17 @@ // On each Pending, Expired, Fulfilled order action a corresponding sound is dispatched +import { UiOrderType } from '@cowprotocol/types' + import { isAnyOf } from '@reduxjs/toolkit' import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from 'redux' -import { getCowSoundError, getCowSoundSend, getCowSoundSuccess } from 'modules/sounds' +import { getCowSoundError, getCowSoundReceiptBundle, getCowSoundSend, getCowSoundSuccess } from 'modules/sounds' + +import { getIsBridgeOrder } from 'common/utils/getIsBridgeOrder' +import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' import { AppState } from '../../index' -import { AddPendingOrderParams, BatchOrdersUpdateParams, UpdateOrderParams } from '../actions' +import { AddPendingOrderParams, BatchOrdersUpdateParams, FulfillOrdersBatchParams, UpdateOrderParams } from '../actions' import * as OrderActions from '../actions' import { getOrderByIdFromState } from '../helpers' import { OrdersState } from '../reducer' @@ -25,7 +30,6 @@ const isBatchFulfillOrderAction = isAnyOf(OrderActions.fulfillOrdersBatch) const isBatchExpireOrderAction = isAnyOf(OrderActions.expireOrdersBatch) const isBatchCancelOrderAction = isAnyOf(OrderActions.cancelOrdersBatch) // const isBatchPresignOrders = isAnyOf(OrderActions.preSignOrders) -const isFulfillOrderAction = isAnyOf(OrderActions.addPendingOrder, OrderActions.fulfillOrdersBatch) // TODO: Reduce function complexity by extracting logic // eslint-disable-next-line complexity @@ -50,28 +54,30 @@ export const soundMiddleware: Middleware, AppState> = (s } } - let cowSound + let cowSounds: HTMLAudioElement[] = [] if (isPendingOrderAction(action)) { if (_shouldPlayPendingOrderSound(action.payload)) { - cowSound = getCowSoundSend() + cowSounds = [getCowSoundSend()] } - } else if (isFulfillOrderAction(action)) { - cowSound = getCowSoundSuccess() + } else if (isBatchFulfillOrderAction(action)) { + cowSounds = _shouldPlayReceiptSound(action.payload.orders) ? getCowSoundReceiptBundle() : [getCowSoundSuccess()] } else if (isBatchExpireOrderAction(action)) { if (_shouldPlayExpiredOrderSound(action.payload, store)) { - cowSound = getCowSoundError() + cowSounds = [getCowSoundError()] } } else if (isBatchCancelOrderAction(action)) { - cowSound = getCowSoundError() + cowSounds = [getCowSoundError()] } else if (isUpdateOrderAction(action)) { - cowSound = _getUpdatedOrderSound(action.payload) + const cowSound = _getUpdatedOrderSound(action.payload) + cowSounds = cowSound ? [cowSound] : [] } - if (cowSound) { + cowSounds.forEach((cowSound) => { + cowSound.currentTime = 0 cowSound.play().catch((e) => { console.error('🐮 Moooooo sound cannot be played', e) }) - } + }) return result } @@ -104,3 +110,7 @@ function _shouldPlayPendingOrderSound(payload: AddPendingOrderParams): boolean { // Only play COW sound if added pending order is not hidden return !payload.order.isHidden } + +function _shouldPlayReceiptSound(orders: FulfillOrdersBatchParams['orders']): boolean { + return orders.some((order) => getUiOrderType(order) === UiOrderType.SWAP && !getIsBridgeOrder(order)) +} diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index a87fd609daa..f1c5359245d 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -451,6 +451,10 @@ msgstr "<0>By adding this list you are implicitly trusting that the data is corr #~ msgid "As soon as anything important or interesting happens, we will definitely let you know." #~ msgstr "As soon as anything important or interesting happens, we will definitely let you know." +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Thanks for swapping" +msgstr "Thanks for swapping" + #: apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/SwapPriceDifferenceWarning.tsx msgid "SWAP order" msgstr "SWAP order" @@ -924,6 +928,7 @@ msgstr "Unsupported Token" #~ msgid "Limit price (incl. costs)" #~ msgstr "Limit price (incl. costs)" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/ContextMenu/OrderContextMenu.pure.tsx msgid "Order receipt" msgstr "Order receipt" @@ -1217,6 +1222,10 @@ msgstr "Available COW balance" msgid "Bridge explorer" msgstr "Bridge explorer" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Completed swap receipt" +msgstr "Completed swap receipt" + #: apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hooks/useClaimData.ts msgid "You possibly have other items to claim, but not Airdrops" msgstr "You possibly have other items to claim, but not Airdrops" @@ -2672,6 +2681,10 @@ msgstr "(via WalletConnect)" msgid "<0>Select an {accountProxyLabelString} and then select a token you want to recover from CoW Shed." msgstr "<0>Select an {accountProxyLabelString} and then select a token you want to recover from CoW Shed." +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Settlement" +msgstr "Settlement" + #: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts msgid "Error loading price. Try again later." msgstr "Error loading price. Try again later." @@ -3528,9 +3541,9 @@ msgstr "Learn how" msgid "Account overview" msgstr "Account overview" -#: apps/cowswap-frontend/src/modules/affiliate/pure/AffiliateTraderActivityTable.tsx -#~ msgid "Network" -#~ msgstr "Network" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Network" +msgstr "Network" #: apps/cowswap-frontend/src/pages/Account/Delegate.tsx msgid "Delegate your" @@ -3978,6 +3991,7 @@ msgid "Safe confirmed signatures" msgstr "Safe confirmed signatures" #: apps/cowswap-frontend/src/modules/bridge/pure/contents/SwapResultContent/contents.tsx +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx #: apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx msgid "Winning solver" msgstr "Winning solver" @@ -4330,6 +4344,7 @@ msgid "Token is temporarily suspended from trading" msgstr "Token is temporarily suspended from trading" #: apps/cowswap-frontend/src/legacy/state/orders/helpers.tsx +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx #: apps/cowswap-frontend/src/modules/orderProgressBar/pure/TransactionSubmittedContent/index.tsx #: apps/cowswap-frontend/src/modules/orders/pure/OrderNotificationContent/index.tsx msgid "Order" @@ -4854,8 +4869,8 @@ msgstr "Order Receipt" #~ msgstr "The swap adapter contracts integrate Aave Flash Loans and CoW Swap to facilitate advanced actions like swapping debt assets. Learn more at https://aave.com/docs/developers/smart-contracts/swap-features." #: apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx -msgid "Transaction completed!" -msgstr "Transaction completed!" +#~ msgid "Transaction completed!" +#~ msgstr "Transaction completed!" #: apps/cowswap-frontend/src/legacy/components/Header/NotificationAlertPopover/NotificationAlertPopover.pure.tsx msgid "When orders fill or expire" @@ -5184,6 +5199,10 @@ msgstr "Current network costs make up <0><1>{formattedFeePercentage}% of msgid "Switch network" msgstr "Switch network" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +#~ msgid "Swap complete" +#~ msgstr "Swap complete" + #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx @@ -5673,6 +5692,10 @@ msgstr "Version" msgid "All tokens" msgstr "All tokens" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Received by" +msgstr "Received by" + #: apps/cowswap-frontend/src/common/utils/tradeSettingsTooltips.tsx msgid "When selling {aNativeCurrency}, the minimum slippage tolerance is set to {minimumETHFlowSlippage}% or higher{amountRange} to ensure a high likelihood of order matching, even in volatile market conditions." msgstr "When selling {aNativeCurrency}, the minimum slippage tolerance is set to {minimumETHFlowSlippage}% or higher{amountRange} to ensure a high likelihood of order matching, even in volatile market conditions." @@ -5955,6 +5978,10 @@ msgstr "Links/codes don't reveal your wallet." msgid "Save & lock code" msgstr "Save & lock code" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Price improvement" +msgstr "Price improvement" + #: apps/cowswap-frontend/src/modules/twap/utils/buildEoaTwapConfirmationPendingSteps.tsx msgid "Sign in your wallet. We'll submit the funding order automatically." msgstr "Sign in your wallet. We'll submit the funding order automatically." @@ -6435,6 +6462,7 @@ msgstr "Enter valid list location" msgid "View" msgstr "View" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Header/ordersTableHeader.constants.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/ReceiptModal.modal.tsx msgid "Execution price" @@ -6521,8 +6549,8 @@ msgid "Refreshing quote..." msgstr "Refreshing quote..." #: apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx -msgid "was sent to" -msgstr "was sent to" +#~ msgid "was sent to" +#~ msgstr "was sent to" #: apps/cowswap-frontend/src/modules/yield/containers/TradeButtons/index.tsx msgid "Deposit" @@ -7198,6 +7226,10 @@ msgstr "Try Again" #~ msgid "Unwrap {inputAmountStr} {wrapped} to {receiveAmountStr} {native}" #~ msgstr "Unwrap {inputAmountStr} {wrapped} to {receiveAmountStr} {native}" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "You sold" +msgstr "You sold" + #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx msgid "The order remains open. Execution requires adequate allowance for" msgstr "The order remains open. Execution requires adequate allowance for" @@ -7857,6 +7889,10 @@ msgstr "Review hook request" msgid "Invalid parameters" msgstr "Invalid parameters" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "You received" +msgstr "You received" + #: apps/cowswap-frontend/src/modules/trade/pure/PartnerFeeRow/index.tsx msgid "{label}" msgstr "{label}" @@ -8065,6 +8101,10 @@ msgstr "This requires connecting a different wallet." #~ msgid "Waiting for settlement" #~ msgstr "Waiting for settlement" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Trade succeeded" +msgstr "Trade succeeded" + #: apps/cowswap-frontend/src/modules/accountProxy/containers/AccountProxyPage/AccountProxyPage.container.tsx #: apps/cowswap-frontend/src/modules/accountProxy/containers/AccountProxyPage/AccountProxyPage.container.tsx msgid "Invalid proxy address" @@ -8084,8 +8124,8 @@ msgstr "Exec. price" #~ msgstr "This wallet has already traded on CoW Swap. <0/> Referral rewards are for new wallets only." #: apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx -msgid "You sold <0/>" -msgstr "You sold <0/>" +#~ msgid "You sold <0/>" +#~ msgstr "You sold <0/>" #: apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/TwapTradeConfirmationDetails.tsx msgid "Order details" @@ -8352,6 +8392,10 @@ msgstr "yield over average" msgid "Uh oh! The market price has moved outside of your slippage tolerance. You can wait for prices to change{cancellationModal}" msgstr "Uh oh! The market price has moved outside of your slippage tolerance. You can wait for prices to change{cancellationModal}" +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx +msgid "Time to fill" +msgstr "Time to fill" + #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/LoadMore/Section/LoadMoreOrdersSection.tsx msgid "Found {totalOpenOrders} open orders in the {limit} most recent ones." msgstr "Found {totalOpenOrders} open orders in the {limit} most recent ones." @@ -8381,6 +8425,7 @@ msgid "Add token" msgstr "Add token" #: apps/cowswap-frontend/src/modules/account/containers/Transaction/StatusDetails.tsx +#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Header/ordersTableHeader.constants.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/getOrderStatusTitleAndColor.ts #: apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/ReceiptModal.modal.tsx diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx index 911a611d752..bbe647c0d81 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx @@ -17,9 +17,10 @@ import { UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' -import { Order } from 'legacy/state/orders/actions' +import { Order, OrderStatus } from 'legacy/state/orders/actions' import { SwapAndBridgeContext, SwapAndBridgeStatus } from 'modules/bridge' +import { getCowSoundReceiptBundle } from 'modules/sounds' import { getOrderMock } from '../../../../mocks/orderMock' import { inputCurrencyInfoMock } from '../../../../mocks/tradeStateMock' @@ -30,7 +31,14 @@ import { OrderProgressBar } from './index' const order = { ...getOrderMock(SupportedChainId.MAINNET), - apiAdditionalInfo: { executedBuyAmount: '1000000000000000000000', executedSellAmount: '5000000000000000000' }, + status: OrderStatus.FULFILLED, + creationTime: '2026-08-19T12:59:50.000Z', + fulfillmentTime: '2026-08-19T13:00:00.000Z', + apiAdditionalInfo: { + executedBuyAmount: '1000000000', + executedSellAmount: '5000000000000000000', + executedSellAmountBeforeFees: '5000000000000000000', + }, } as Order const receiveAmountInfo = inputCurrencyInfoMock.receiveAmountInfo! @@ -115,7 +123,7 @@ const defaultProps: OrderProgressBarProps = { totalSolvers: 52, surplusData: { surplusFiatValue: CurrencyAmount.fromRawAmount(USDC_GNOSIS_CHAIN, '10000000'), - surplusAmount: CurrencyAmount.fromRawAmount(order.outputToken, '1000000000000000000'), + surplusAmount: CurrencyAmount.fromRawAmount(order.outputToken, '10000000'), surplusToken: order.outputToken, surplusPercent: '10', showFiatValue: true, @@ -126,17 +134,17 @@ const defaultProps: OrderProgressBarProps = { } const Wrapper = styled.div` - width: 560px; + width: min(560px, 100%); margin: 0 auto; background: var(${UI.COLOR_PAPER}); ` const NarrowWrapper = styled(Wrapper)` - width: 375px; + width: min(375px, 100%); ` const WideWrapper = styled(Wrapper)` - width: 720px; + width: min(720px, 100%); ` const cloneTokenWithSymbol = (token: Token | TokenWithLogo, symbol: string): TokenWithLogo => { @@ -158,6 +166,21 @@ const orderWithLongSymbols: Order = { outputToken: cloneTokenWithSymbol(order.outputToken, 'WRAPPED-SUPER-STABLECOIN-WITH-AN-EXTRA-SUFFIX'), } +function FinishedReceiptFixture(): ReactNode { + useEffect(() => { + getCowSoundReceiptBundle().forEach((sound) => { + sound.currentTime = 0 + sound.play().catch(() => undefined) + }) + }, []) + + return ( + + + + ) +} + function SolvingFixture(): ReactNode { const [countdown, setCountdown] = useState(15) const intervalRef = useRef(null) @@ -323,11 +346,7 @@ const Fixtures = { ), - '4-finished': () => ( - - - - ), + '4-finished': () => , '4-finished-customReceiver': () => ( { + return function MockSvg() { + return + } +}) +jest.mock('./assets/cowswap-thermal-wordmark.png', () => '/cowswap-thermal-wordmark.png') +jest.mock('./assets/cowswap-thermal-hero.png', () => '/cowswap-thermal-hero.png') + +const order = { + ...getOrderMock(SupportedChainId.MAINNET), + status: OrderStatus.FULFILLED, + creationTime: '2026-08-19T12:59:50.000Z', + fulfillmentTime: '2026-08-19T13:00:00.000Z', + apiAdditionalInfo: { + executedBuyAmount: '1000000', + executedSellAmount: '1000000000000000000', + executedSellAmountBeforeFees: '1000000000000000000', + }, +} as Order + +function renderReceipt(children: ReactNode): ReturnType { + return render( + + {children} + , + ) +} + +describe('PrintedOrderReceipt', () => { + it('renders fulfilled order details from the existing order model', () => { + renderReceipt( + , + ) + + expect(screen.getByRole('status', { name: 'Completed swap receipt' })).not.toBeNull() + expect(screen.getByRole('img', { name: 'CoW Swap' })).not.toBeNull() + expect(screen.getByText('CASHCOW SYSTEMS')).not.toBeNull() + expect(screen.queryByText(/^v\d+\.\d+\.\d+/)).toBeNull() + expect(screen.getByText('Thanks for swapping')).not.toBeNull() + expect(screen.getByText('Trade succeeded')).not.toBeNull() + expect(screen.getByText('Ethereum')).not.toBeNull() + expect(screen.getByText('10s')).not.toBeNull() + expect(screen.getByText('trader.eth ↗')).not.toBeNull() + expect(screen.getByText('CoW Solver')).not.toBeNull() + }) + + it('shows price improvement when surplus data is available', () => { + renderReceipt( + , + ) + + expect(screen.getByText('Price improvement')).not.toBeNull() + expect(screen.getByTitle('0.25 USDC')).not.toBeNull() + }) +}) diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx new file mode 100644 index 00000000000..417f68bb8d9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.pure.tsx @@ -0,0 +1,315 @@ +import { ReactNode } from 'react' + +import iconCowSrc from '@cowprotocol/assets/images/logo-icon-cow.svg' +import { CHAIN_INFO } from '@cowprotocol/common-const' +import { + ExplorerDataType, + getExplorerLink, + getExplorerOrderLink, + shortenAddress, + shortenOrderId, +} from '@cowprotocol/common-utils' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { CurrencyAmount, Price, Token } from '@cowprotocol/currency' +import { ExternalLink, TokenAmount } from '@cowprotocol/ui' + +import { Trans, useLingui } from '@lingui/react/macro' +import { PiArrowRightBold, PiDotsNineBold, PiSparkleFill } from 'react-icons/pi' +import SVG from 'react-inlinesvg' + +import { Order } from 'legacy/state/orders/actions' + +import { SurplusData } from 'common/hooks/useGetSurplusFiatValue' +import { SolverCompetition } from 'common/types/soverCompetition' + +import cowswapThermalHeroSrc from './assets/cowswap-thermal-hero.png' +import cowswapThermalWordmarkSrc from './assets/cowswap-thermal-wordmark.png' +import successCheckSrc from './assets/success-check-1bit@2x.png' +import * as styledEl from './PrintedOrderReceipt.styled' + +interface PrintedOrderReceiptProps { + order: Order + chainId: SupportedChainId + receiverEnsName?: string | null + surplusData?: SurplusData + winningSolver?: SolverCompetition +} + +interface ReceiptData { + executedPrice: Price | null + executionTime: string | null + filledAt: Date | null + networkName: string + receivedAmount: CurrencyAmount + receiver: string + soldAmount: CurrencyAmount + transactionHash: string | undefined +} + +export function PrintedOrderReceipt({ + order, + chainId, + receiverEnsName, + surplusData, + winningSolver, +}: PrintedOrderReceiptProps): ReactNode { + const { i18n, t } = useLingui() + const receiptData = getReceiptData(order, chainId) + + return ( + + + + + + + + + + + + + + Thanks for swapping + + + + + + + ) +} + +function formatReceiptTime(date: Date, locale: string): string { + return new Intl.DateTimeFormat(locale, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(date) +} + +function getExecutionTime(createdAt: Date | null, filledAt: Date | null): string | null { + if (!createdAt || !filledAt) return null + + const elapsedSeconds = Math.max(0, Math.round((filledAt.getTime() - createdAt.getTime()) / 1_000)) + if (elapsedSeconds < 60) return `${elapsedSeconds}s` + + const minutes = Math.floor(elapsedSeconds / 60) + const seconds = elapsedSeconds % 60 + return seconds ? `${minutes}m ${seconds}s` : `${minutes}m` +} + +function getReceiptData(order: Order, chainId: SupportedChainId): ReceiptData { + const executedSellAmount = + order.apiAdditionalInfo?.executedSellAmountBeforeFees || + order.apiAdditionalInfo?.executedSellAmount || + order.sellAmount + const executedBuyAmount = order.apiAdditionalInfo?.executedBuyAmount || order.buyAmount + const soldAmount = CurrencyAmount.fromRawAmount(order.inputToken, executedSellAmount) + const receivedAmount = CurrencyAmount.fromRawAmount(order.outputToken, executedBuyAmount) + const filledAt = getValidDate(order.fulfillmentTime) + + return { + executedPrice: + executedSellAmount === '0' || executedBuyAmount === '0' + ? null + : new Price({ baseAmount: soldAmount, quoteAmount: receivedAmount }), + executionTime: getExecutionTime(getValidDate(order.creationTime), filledAt), + filledAt, + networkName: CHAIN_INFO[chainId]?.label || String(chainId), + receivedAmount, + receiver: order.receiver || order.owner, + soldAmount, + transactionHash: order.fulfilledTransactionHash, + } +} + +function getValidDate(value: string | undefined): Date | null { + if (!value) return null + + const date = new Date(value) + return Number.isNaN(date.getTime()) ? null : date +} + +function ReceiptAmounts({ order, receiptData }: { order: Order; receiptData: ReceiptData }): ReactNode { + return ( + <> + + + + + You sold + + + + + + + + + You received + + + + + + + + + ) +} + +function ReceiptDetails({ + chainId, + locale, + receiptData, + receiverEnsName, + surplusData, + winningSolver, +}: { + chainId: SupportedChainId + locale: string + receiptData: ReceiptData + receiverEnsName?: string | null + surplusData?: SurplusData + winningSolver?: SolverCompetition +}): ReactNode { + const { executedPrice, executionTime, filledAt, networkName, receiver, transactionHash } = receiptData + const showSurplus = Boolean(surplusData?.showSurplus && surplusData.surplusAmount) + + return ( + <> + {executedPrice && ( + Execution price} + value={`1 ${executedPrice.baseCurrency.symbol || ''} = ${executedPrice.toSignificant(6)} ${ + executedPrice.quoteCurrency.symbol || '' + }`} + emphasize + /> + )} + {showSurplus && surplusData?.surplusAmount && } + Network} value={networkName} /> + {executionTime && Time to fill} value={executionTime} />} + {filledAt && Filled} value={formatReceiptTime(filledAt, locale)} />} + Received by} + value={ + + {receiverEnsName || shortenAddress(receiver)} ↗ + + } + /> + {winningSolver && ( + Winning solver} value={winningSolver.displayName || winningSolver.solver} /> + )} + {transactionHash && ( + Settlement} + value={ + + {shortenAddress(transactionHash)} ↗ + + } + /> + )} + + ) +} + +function ReceiptHeader({ order, chainId }: { order: Order; chainId: SupportedChainId }): ReactNode { + return ( + <> + + Order receipt + + + + + Trade succeeded + + + + Order {shortenOrderId(order.id)} ↗ + + + ) +} + +function ReceiptRow({ + label, + value, + emphasize = false, +}: { + label: ReactNode + value: ReactNode + emphasize?: boolean +}): ReactNode { + return ( + + {label} + {value} + + ) +} + +function ReceiptSurplus({ surplusData }: { surplusData: SurplusData }): ReactNode { + const { surplusAmount, surplusFiatValue } = surplusData + if (!surplusAmount) return null + + return ( + Price improvement} + value={ + + + + {surplusFiatValue && +surplusFiatValue.toFixed(2) > 0 ? ` (~$${surplusFiatValue.toFixed(2)})` : null} + + } + emphasize + /> + ) +} diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.styled.ts b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.styled.ts new file mode 100644 index 00000000000..2d82c021c50 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/PrintedOrderReceipt.styled.ts @@ -0,0 +1,586 @@ +import { ExternalLink, Font, Media, UI } from '@cowprotocol/ui' + +import styled, { css, keyframes } from 'styled-components/macro' + +const feedReceipt = keyframes` + 0% { + max-height: 0; + } + 4%, 16% { + max-height: 26px; + } + 40%, 58% { + max-height: var(--receipt-compact-height); + } + 100% { + max-height: 1200px; + } +` + +const cutterKick = keyframes` + 0%, 93% { + transform: translateY(0); + } + 95% { + transform: translateY(2px); + } + 97% { + transform: translateY(-1px); + } + 100% { + transform: translateY(0); + } +` + +export const ReceiptStage = styled.section` + width: 100%; + max-width: 520px; + margin: 0 auto; + padding: 0 12px 16px; + background: ${({ theme }) => (theme.darkMode ? `var(${UI.COLOR_PAPER})` : `var(${UI.COLOR_PAPER_DARKER})`)}; + border-radius: 24px; +` + +export const PrinterDevice = styled.div` + position: relative; + z-index: 3; + width: 100%; + min-height: 94px; + padding: 13px 20px 6px; + color: var(${UI.COLOR_NEUTRAL_100}); + background: var(${UI.COLOR_PAPER_DARKER}); + border: 1px solid var(${UI.COLOR_PAPER_DARKEST}); + border-radius: 22px 22px 18px 18px; + box-shadow: 0 13px 28px var(${UI.COLOR_BLACK_OPACITY_30}); + + ${Media.upToExtraSmall()} { + min-height: 84px; + padding: 10px 12px 5px; + border-radius: 18px 18px 15px 15px; + } +` + +export const SpeakerGrille = styled.div` + position: absolute; + top: 50%; + display: flex; + align-items: center; + justify-content: center; + color: var(${UI.COLOR_NEUTRAL_40}); + opacity: 0.48; + transform: translateY(-50%); + + &:first-child { + left: 20px; + } + + &:last-child { + right: 20px; + } + + > svg { + width: 36px; + height: 36px; + } + + ${Media.upToExtraSmall()} { + opacity: 0.38; + + &:first-child { + left: 12px; + } + + &:last-child { + right: 12px; + } + + > svg { + width: 28px; + height: 28px; + } + } +` + +export const PrinterCore = styled.div` + position: absolute; + inset: 0; + pointer-events: none; +` + +export const DeviceMark = styled.div` + position: absolute; + top: 50%; + left: 50%; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: max-content; + height: 14px; + color: var(${UI.COLOR_NEUTRAL_40}); + transform: translate(-50%, -50%); + + > svg { + flex: 0 0 auto; + width: 21px; + height: 14px; + opacity: 0.58; + } + + ${Media.upToExtraSmall()} { + height: 12px; + + > svg { + width: 18px; + height: 12px; + } + } +` + +export const DeviceIdentity = styled.span` + display: block; + color: inherit; + font-family: ${Font.familyMono}; + line-height: 1; + letter-spacing: 0.08em; + text-transform: uppercase; + white-space: nowrap; + opacity: 0.72; + + > strong { + font-size: 8px; + font-weight: 600; + } + + ${Media.upToExtraSmall()} { + > strong { + font-size: 7px; + } + } +` + +export const PrinterMouth = styled.div` + position: absolute; + z-index: 5; + left: 50%; + bottom: 2px; + width: calc(100% - 80px); + height: 12px; + background: var(${UI.COLOR_NEUTRAL_0}); + border: 1px solid color-mix(in srgb, var(${UI.COLOR_NEUTRAL_40}) 72%, transparent); + border-radius: 3px 3px 6px 6px; + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(${UI.COLOR_NEUTRAL_100}) 22%, var(${UI.COLOR_NEUTRAL_40})), + inset 0 -1px 2px var(${UI.COLOR_BLACK_OPACITY_30}), + 0 4px 8px var(${UI.COLOR_BLACK_OPACITY_30}); + transform: translateX(-50%); + + ${Media.upToExtraSmall()} { + width: 100%; + height: 10px; + } +` + +export const ReceiptReveal = styled.div` + --receipt-compact-height: 292px; + + position: relative; + z-index: 4; + width: calc(100% - 104px); + max-height: 0; + display: flex; + flex-direction: column; + justify-content: flex-end; + margin: -7px auto 0; + overflow: hidden; + transform-origin: top center; + will-change: max-height, transform; + -webkit-mask: + linear-gradient(#000 0 0) top / 100% calc(100% - 13px) no-repeat, + conic-gradient(from -45deg at 50% 100%, #000 0 90deg, transparent 90deg 360deg) bottom / 16px 13px repeat-x; + mask: + linear-gradient(#000 0 0) top / 100% calc(100% - 13px) no-repeat, + conic-gradient(from -45deg at 50% 100%, #000 0 90deg, transparent 90deg 360deg) bottom / 16px 13px repeat-x; + animation: + ${feedReceipt} 3.4s steps(30, end) forwards, + ${cutterKick} 3.4s linear forwards; + + @media (prefers-reduced-motion: reduce) { + max-height: none; + transform: none; + opacity: 1; + animation: none; + } + + ${Media.upToExtraSmall()} { + --receipt-compact-height: 262px; + + width: calc(100% - 16px); + margin-top: -6px; + } +` + +export const ReceiptPaper = styled.div` + width: 100%; + flex: 0 0 auto; + display: flex; + flex-direction: column; + padding: 26px 26px 42px; + color: color-mix(in srgb, var(${UI.COLOR_NEUTRAL_0}) 96%, var(${UI.COLOR_NEUTRAL_40})); + background-color: var(${UI.COLOR_NEUTRAL_100}); + background-color: color-mix(in srgb, var(${UI.COLOR_NEUTRAL_100}) 97%, #d8c8aa); + background-image: + radial-gradient( + circle, + color-mix(in srgb, var(${UI.COLOR_NEUTRAL_0}) 2.5%, transparent) 0 0.45px, + transparent 0.75px + ), + radial-gradient(circle, color-mix(in srgb, #8a7358 2%, transparent) 0 0.4px, transparent 0.7px); + background-position: + 0 0, + 7px 11px; + background-size: + 13px 17px, + 19px 23px; + box-shadow: 0 18px 30px var(${UI.COLOR_BLACK_OPACITY_30}); + font-family: ${Font.familyMono}; + font-size: 13px; + line-height: 1.35; + + ${Media.upToExtraSmall()} { + padding: 22px 18px 38px; + font-size: 12px; + } +` + +export const Brand = styled.div` + display: flex; + align-items: center; + justify-content: center; + min-width: 0; + min-height: 24px; + color: var(${UI.COLOR_NEUTRAL_40}); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + white-space: nowrap; +` + +export const PrintedWordmark = styled.img` + width: 204px; + height: auto; + opacity: 0.94; + mix-blend-mode: multiply; + image-rendering: crisp-edges; + image-rendering: pixelated; + + ${Media.upToExtraSmall()} { + width: 186px; + } +` + +export const HeroArtwork = styled.div` + position: relative; + width: 180px; + height: 80px; + display: flex; + align-items: center; + justify-content: center; + + ${Media.upToExtraSmall()} { + width: 164px; + height: 72px; + } +` + +export const HeroSparkle = styled.span<{ $side: 'left' | 'right' }>` + position: absolute; + top: 7px; + ${({ $side }) => $side}: 9px; + display: flex; + color: var(${UI.COLOR_NEUTRAL_0}); + + > svg { + width: 15px; + height: 15px; + } + + ${Media.upToExtraSmall()} { + top: 6px; + ${({ $side }) => $side}: 7px; + + > svg { + width: 13px; + height: 13px; + } + } +` + +export const SuccessStamp = styled.img` + position: absolute; + right: 17px; + bottom: 1px; + display: block; + width: 24px; + height: 24px; + opacity: 0.94; + mix-blend-mode: multiply; + image-rendering: crisp-edges; + image-rendering: pixelated; + + ${Media.upToExtraSmall()} { + right: 15px; + } +` + +export const PrintedHero = styled.img` + display: block; + width: 120px; + height: 80px; + object-fit: contain; + opacity: 0.94; + mix-blend-mode: multiply; + image-rendering: crisp-edges; + image-rendering: pixelated; + + ${Media.upToExtraSmall()} { + width: 108px; + height: 72px; + } +` + +export const CompletionHeading = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + margin: 18px auto 14px; + text-align: center; + + > strong { + font-family: ${Font.familyMono}; + font-size: 26px; + font-weight: 700; + line-height: 1.15; + letter-spacing: 0.045em; + text-transform: uppercase; + text-shadow: 0.75px 0 0 currentColor; + white-space: nowrap; + } + + ${Media.upToExtraSmall()} { + gap: 12px; + margin-top: 16px; + + > strong { + font-size: 21px; + letter-spacing: 0.035em; + } + } +` + +export const OrderLink = styled(ExternalLink)` + margin: 0 auto; + color: inherit; + font-size: 15px; + text-decoration: underline; + text-underline-offset: 3px; + + &:hover { + text-decoration-thickness: 2px; + } +` + +export const Divider = styled.hr` + width: 100%; + height: 0; + margin: 22px 0; + border: 0; + border-top: 2px dotted var(${UI.COLOR_NEUTRAL_40}); +` + +export const TearOffDivider = styled.div` + position: relative; + width: calc(100% + 52px); + height: 22px; + margin: 12px -26px 10px; + + > hr { + position: absolute; + top: 50%; + right: 11px; + left: 11px; + height: 1px; + margin: 0; + border: 0; + background: repeating-linear-gradient(to right, var(${UI.COLOR_NEUTRAL_50}) 0 8px, transparent 8px 16px); + transform: translateY(-50%); + } + + &::before, + &::after { + content: ''; + position: absolute; + z-index: 1; + top: 50%; + width: 22px; + height: 22px; + background: var(${UI.COLOR_PAPER_DARKER}); + border-radius: 50%; + transform: translateY(-50%); + } + + &::before { + left: -11px; + } + + &::after { + right: -11px; + } + + ${Media.upToExtraSmall()} { + width: calc(100% + 36px); + margin-right: -18px; + margin-left: -18px; + + > hr { + right: 10px; + left: 10px; + } + + &::before, + &::after { + width: 20px; + height: 20px; + } + + &::before { + left: -10px; + } + + &::after { + right: -10px; + } + } +` + +export const AmountBlock = styled.div` + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + text-align: center; + + > span { + color: var(${UI.COLOR_NEUTRAL_40}); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + } + + > strong { + max-width: 100%; + font-family: ${Font.family}; + font-size: clamp(16px, 5vw, 22px); + line-height: 1.2; + overflow-wrap: anywhere; + } +` + +export const AmountsContent = styled.div` + display: grid; + grid-template-columns: minmax(0, 1fr) 24px minmax(0, 1fr); + align-items: center; + gap: 10px; + padding-top: 12px; + + ${Media.upToExtraSmall()} { + grid-template-columns: minmax(0, 1fr) 22px minmax(0, 1fr); + gap: 6px; + } +` + +export const SwapArrow = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + margin: 0; + border: 1px solid var(${UI.COLOR_NEUTRAL_80}); + border-radius: 50%; + + > svg { + width: 13px; + height: 13px; + } + + ${Media.upToExtraSmall()} { + width: 22px; + height: 22px; + + > svg { + width: 11px; + height: 11px; + } + } +` + +export const ReceiptRow = styled.div<{ $emphasize: boolean }>` + display: grid; + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.4fr); + align-items: start; + gap: 14px; + margin: 0 0 10px; + + > span { + color: var(${UI.COLOR_NEUTRAL_40}); + } + + > strong { + min-width: 0; + color: inherit; + font-weight: ${({ $emphasize }) => ($emphasize ? 700 : 400)}; + text-align: right; + overflow-wrap: anywhere; + + a { + color: inherit; + text-decoration: underline; + text-underline-offset: 2px; + } + } + + ${({ $emphasize }) => + $emphasize && + css` + margin-bottom: 16px; + `} +` + +export const SurplusValue = styled.span` + color: var(${UI.COLOR_COWAMM_DARK_GREEN}); +` + +export const ReceiptFooter = styled.footer` + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin: 0; + text-align: center; + + > img { + color: var(${UI.COLOR_NEUTRAL_0}); + } +` + +export const ReceiptSignoff = styled.span` + color: var(${UI.COLOR_NEUTRAL_40}); + font-family: ${Font.familyMono}; + font-size: 11px; + line-height: 1; + letter-spacing: 0.1em; + text-transform: uppercase; +` diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/cowswap-thermal-hero.png b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/cowswap-thermal-hero.png new file mode 100644 index 00000000000..4c47e2ffa9e Binary files /dev/null and b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/cowswap-thermal-hero.png differ diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/cowswap-thermal-wordmark.png b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/cowswap-thermal-wordmark.png new file mode 100644 index 00000000000..d244bf9916e Binary files /dev/null and b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/cowswap-thermal-wordmark.png differ diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/success-check-1bit@2x.png b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/success-check-1bit@2x.png new file mode 100644 index 00000000000..1644a8cb08f Binary files /dev/null and b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/PrintedOrderReceipt/assets/success-check-1bit@2x.png differ diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.test.tsx b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.test.tsx new file mode 100644 index 00000000000..7967af57752 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.test.tsx @@ -0,0 +1,98 @@ +import { i18n } from '@lingui/core' +import { I18nProvider } from '@lingui/react' + +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +import { fireEvent, render, screen } from '@testing-library/react' +import { ThemeProvider } from 'styled-components/macro' +import { getCowswapTheme } from 'theme' + +import { Order, OrderStatus } from 'legacy/state/orders/actions' + +import { getCowSoundReceiptBundle } from 'modules/sounds' + +import { FinishedStep } from './FinishedStep' + +import { getOrderMock } from '../../../../mocks/orderMock' +import { OrderProgressBarStepName } from '../../constants' + +jest.mock('entities/injectedWidget', () => ({ + useInjectedWidgetParams: () => ({ disablePostTradeTips: false }), +})) + +jest.mock('modules/sounds') + +jest.mock('react-inlinesvg', () => { + return function MockSvg() { + return + } +}) + +jest.mock('../PrintedOrderReceipt/PrintedOrderReceipt.pure', () => ({ + PrintedOrderReceipt: () =>
Printed receipt
, +})) + +const order = { + ...getOrderMock(SupportedChainId.MAINNET), + status: OrderStatus.FULFILLED, +} as Order + +const receiptPlayMock = jest.fn().mockResolvedValue(undefined) +const successPlayMock = jest.fn().mockResolvedValue(undefined) +const receiptSoundMock = { currentTime: 1, play: receiptPlayMock } as unknown as HTMLAudioElement +const successSoundMock = { currentTime: 1, play: successPlayMock } as unknown as HTMLAudioElement + +function renderFinishedStep(stepName: OrderProgressBarStepName): ReturnType { + return render( + + + +
Post-trade extra
+
+
+
, + ) +} + +describe('FinishedStep', () => { + beforeEach(() => { + jest.clearAllMocks() + receiptSoundMock.currentTime = 1 + successSoundMock.currentTime = 1 + jest.mocked(getCowSoundReceiptBundle).mockReturnValue([successSoundMock, receiptSoundMock]) + }) + + it('shows only the printed receipt for a successful completion', () => { + renderFinishedStep(OrderProgressBarStepName.FINISHED) + + expect(screen.getByText('Printed receipt')).not.toBeNull() + expect(screen.queryByText('Solver auction rankings')).toBeNull() + expect(screen.queryByText('Post-trade extra')).toBeNull() + expect(screen.queryByText(/Share this/)).toBeNull() + }) + + it('keeps post-trade extras for cancellation failures', () => { + renderFinishedStep(OrderProgressBarStepName.CANCELLATION_FAILED) + + expect(screen.getByText('Printed receipt')).not.toBeNull() + expect(screen.getByText('Solver auction rankings')).not.toBeNull() + expect(screen.getByText('Post-trade extra')).not.toBeNull() + expect(screen.getByText(/Share this/)).not.toBeNull() + }) + + it('replays the receipt animation and sound from a user click', () => { + renderFinishedStep(OrderProgressBarStepName.FINISHED) + + fireEvent.click(screen.getByRole('button', { name: 'Replay' })) + + expect(receiptSoundMock.currentTime).toBe(0) + expect(successSoundMock.currentTime).toBe(0) + expect(receiptPlayMock).toHaveBeenCalledTimes(1) + expect(successPlayMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx index 234e71d24ba..bb54d1b6fc4 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/FinishedStep.tsx @@ -1,16 +1,9 @@ -import React, { ReactNode, useMemo, useState, Suspense, lazy } from 'react' - -import { i18n } from '@lingui/core' +import { ReactNode, useMemo, useState } from 'react' import iconSocialXSrc from '@cowprotocol/assets/images/icon-social-x.svg' -import LOTTIE_GREEN_CHECKMARK_DARK from '@cowprotocol/assets/lottie/green-checkmark-dark.json' -import LOTTIE_GREEN_CHECKMARK from '@cowprotocol/assets/lottie/green-checkmark.json' -import { RECEIVED_LABEL } from '@cowprotocol/common-const' -import { ExplorerDataType, getExplorerLink, getRandomInt, isSellOrder, shortenAddress } from '@cowprotocol/common-utils' +import { getRandomInt } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { Currency, CurrencyAmount } from '@cowprotocol/currency' -import { TokenLogo } from '@cowprotocol/tokens' -import { Confetti, ExternalLink, InfoTooltip, TokenAmount } from '@cowprotocol/ui' +import { InfoTooltip } from '@cowprotocol/ui' import { Trans, useLingui } from '@lingui/react/macro' import { useInjectedWidgetParams } from 'entities/injectedWidget' @@ -19,21 +12,22 @@ import SVG from 'react-inlinesvg' import { AMM_LOGOS } from 'legacy/components/AMMsLogo' import { Order } from 'legacy/state/orders/actions' -import { useIsDarkMode } from 'legacy/state/user/hooks' + +import { getCowSoundReceiptBundle } from 'modules/sounds' import { CowSwapAnalyticsCategory, toCowSwapGtmEvent } from 'common/analytics/types' import { SurplusData } from 'common/hooks/useGetSurplusFiatValue' import { SolverCompetition } from 'common/types/soverCompetition' -import { getIsCustomRecipient } from 'utils/orderUtils/getIsCustomRecipient' import * as styledEl from './styled' -import { CHAIN_SPECIFIC_BENEFITS, SURPLUS_IMAGES } from '../../constants' -import { getSurplusText, getTwitterShareUrl, getTwitterShareUrlForBenefit } from '../../helpers' -import { useWithConfetti } from '../../hooks/useWithConfetti' +import { CHAIN_SPECIFIC_BENEFITS } from '../../constants' +import { getTwitterShareUrl, getTwitterShareUrlForBenefit } from '../../helpers' import { OrderProgressBarStepName } from '../../types' +import { PrintedOrderReceipt } from '../PrintedOrderReceipt/PrintedOrderReceipt.pure' -const Lottie = lazy(() => import('lottie-react')) +// Temporary launch flag: let the printed receipt own the successful-completion surface. +const SHOW_POST_TRADE_EXTRAS_ON_SUCCESS = false interface FinishedStepProps { children: React.ReactNode @@ -64,31 +58,30 @@ export function FinishedStep({ const { t } = useLingui() const { disablePostTradeTips } = useInjectedWidgetParams() const [showAllSolvers, setShowAllSolvers] = useState(false) + const [receiptRun, setReceiptRun] = useState(0) const cancellationFailed = stepName === 'cancellationFailed' - const { surplusFiatValue, surplusAmount, showSurplus } = surplusData || {} + const { showSurplus } = surplusData || {} const shouldShowSurplus = debugForceShowSurplus || showSurplus - - const showConfetti = useWithConfetti({ - isFinished: stepName === 'finished', - surplusData, - debugForceShowSurplus, - }) + const showPostTradeExtras = SHOW_POST_TRADE_EXTRAS_ON_SUCCESS || stepName !== OrderProgressBarStepName.FINISHED + + const replayReceipt = (): void => { + getCowSoundReceiptBundle().forEach((sound) => { + sound.currentTime = 0 + sound.play().catch((error: unknown) => { + console.error('Receipt sound cannot be replayed', error) + }) + }) + setReceiptRun((currentRun) => currentRun + 1) + } const visibleSolvers = useMemo(() => { return showAllSolvers ? solvers : solvers?.slice(0, 3) }, [showAllSolvers, solvers]) - const isSell = order && isSellOrder(order.kind) - const isCustomRecipient = order && getIsCustomRecipient(order) - const receiver = order?.receiver || order?.owner - - const isDarkMode = useIsDarkMode() - const { randomBenefit } = useMemo(() => { const benefits = CHAIN_SPECIFIC_BENEFITS[chainId] return { - randomImage: SURPLUS_IMAGES[getRandomInt(0, SURPLUS_IMAGES.length - 1)], randomBenefit: t(benefits[getRandomInt(0, benefits.length - 1)]), } }, [chainId, t]) @@ -105,7 +98,6 @@ export function FinishedStep({ return ( - {showConfetti && } {cancellationFailed && ( @@ -116,30 +108,22 @@ export function FinishedStep({ )} - - - {order?.apiAdditionalInfo?.executedSellAmount && } + - {order?.apiAdditionalInfo?.executedBuyAmount && ( - + {stepName === OrderProgressBarStepName.FINISHED && ( + + Replay + )} - {shouldShowSurplus ? ( - - ) : null} - - {solvers && solversLength > 0 && ( + {showPostTradeExtras && solvers && solversLength > 0 && (

Solver auction rankings @@ -191,8 +175,8 @@ export function FinishedStep({ )} - {children} - {(!disablePostTradeTips || shouldShowSurplus) && ( + {showPostTradeExtras && children} + {showPostTradeExtras && (!disablePostTradeTips || shouldShowSurplus) && ( | null - surplusFiatValue?: CurrencyAmount | null - isCustomRecipient?: boolean - isSell?: boolean -}): ReactNode { - return ( - - {getSurplusText(isSell, isCustomRecipient)} - - + - {' '} - {surplusFiatValue && +surplusFiatValue.toFixed(2) > 0 && <>(~${surplusFiatValue.toFixed(2)})} - - ) -} - -function ReceivedAmount({ - order, - chainId, - isCustomRecipient, - receiver, - receiverEnsName, -}: { - order: Order - chainId: SupportedChainId - isCustomRecipient?: boolean - receiver?: string | null - receiverEnsName?: string | null -}): ReactNode { - return ( - - {!isCustomRecipient && i18n._(RECEIVED_LABEL)} - - - - {' '} - {isCustomRecipient && receiver && ( - <> - was sent to - - {receiverEnsName || shortenAddress(receiver)} ↗ - - - )} - - ) -} - -function SoldAmount({ order }: { order: Order }): ReactNode { - return ( - - - You sold - - - - - - ) -} - function SolverRow({ solver, index, @@ -334,20 +245,3 @@ function SolverRow({ ) } - -function TransactionStatus({ isDarkMode }: { isDarkMode: boolean }): ReactNode { - return ( - - {/* TODO: what fallback should be used here? */} - - - - Transaction completed! - - ) -} diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/styled.ts b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/styled.ts index 95dc69d4f0f..c241a6ce0c2 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/styled.ts +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/styled.ts @@ -245,6 +245,30 @@ export const ConclusionContent = styled.div` margin: 20px auto 0; ` +export const ReceiptReplayButton = styled.button` + position: fixed; + z-index: 20; + bottom: max(16px, env(safe-area-inset-bottom)); + left: max(16px, env(safe-area-inset-left)); + padding: 8px 14px; + color: var(${UI.COLOR_NEUTRAL_100}); + background: var(${UI.COLOR_NEUTRAL_0}); + border: 0; + border-radius: 999px; + font: inherit; + font-weight: 600; + cursor: pointer; + + &:hover { + opacity: 0.86; + } + + &:focus-visible { + outline: 2px solid var(${UI.COLOR_PRIMARY}); + outline-offset: 2px; + } +` + export const ShareButton = styled(ButtonPrimary)` gap: 10px; diff --git a/apps/cowswap-frontend/src/modules/sounds/index.ts b/apps/cowswap-frontend/src/modules/sounds/index.ts index 0d3a57be0a7..3766248acee 100644 --- a/apps/cowswap-frontend/src/modules/sounds/index.ts +++ b/apps/cowswap-frontend/src/modules/sounds/index.ts @@ -1 +1,8 @@ -export { getCowSoundSend, getCowSoundSuccess, getCowSoundError } from './utils/sound' +export { + getCowSoundSend, + getCowSoundSuccess, + getCowSoundReceipt, + getCowSoundReceiptBundle, + getCowSoundError, + setupCowSoundUnlock, +} from './utils/sound' diff --git a/apps/cowswap-frontend/src/modules/sounds/utils/sound.test.ts b/apps/cowswap-frontend/src/modules/sounds/utils/sound.test.ts index 9593f3022eb..87e0817c9f7 100644 --- a/apps/cowswap-frontend/src/modules/sounds/utils/sound.test.ts +++ b/apps/cowswap-frontend/src/modules/sounds/utils/sound.test.ts @@ -16,7 +16,7 @@ import { jotaiStore } from '@cowprotocol/core' import { cowSwapStore } from 'legacy/state' -import { __soundTestUtils } from './sound' +import { __soundTestUtils, setupCowSoundUnlock } from './sound' function buildFlags(overrides: Partial = {}): FeatureFlags { return { @@ -107,6 +107,12 @@ describe('getThemeBasedSound', () => { expect(getThemeBasedSound('SUCCESS')).toBe('/audio/success.mp3') }) + it('uses the thermal printer sound for receipts across seasonal themes', () => { + jotaiGet.mockReturnValue({ isChristmasEnabled: true }) + + expect(getThemeBasedSound('RECEIPT')).toBe('/audio/receipt-printer.wav') + }) + it('falls back to default sounds when April flag is disabled', () => { jotaiGet.mockReturnValue({}) @@ -138,3 +144,74 @@ describe('getThemeBasedSound', () => { expect(getThemeBasedSound('SEND')).toBe('/audio/send.mp3') }) }) + +describe('receipt widget sound', () => { + const getWidgetSoundUrl = __soundTestUtils.getWidgetSoundUrl + const jotaiGet = jotaiStore.get as jest.Mock + + afterEach(() => { + jest.clearAllMocks() + }) + + it('uses the existing orderExecuted widget customization', () => { + jotaiGet.mockReturnValue({ params: { sounds: { orderExecuted: 'https://example.com/filled.mp3' } } }) + + expect(getWidgetSoundUrl('RECEIPT')).toBe('https://example.com/filled.mp3') + }) + + it('preserves an explicitly muted orderExecuted widget sound', () => { + jotaiGet.mockReturnValue({ params: { sounds: { orderExecuted: null } } }) + + expect(getWidgetSoundUrl('RECEIPT')).toBeNull() + }) +}) + +describe('receipt sound unlock', () => { + it('primes the cached success and receipt sounds on the first user interaction', async () => { + const originalAudio = global.Audio + const successSound = { + currentTime: 1, + muted: false, + pause: jest.fn(), + play: jest.fn().mockResolvedValue(undefined), + preload: 'none', + volume: 1, + } as unknown as HTMLAudioElement + const receiptSound = { + currentTime: 1, + muted: false, + pause: jest.fn(), + play: jest.fn().mockResolvedValue(undefined), + preload: 'none', + volume: 1, + } as unknown as HTMLAudioElement + + Object.defineProperty(global, 'Audio', { + configurable: true, + value: jest.fn((src: string) => (src.includes('receipt-printer') ? receiptSound : successSound)), + writable: true, + }) + jest.mocked(jotaiStore.get).mockReturnValue({}) + + setupCowSoundUnlock() + window.dispatchEvent(new Event('pointerdown')) + await Promise.resolve() + + const sounds = [successSound, receiptSound] + sounds.forEach((sound) => { + expect(sound.play).toHaveBeenCalledTimes(1) + expect(sound.pause).toHaveBeenCalledTimes(1) + expect(sound.currentTime).toBe(0) + expect(sound.muted).toBe(false) + expect(sound.preload).toBe('auto') + }) + expect(successSound.volume).toBe(1) + expect(receiptSound.volume).toBe(0.2) + + Object.defineProperty(global, 'Audio', { + configurable: true, + value: originalAudio, + writable: true, + }) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts index 2b06b1c9690..3fa51a55271 100644 --- a/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts +++ b/apps/cowswap-frontend/src/modules/sounds/utils/sound.ts @@ -11,15 +11,23 @@ import { cowSwapStore } from 'legacy/state' import { featureFlagsAtom } from 'common/state/featureFlagsState' type Sounds = Record -type SoundType = 'SEND' | 'SUCCESS' | 'ERROR' +type SoundType = 'SEND' | 'SUCCESS' | 'RECEIPT' | 'ERROR' type WidgetSounds = keyof NonNullable const DEFAULT_COW_SOUNDS: Sounds = { SEND: '/audio/send.mp3', SUCCESS: '/audio/success.mp3', + RECEIPT: '/audio/receipt-printer.wav', ERROR: '/audio/error.mp3', } +const DEFAULT_COW_SOUND_VOLUMES: Record = { + SEND: 1, + SUCCESS: 1, + RECEIPT: 0.2, + ERROR: 1, +} + const WINTER_SOUNDS: Partial = { SEND: '/audio/send-winterTheme.mp3', SUCCESS: '/audio/success-winterTheme.mp3', @@ -33,6 +41,7 @@ const HALLOWEEN_SOUNDS: Partial = { const COW_SOUND_TO_WIDGET_KEY: Record = { SEND: 'postOrder', SUCCESS: 'orderExecuted', + RECEIPT: 'orderExecuted', ERROR: 'orderError', } @@ -109,11 +118,22 @@ function pickRandomAprilsFoolSound(): string { } const SOUND_CACHE: Record = {} +let soundUnlockInitialized = false export function getCowSoundError(): HTMLAudioElement { return getAudio('ERROR') } +export function getCowSoundReceipt(): HTMLAudioElement { + return getAudio('RECEIPT') +} + +export function getCowSoundReceiptBundle(): HTMLAudioElement[] { + if (isInjectedWidget() && getWidgetSoundUrl('RECEIPT') === null) return [] + + return Array.from(new Set([getCowSoundSuccess(), getCowSoundReceipt()])) +} + export function getCowSoundSend(): HTMLAudioElement { return getAudio('SEND') } @@ -122,8 +142,50 @@ export function getCowSoundSuccess(): HTMLAudioElement { return getAudio('SUCCESS') } -function createAudioOrEmpty(src: string): HTMLAudioElement { - return typeof Audio !== 'undefined' ? new Audio(src) : getEmptySound() +export function setupCowSoundUnlock(): void { + if (typeof window === 'undefined' || soundUnlockInitialized) return + if (isInjectedWidget() && getWidgetSoundUrl('RECEIPT') === null) return + + soundUnlockInitialized = true + + const cleanup = (): void => { + window.removeEventListener('pointerdown', unlock, true) + window.removeEventListener('keydown', unlock, true) + } + const unlock = (): void => { + cleanup() + + Promise.all( + getCowSoundReceiptBundle().map(async (sound) => { + const wasMuted = sound.muted + + sound.muted = true + sound.preload = 'auto' + try { + await sound.play() + sound.pause() + sound.currentTime = 0 + } finally { + sound.muted = wasMuted + } + }), + ).catch(() => { + soundUnlockInitialized = false + setupCowSoundUnlock() + }) + } + + window.addEventListener('pointerdown', unlock, { capture: true, once: true }) + window.addEventListener('keydown', unlock, { capture: true, once: true }) +} + +function createAudioOrEmpty(src: string, volume: number): HTMLAudioElement { + if (typeof Audio === 'undefined') return getEmptySound() + + const sound = new Audio(src) + sound.preload = 'auto' + sound.volume = volume + return sound } function getAudio(type: SoundType): HTMLAudioElement { @@ -139,7 +201,7 @@ function getAudio(type: SoundType): HTMLAudioElement { let sound = SOUND_CACHE[soundPath] if (!sound) { - sound = createAudioOrEmpty(soundPath) + sound = createAudioOrEmpty(soundPath, widgetSound ? 1 : DEFAULT_COW_SOUND_VOLUMES[type]) SOUND_CACHE[soundPath] = sound } @@ -151,7 +213,7 @@ function getAudio(type: SoundType): HTMLAudioElement { let sound = SOUND_CACHE[soundPath] if (!sound) { - sound = createAudioOrEmpty(soundPath) + sound = createAudioOrEmpty(soundPath, DEFAULT_COW_SOUND_VOLUMES[type]) SOUND_CACHE[soundPath] = sound } @@ -182,4 +244,5 @@ function getWidgetSoundUrl(type: SoundType): string | null | undefined { export const __soundTestUtils = { getThemeBasedSound, + getWidgetSoundUrl, } as const diff --git a/apps/cowswap-frontend/src/styles/fonts.css b/apps/cowswap-frontend/src/styles/fonts.css index ce302ad6bef..aa1d1d45e6f 100644 --- a/apps/cowswap-frontend/src/styles/fonts.css +++ b/apps/cowswap-frontend/src/styles/fonts.css @@ -55,4 +55,12 @@ font-weight: 400; font-style: normal; font-display: fallback; -} \ No newline at end of file +} + +@font-face { + font-family: 'studiofeixenmono'; + src: url('/static/StudioFeixenMono-Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: fallback; +} diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 00000000000..ce8b160fb55 --- /dev/null +++ b/design-qa.md @@ -0,0 +1,261 @@ +# Design QA: animated CoW order receipt + +## Evidence + +- Source hero truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_UMVvVw/Screenshot 2026-08-19 at 17.26.02.png` (734 × 536 px; source CSS size and density unknown). +- Source analog wordmark truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_6SUggh/Screenshot 2026-08-19 at 16.59.49.png` (692 × 290 px; source CSS size and density unknown). +- Source thin footer-wordmark truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_I4ZtbJ/Screenshot 2026-08-19 at 23.07.06.png` (316 × 84 px; the live pre-change footer crop supplied by the user). +- Source heavy-print wordmark truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_P24Jlq/Screenshot 2026-08-19 at 23.07.25.png` (1404 × 376 px; the supplied reference for thicker deposited-ink strokes). +- Source printer-housing truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_XcHlmy/Screenshot 2026-08-19 at 17.29.51.png` (1350 × 200 px; source CSS size and density unknown). +- Source receipt-label truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_LSDEAD/Screenshot 2026-08-19 at 17.40.48.png` (1234 × 179 px; source CSS size and density unknown). +- Source success-ornament truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_3xVlaI/Screenshot 2026-08-19 at 17.42.03.png` (706 × 526 px; source CSS size and density unknown). +- Source compact motion checkpoint: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_aapDtC/Screenshot 2026-08-19 at 18.03.50.png` (844 × 474 px; source CSS size and density unknown). +- Source pre-simplification printer controls: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_zwcV9i/Screenshot 2026-08-19 at 18.04.33.png` (1022 × 217 px; source CSS size and density unknown). +- Source detachable-stub truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_gmN2EN/Screenshot 2026-08-19 at 18.05.39.png` (1022 × 217 px; source CSS size and density unknown). +- Source thank-you footer truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_7KbbD3/Screenshot 2026-08-19 at 18.17.40.png` (329 × 180 px; source CSS size and density unknown). +- Source pre-change success-check crop: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_bKIcJs/Screenshot 2026-08-19 at 18.19.44.png` (148 × 94 px; source CSS size and density unknown). +- Source smooth completion-stamp truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_DO0Yd7/Screenshot 2026-08-19 at 19.15.20.png` (128 × 84 px; the live pre-fix filled vector stamp supplied by the user). +- Source off-center manufacturer-lockup truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_TLJ4Qf/Screenshot 2026-08-19 at 19.21.37.png` (1022 × 204 px; the live pre-fix hardware crop supplied by the user). +- Source two-line manufacturer-lockup truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_6iYxDr/Screenshot 2026-08-19 at 19.28.45.png` (313 × 97 px; the live pre-simplification crop supplied by the user). +- Source vertical sold/received truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_OpNiFo/Screenshot 2026-08-19 at 19.31.57.png` (824 × 452 px; the live pre-compaction amount region supplied by the user). +- Source order-link type truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_8qWxwh/Screenshot 2026-08-19 at 19.36.19.png` (588 × 98 px; the live pre-change link crop supplied with an explicit 15 px target). +- Source undersized-slot truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_7pwe8L/Screenshot 2026-08-19 at 22.21.02.png` (1020 × 762 px; the live pre-fix printer/receipt capture supplied by the user). +- Source detached-feed truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_nlEdTn/Screenshot 2026-08-19 at 22.44.28.png` (1068 × 252 px; the live light-theme crop where the sheet began below the housing instead of visibly crossing the slit). +- Source pre-plate hardware crop: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_VbMldz/Screenshot 2026-08-19 at 18.31.17.png` (992 × 164 px; normalized to 496 × 82 for the focused comparison, consistent with a 2× source capture). +- Source simple-print footer truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_D89diL/Screenshot 2026-08-19 at 19.09.36.png` (560 × 240 px; source CSS size and density unknown). +- Source pre-simplification CoW footer: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_QlOqGK/Screenshot 2026-08-19 at 19.09.41.png` (466 × 220 px; source CSS size and density unknown). +- Source inset-perforation truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_rxI2sd/Screenshot 2026-08-19 at 19.10.41.png` (918 × 98 px; normalized to 392 px wide for the focused comparison). +- Source floating-slot truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_UXTzfp/Screenshot 2026-08-19 at 19.13.31.png` (952 × 60 px; the live pre-fix slot crop supplied by the user). +- Source motion truth: `/Users/mb/Downloads/HPmba-zbgAAcZHd.mp4` (952 × 1080 px, 11.43 s variable-frame-rate H.264, no audio track). +- Source bottom-first stub truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_d4Dzls/Screenshot 2026-08-19 at 19.14.02.png` (960 × 574 px; torn leading edge and footer sit below the later `Requested` band). +- Source bottom-first body truth: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_Jl0PpD/Screenshot 2026-08-19 at 19.14.09.png` (962 × 700 px; transaction bands enter at the printer side while earlier content extends below). +- Source light-theme contrast issue: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_zDCZp8/Screenshot 2026-08-20 at 12.26.59.png` (1162 × 1718 px; white receipt stock visually merges into the white modal surface). +- Source dark-theme saw artifact: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_V82YD2/Screenshot 2026-08-20 at 12.27.06.png` (1036 × 1714 px; the theme-colored saw cutout does not match the modal navy). +- Source low-contrast exit slot crop: `/var/folders/yc/4_t8sry90_gcz8671m1vcqhm0000gn/T/TemporaryItems/NSIRD_screencaptureui_AU6add/Screenshot 2026-08-20 at 12.27.52.png` (1158 × 72 px; only a very thin black aperture remains visible above the paper). +- User-supplied audio truth: `/Users/mb/Downloads/freesound_community-cashierreceiptservo-107601.mp3` (3.288 s decoded duration, 160 kbps MP3 source, 24 kHz stereo container metadata). +- Implementation theme correction, light: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-theme-light-fixed.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation theme correction, dark: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-theme-dark-fixed.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation theme correction, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-theme-dark-narrow-fixed.png` (986 × 934 browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Implementation compact stage: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-compact-latest.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation expanded hero: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-expanded-hero-latest.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation expanded footer: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-footer-latest.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation narrow compact stage: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-narrow-compact-latest.png` (986 × 934 px; 331 px rendered receipt width; device scale factor 1). +- Implementation light-theme check: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-light-compact-latest.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation CoW terminal, desktop compact: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cow-device-desktop-compact-final.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation CoW terminal, desktop expanded: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cow-device-desktop-expanded-final.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation CoW terminal, iPhone SE preview: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cow-device-narrow-final.png` (986 × 934 browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined printer comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cow-device-comparison.png` (1350 × 670 px; source at native pixels above a focused implementation crop below). +- Implementation centered receipt label: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-centered-label-final.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Combined label comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-centered-label-comparison.png` (986 × 482 px; normalized source above a focused implementation crop below). +- Implementation success ornaments, desktop compact: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-success-ornaments-first.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1). +- Implementation success ornaments, iPhone SE compact: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-success-ornaments-narrow.png` (986 × 934 browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined success-ornament comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-success-ornaments-comparison.png` (706 × 973 px; source above a focused implementation crop below). +- Implementation motion sequence: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-motion-zero.jpg`, `receipt-motion-teeth.jpg`, `receipt-motion-early-feed.jpg`, `receipt-motion-compact.jpg`, and `receipt-motion-expanded.jpg` (each 560 × 420 px focused browser capture at device scale factor 1). +- Implementation narrow motion sequence: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-motion-narrow-teeth.jpg` and `receipt-motion-narrow-compact.jpg` (each 986 × 934 browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined motion comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-motion-comparison.jpg` (1000 × 854 px; source compact checkpoint above the normalized zero/teeth/feed/compact/full implementation sequence). +- Implementation simplified slot, desktop compact: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-simplified-slot-desktop.jpg` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation simplified slot, iPhone SE compact: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-simplified-slot-narrow.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined slot comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/printer-slot-comparison.png` (1090 × 168 px; the user's pre-simplification device crop and a same-region implementation crop shown side by side). +- Implementation detachable stub, desktop compact: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-tear-off-desktop-final.jpg` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation detachable stub, iPhone SE compact: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-tear-off-narrow-final.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined detachable-stub comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-tear-off-comparison-final.png` (640 × 255 px; normalized source and implementation perforation regions stacked at the same width). +- Implementation thank-you footer, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-thank-you-desktop.jpg` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation thank-you footer, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-thank-you-narrow.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined thank-you footer comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-thank-you-comparison.png` (420 × 411 px; normalized source and implementation footer regions stacked at the same width). +- Implementation filled success stamp, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-circle-check-desktop.jpg` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation filled success stamp, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-circle-check-narrow.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined success-stamp comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-circle-check-comparison.png` (984 × 376 px; the supplied pre-change crop and the focused implementation shown side by side with nearest-neighbor enlargement). +- Implementation one-bit success stamp, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-pixel-check-desktop.png` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation one-bit success stamp, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-pixel-check-narrow.png` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined one-bit stamp comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-pixel-check-comparison.png` (930 × 320 px; the supplied smooth-vector crop, focused live receipt, and enlarged 12 × 12 logical bitmap shown together). +- Implementation centered manufacturer lockup: live in-app Browser captures at the 986 × 934 desktop canvas and 375 × 667 iPhone SE fixture. The supplied source crop and post-fix desktop capture were opened together in one comparison input. Browser geometry measured the desktop lockup and housing at the same center `(489, 195.5)` and the narrow lockup and housing at the same center `(183.5, 190.5)`. +- Implementation minimal manufacturer lockup: live in-app Browser captures at the 986 × 934 desktop canvas and 375 × 667 iPhone SE fixture. The supplied two-line source crop and single-line post-fix capture were opened together in one comparison input. The final lockup measures 118.45 × 14 px on desktop and 106.42 × 12 px on narrow screens, with no version node in the rendered fixture. +- Implementation horizontal sold/received lockup: live in-app Browser captures at the 986 × 934 desktop canvas and 375 × 667 iPhone SE fixture. The supplied vertical source and post-fix desktop capture were opened together in one comparison input. The desktop amount grid resolves to `148px 24px 148px`; the narrow grid resolves to `128.5px 22px 128.5px`, with no horizontal document overflow. +- Implementation 15 px order link: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-order-link-15px.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1) and focused crop `receipt-order-link-15px-crop.png` (360 × 110 px). The supplied crop and focused post-change capture were opened together in one comparison input. Browser-computed type is exactly `15px` with a single `20.25px` line, a 189.66 px rendered width, and no wrapping or clipping. +- Implementation paper-width slot: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-slot-paper-width-desktop.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1) and `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-slot-paper-width-narrow.png` (986 × 934 browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). The supplied pre-fix capture and both post-fix captures were opened together in one comparison input. Browser geometry measures a centered 414 px slot over a 392 px paper on desktop and a centered 341 px slot over a 327 px paper at iPhone SE, with no document overflow. +- Implementation slit-centered feed: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-feed-origin-light.png` and `receipt-feed-origin-dark.png` (986 × 934 px at a 986 × 934 CSS viewport; device scale factor 1), plus `receipt-feed-origin-narrow.png` (986 × 934 browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). The supplied light-theme crop and all three post-fix captures were opened together in one comparison input. Browser geometry places the paper origin 0.5 px below the 7 px desktop slit center and 1 px below the 6 px iPhone SE slit center, with the sheet layered above the lower aperture half and no horizontal overflow. +- Implementation heavy printed wordmark: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-thick-wordmark-desktop.png` and `receipt-thick-wordmark-narrow.png` (each 986 × 934 px; the narrow capture contains a 375 × 667 CSS fixture viewport). The thin source crop, heavy-print reference, desktop implementation, and narrow implementation were opened together in one comparison input. The live mark renders at 204 × 25.77 px on desktop and 186 × 23.49 px at iPhone SE, with its intrinsic 380 × 48 one-bit raster preserved and no horizontal document overflow. +- First manufacturer-plate pass, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cashcow-device-plate-narrow-first.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Implementation CashCow manufacturer plate, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cashcow-device-plate-desktop.jpg` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation CashCow manufacturer plate, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cashcow-device-plate-narrow.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined manufacturer-plate comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/cashcow-device-plate-comparison.png` (540 × 278 px; normalized pre-change hardware above a same-scale focused live implementation crop). +- Implementation simplified footer, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-simple-footer-desktop.jpg` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation simplified footer, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-simple-footer-narrow.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined simple-footer comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-simple-footer-comparison.png` (428 × 634 px; source, pre-simplification CoW footer, and focused final implementation stacked for direct visual-beat comparison). +- Implementation edge-to-edge perforation, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-edge-perforation-desktop.jpg` (986 × 932 px at a 986 × 932 CSS viewport; device scale factor 1). +- Implementation edge-to-edge perforation, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-edge-perforation-narrow.jpg` (986 × 932 px browser capture containing a 375 × 667 CSS fixture viewport; device scale factor 1). +- Combined perforation comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-edge-perforation-comparison.png` (428 × 198 px; normalized pre-change crop above the focused live implementation). +- Implementation integrated slit, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/printer-integrated-slit-desktop.jpg` (560 × 155 px focused browser capture; device scale factor 1). +- Implementation integrated slit, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/printer-integrated-slit-narrow.jpg` (390 × 145 px focused browser capture containing the 375 px fixture; device scale factor 1). +- Combined integrated-slit comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/printer-integrated-slit-comparison.png` (952 × 388 px; supplied pre-fix crop above the focused post-fix hardware). +- Implementation bottom-first feed, desktop: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-reverse-feed-zero.jpg`, `receipt-reverse-feed-teeth.jpg`, `receipt-reverse-feed-lower-first.jpg`, and `receipt-reverse-feed-header-last.jpg` (focused browser captures; 600 px wide at device scale factor 1). +- Implementation bottom-first feed, iPhone SE: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-reverse-feed-narrow-teeth.jpg`, `receipt-reverse-feed-narrow-lower-first.jpg`, and `receipt-reverse-feed-narrow-header-last.jpg` (focused browser captures containing the 375 px fixture; device scale factor 1). +- Combined bottom-first feed comparison: `/Users/mb/Documents/Codex/2026-08-19/i-go/work/design-qa/receipt-reverse-feed-comparison.jpg` (1200 × 760 px; both supplied source states above the normalized teeth/lower-content/header-last implementation sequence). +- Route: React Cosmos fixture `src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx`, fixture `4-finished`. +- State: fulfilled standard swap; short success stub first, then full order receipt. + +The references are cropped source captures while the implementation is a component inside the Cosmos canvas, so a reliable pixel overlay is not possible. Density mismatch, browser chrome, and surrounding canvas were excluded from findings. The source and implementation were opened together in the same comparison inputs and judged on the shared receipt region, hierarchy, silhouette, typography, and analog-print character. + +## Findings + +- No actionable P0, P1, or P2 findings remain. +- The successful-completion surface is now intentionally receipt-only: solver auction rankings, the surplus/share card, injected post-trade content, and the share CTA are gated off for `FINISHED` while remaining available for cancellation-failed states. This removes the dense 1296 × 1094 post-trade section supplied in the latest source screenshot without changing failure recovery. +- The printer gained three restrained realism cues without adding UI density: a warm paper cast and slightly softened one-bit ink, a shallow contact shadow/highlight inside the exit slit, and a 1–2 px cutter kick synchronized to the final mechanical beat. All three are token-led or progressive-enhancement treatments and collapse under reduced motion. +- The Cosmos finished fixture now starts the animation and attempts the configured receipt sound on mount, with a single fixed `Replay` control at the bottom-left for deterministic user-gesture playback. Production swaps continue to use the fulfillment middleware as the sole automatic audio trigger, preventing duplicate playback on receipt remount. +- Light mode now gives the receipt a dedicated `COLOR_PAPER_DARKER` stage surface, creating a visible grey gutter between warm-white stock and the white modal. Dark mode instead uses `COLOR_PAPER`, which browser measurements confirm exactly matches the modal surface at `rgb(24, 25, 59)`. +- The moving saw edge is now a binary alpha mask on the reveal viewport rather than an overlay painted with a guessed theme color. Its cutouts therefore expose the real surface beneath in every theme and at every animation height; the former black/navy mismatch is structurally impossible. +- The exit aperture increased from 7/6 px to 12/10 px desktop/narrow while the paper still begins near its midpoint. A tokenized neutral border and inner highlight make 7 px of the desktop recess visible without recreating the earlier floating capsule. +- The hero and footer raster marks now include two 2%-high opacity bands at 97% and 98% density. This is intentionally sub-perceptual at a glance and affects only those printed assets; semantic content, dividers, and metadata remain uniform. +- The former printer mouth used a thick 3 px outline, full capsule radius, external drop shadow, and a 340 px span; in the supplied crop those cues detached it from the housing and made it read as a floating black UI control. The final mouth is a shorter 300 × 7 px recessed slit on desktop (218 × 6 px in the iPhone SE fixture), positioned 3 px inside the device bottom with no border or cast shadow. +- The receipt icon and progress line have been removed from the printer mouth. A slim recessed aperture remains, so the housing reads as one coherent device while the sheet still has a believable physical origin. +- The success header is now a visually detachable order stub: the abbreviated explorer-linked order ID sits above two edge punch-outs and a long-dash perforation guide, clearly separating the keepsake header from the order details below. +- Sold and received amounts now share one compact horizontal receipt row with a right-pointing Phosphor arrow. Labels remain directly above their respective values, preserving unambiguous input/output association while substantially reducing paper length. +- The abbreviated order link now renders at the requested 15 px. It remains centered on one line and retains its underline and external-link affordance without crowding the success title or perforation. +- The recessed printer opening is now slightly wider than the receipt instead of 92 px narrower. Its 11 px desktop and 7 px narrow overhang per side makes the sheet read as physically emerging from the housing while preserving the existing 7/6 px low-profile aperture. +- The receipt now visibly crosses the aperture rather than beginning beneath the whole printer. The upper half of the black slit remains exposed, the paper begins at its vertical midpoint, and the housing still masks the sheet above that origin in light, dark, and iPhone SE states. +- The receipt now ends with only two visual beats: the quiet mono line `THANKS FOR SWAPPING` and the one-bit CoW Swap lockup. The former `THANK YOU!`, `SEE YOU NEXT SWAP`, and `SETTLED WITH COW PROTOCOL` stack was removed because it competed with the printed mark and felt more like composed UI than a real receipt. +- The latest printer housing deliberately translates the reference machine into a restrained CoW terminal: a wider matte shell, recessed low slot, overlapping feed lip, symmetric perforation icons, and a muted official cow mark. It avoids realistic textures, gradients, chrome, knobs, and copied device detailing. +- `ORDER RECEIPT` is now centered on the paper and uses `UI.COLOR_NEUTRAL_40`, exactly matching the muted metadata labels below it. +- Two restrained one-bit sparkles and a small filled circle-check now surround the cow. The final stamp is an actual 12 × 12 one-bit bitmap, so its circumference and knocked-out check have intentional stair-steps instead of the former vector-smooth curves. +- The device face now carries only a restrained `CASHCOW SYSTEMS` manufacturer line beside a smaller official cow mark. The version was removed to reduce visual noise; the cow and label remain one lockup geometrically centered on both axes of the housing. +- The detachable-stub perforation now runs to the inner tangent of both circular punch-outs instead of stopping 17 px early. Its ink was moved from neutral-40 to the lighter neutral-50 receipt grey, matching the requested physical ticket treatment. +- The receipt now behaves like a hanging roll instead of a top-down web reveal. It begins at true zero, exposes one torn leading edge, then reveals the footer and lower metadata first; later bands enter at the slot and push that printed content downward until `ORDER RECEIPT` and `TRADE SUCCEEDED` arrive last. The final document order is unchanged. +- The Bai Bai hamster is intentionally translated into the official CoW head silhouette rather than copied. At 120 × 80 CSS px it is the dominant compact-stage mark and preserves the requested mascot-like hierarchy. +- P3: the official CoW head has a shorter vertical silhouette than the source full-body hamster. This is an acceptable brand-specific deviation; a future custom full-body CoW mascot asset could increase character without changing layout. + +## Required fidelity surfaces + +- Fonts and typography: `TRADE SUCCEEDED` uses the real StudioFeixen Mono Bold asset at 26 px with uppercase tracking and a slight mechanical overprint. Receipt metadata stays in StudioFeixen Mono. `ORDER RECEIPT` remains 11 px uppercase mono but is now centered and visually subordinate in the same neutral-40 tone as the detail labels. The title remains on one line at the 331 px receipt width. The footer now uses one regular 11 px mono line, letting the raster wordmark—not a second bold UI heading—carry the visual weight. +- Footer mark print weight: each previously isolated logical ink pixel now occupies a 2 × 2 logical block before nearest-neighbor presentation. The heavier cells join into continuous thermal-style strokes while retaining open counters, the official CoW-derived silhouette, the barcode registration bars, and the existing accessible `CoW Swap` image label. No layout dimensions or responsive breakpoints changed. +- Spacing and layout: the housing is wider than the paper and the aperture is a 62%/300 px maximum-width slit placed 3 px inside the device bottom; narrow screens use 64% width and a 6 px height. The paper still begins behind that line, making it read as emerging from the device without introducing a second UI-like pill. The official cow mark is 21 × 14 px desktop and 18 × 12 px narrow; together with the single manufacturer line it forms a 118.45 × 14 px desktop lockup and 106.42 × 12 px narrow lockup, each positioned at the exact horizontal and vertical center of its device face. The tear guide spans from the 11 px desktop/10 px narrow punch-out tangent on one edge to the matching tangent on the other. The teeth-only keyframe reveals 26 px in total so the first 13 px remains behind the lip and exactly one 13 px tear row is exposed. The reveal is a bottom-aligned flex viewport and the paper is a non-shrinking flex item: at the 292 px desktop hold, the paper's bottom aligns exactly with the viewport bottom while its top remains above the slot; at completion both top and bottom edges align. Narrow layouts keep the 262 px checkpoint. Final expanded content maintains the existing header/amount/detail/footer order. +- Amount layout: the amount section is a three-column grid with two flexible value columns around a fixed 24 px desktop/22 px narrow arrow. Desktop uses 10 px gaps and 148 px amount columns; the iPhone SE state uses 6 px gaps and 128.5 px columns. Amount type scales from 22 px down to 18.75 px in the 375 px fixture via `clamp()`, preserving emphasis without wrapping the fixture values. +- Colors and tokens: paper, housing, ink, rules, shadows, grilles, and surplus treatment continue to use CoW UI tokens. The perforation uses `UI.COLOR_NEUTRAL_50` (`#827474` in the current theme) so it reads as faded grey ticket ink rather than a black content rule. The matte shell uses one border and one restrained shadow rather than faux-material effects. The one-bit hero and footer assets remain true black on white receipt stock in both themes. +- Image quality and asset fidelity: the hero is a strict one-bit raster derived only from the official `logo-icon-cow.svg`, shown nearest-neighbor at 120 × 80. The footer lockup is a transparent one-bit CoW-specific raster with the official icon, matrix `COW SWAP` lettering, and restrained registration bars. The completion stamp is derived from the existing Phosphor `PiCheckCircleFill`, reduced to a 12 × 12 binary-alpha grid and nearest-neighbor enlarged into a 48 × 48 source asset. All three remain crisp, undistorted, and halo-free. +- Copy and content: `CASHCOW SYSTEMS` is treated as a fictional hardware-maker label. The app-version line was deliberately removed from this decorative surface; the existing application footer remains the canonical place for version metadata. `Trade succeeded`, `Order receipt`, the real abbreviated order-UID link inside the detachable stub, sold/received values, execution price, surplus, network, timing, recipient, solver, and the compact `Thanks for swapping` sign-off form one coherent printed narrative. No transaction hash is invented. +- Icons: the printer mouth is intentionally icon-free. The housing uses the official cow SVG; the speaker grilles and hero sparkles use the repository's Phosphor icon family. The completion stamp is a deterministic one-bit raster derivative of the repository's Phosphor filled circle-check, displayed at exactly 24 × 24 CSS px with `image-rendering: pixelated` at both desktop and narrow widths. Each logical source pixel therefore occupies a stable 2 × 2 CSS-pixel block. No emoji, placeholder, handcrafted SVG, or copied Bai Bai mascot appears. +- Responsiveness: the Cosmos wrappers now use `min(..., 100%)`, so the 520 px terminal and paper center correctly inside the 375 × 667 iPhone SE preview. The two grilles, cow mark, success ornaments, slot, receipt title, link, and saw edge remain visible without horizontal clipping or title wrapping. + +## Motion, sound, interaction, and accessibility + +- The 3.4 s, 30-step feed starts at zero; reaches the 26 px teeth-only state at 4% and holds it through 16%; reaches the 292 px lower-receipt checkpoint at 40% and holds through 58%; then feeds to the full 1200 px ceiling. Because the full paper is bottom-aligned inside the growing viewport, newly exposed bands enter at the slot and push the footer downward. The 13 px saw edge remains attached to the animated leading edge throughout. +- The same 3.4 s timeline now adds a small cutter response at 95–97%: the paper moves down 2 px, rebounds 1 px, and settles. It is intentionally subordinate to the stepped feed and is disabled by the existing reduced-motion rule. +- The cashier-servo clip was re-aligned into a 3.400 s mono 44.1 kHz/16-bit PCM WAV: source 0.08–1.44 s drives emergence and the compact print, 1.36–1.97 s is a true hold, and source 1.79–3.22 s drives the full feed. Output peak is −4.11 dBFS and RMS is −22.50 dBFS with no clipping. +- Compared with the previous soft asset, the processed cashier-servo version has over three times the first-difference brightness proxy (0.626 versus 0.194) and six detected mechanical attacks instead of four, while preserving two intentional active phases. +- The preview replay control remains wired to the same receipt getter and animation restart path, and now returns immediately while audio playback resolves in the background. The renderer served the replacement WAV as `audio/wav` with SHA-256 `209792988c9a3fdbfbef7aff4cb3fae554b8a0964eb42b39a0567150014abe9d`, identical to the checked-in asset. +- Production playback remains in the existing fulfillment middleware, so standard swaps play once and widget `orderExecuted` custom/muted configuration remains respected. Bridge and non-swap completions keep the existing success sound. +- Standard completion confetti remains removed. Browser inspection found zero canvas elements during and after replay; bridge-specific behavior is unchanged. +- The receipt is a polite atomic `role="status"`; the cow, sparkles, and check are grouped as decorative hero artwork because `TRADE SUCCEEDED` already supplies the spoken status. The footer wordmark supplies one `CoW Swap` accessible name. The order link and replay button are semantic and keyboard reachable. Reduced motion resolves directly to the full receipt. +- Browser inspection found no audio error state after replay. Console errors were checked; no local-app errors were present. One older third-party Safary-tag error from `files.cow.fi` remained in the tab log and is unrelated to this component. + +## Full-view and focused comparison evidence + +- Latest success-surface comparison: the supplied 1296 × 1094 screenshot was reviewed alongside `receipt-realism-final.png` and `receipt-realism-narrow-final.png`. The former rankings, surplus card, and share CTA are absent from the live success DOM and viewport; the printed receipt remains centered, complete, and overflow-free at desktop and 375 px. +- Realism comparison: `receipt-realism-leading-edge.png` confirms that only the torn leading edge emerges first, while `receipt-realism-final.png` confirms the warmer paper, softer thermal ink, recessed slot contact shadow, fixed bottom-left Replay control, and full receipt-only completion state. +- Full-view compact comparison: the latest 734 × 536 hero reference and `receipt-compact-latest.png` were opened together. Both establish a dominant monochrome mascot, bold printed success title, compact explorer link region, and large surrounding white-paper field. +- Focused hero comparison: the implementation intentionally replaces the source full-body hamster with the official CoW head, while preserving the measured 120 px-wide visual mass and immediate title relationship. +- Focused footer comparison: the 692 × 290 Bai Bai wordmark crop and `receipt-footer-latest.png` were opened together. The CoW footer reproduces the one-bit matrix cadence and barcode-like registration beat without copying the BAIBAI mark or guard-bar construction. +- Responsive comparison: `receipt-narrow-compact-latest.png` was reviewed for title wrapping, edge clipping, alignment, and receipt overflow at 331 px. No overlap, wrapping, or horizontal scroll remains. +- Printer-housing comparison: `cow-device-comparison.png` places the 1350 × 200 source crop and the final focused implementation in one image. Both use a device face wider than the receipt, symmetric grilles, a low recessed slot, and a dark lower lip. The implementation intentionally replaces the source's literal wide appliance with a compact CoW-branded terminal appropriate to the existing progress panel. +- Receipt-label comparison: `receipt-centered-label-comparison.png` places the user's label crop and the focused rendered header in one image. The implementation preserves the mono uppercase treatment while centering the label and matching the muted gray used by receipt metadata. +- Success-ornament comparison: `receipt-success-ornaments-comparison.png` places the user's hero crop and the focused rendered receipt in one image. The implementation carries over the pair of tiny celebratory marks and deliberately retains the official CoW head rather than copying the source mascot. +- Success-stamp comparison: `receipt-pixel-check-comparison.png` places the user's 128 × 84 smooth-vector crop beside the final live receipt and an enlarged view of the underlying 12 × 12 grid. The stepped perimeter and three-diagonal knocked-out check are now visible at normal scale; desktop and iPhone SE captures confirm clear separation from the cow and no title crowding. +- Manufacturer-plate comparison: `cashcow-device-plate-comparison.png` normalizes the user's 992 × 164 hardware crop to its likely 1× CSS size and places it above the final live hardware crop. The cow's center point is unchanged, while the new two-line plate occupies only the previously empty space to its right and stays clear of both speaker grilles. +- Motion comparison: `receipt-motion-comparison.jpg` places the user's requested compact checkpoint above five focused rendered states. The sequence visibly progresses from no sheet, to one tear row, to early header feed, to the matching compact success receipt, and finally to the detail body. The same teeth-first and compact states were also captured at 375 × 667 CSS px with no horizontal clipping. +- Bottom-first feed comparison: `receipt-reverse-feed-comparison.jpg` places both supplied Bai Bai states above the revised live sequence. It confirms the same physical reading: the torn leading edge appears first, footer/lower bands follow, and upper receipt bands enter last at the mouth. Desktop geometry measured the 292 px hold with the 830.76 px paper bottom exactly aligned to the reveal bottom; the completed reveal collapses to the paper's intrinsic 830.76 px height with both top edges aligned, so no final blank gap is introduced. +- Printer-slot comparison: `printer-slot-comparison.png` puts the user's icon-and-progress mouth beside the simplified implementation at the same focused scale. The new version preserves the low centered aperture and feed shadow while removing the nested-control appearance. +- Detachable-stub comparison: `receipt-tear-off-comparison-final.png` normalizes the user's delivery-ticket reference and the implemented receipt at the same width. Both use matching inward semicircle notches and a centered light dashed tear guide; the implementation deliberately retains the linked order ID immediately above the guide. +- Earlier thank-you footer comparison: `receipt-thank-you-comparison.png` documents the prior four-tier sign-off. It is retained as iteration history but is superseded by the simplified footer evidence below. +- Simple-footer comparison: `receipt-simple-footer-comparison.png` places the latest Bai Bai crop, the user's pre-simplification CoW crop, and the live simplified footer in one image. The content hierarchy drops from four competing beats to two: one quiet message and one dominant raster mark. The structural dotted divider remains outside that sign-off hierarchy. +- Perforation comparison: `receipt-edge-perforation-comparison.png` places the user's inset-rule crop above the final live cut line. The old 28 px insets leave visible gaps after each circular punch-out; the final 11/10 px tangent offsets visually join the lighter grey dashed rule to both cut edges without drawing through the holes. +- Integrated-slit comparison: `printer-integrated-slit-comparison.png` places the user's ambiguous black-capsule crop above the final live terminal. The post-fix aperture is visibly contained within the hardware face, shorter than the paper-adjacent shell, and uses only a subtle inset shadow; the desktop and iPhone SE captures confirm that the receipt still emerges from directly behind it. +- Theme-surface comparison: the three latest supplied screenshots and `receipt-theme-light-fixed.png`, `receipt-theme-dark-fixed.png`, and `receipt-theme-dark-narrow-fixed.png` were opened in the same comparison input. The light implementation has a continuous grey gutter around the warm-white stock; the dark implementation has no differently colored saw wedges or side panel; the expanded slot remains centered, recessed, and visible at both widths. + +## Comparison history + +1. Initial build lacked a convincing tear edge, staged compact state, and audible fixture replay. The moving saw edge, compact-first feed, and explicit replay control were added. +2. A full-window confetti canvas contended with the layout-bound print animation. Standard completion confetti was removed and the finished fixture was verified with zero canvases. +3. User feedback identified smooth branding and soft audio as a P2 analog-character gap. The header became a one-bit official-icon/matrix lockup and the printer mix became sharper. +4. Latest feedback requested the Bai Bai hierarchy: a large top character, `TRADE SUCCEEDED`, the full mark at the bottom, and the supplied mechanical sound. +5. The hero was rebuilt around a 120 × 80 one-bit official CoW mark; the real Mono Bold font was added; the footer gained the full matrix lockup; the supplied clip was trimmed and phase-aligned to the two-stage feed. +6. The natural compact capture, expanded footer, light-theme state, and 331 px state were rechecked against the sources. No actionable P0/P1/P2 mismatch remains. +7. Latest feedback requested a visible printer device with a central slot and side grilles while avoiding heavy skeuomorphism. The stage was rebuilt as a 520 px CoW terminal with tokenized matte surfaces, symmetric Phosphor perforation icons, an official cow mark, and a slot lip that overlaps the animated sheet. +8. The first narrow preview exposed a fixed 560 px Cosmos wrapper, which shifted and clipped the terminal at 375 px. The fixture wrappers were changed to `min(..., 100%)`; the post-fix iPhone SE capture shows the terminal, both grilles, slot, receipt, title, and saw edge centered with no clipping. +9. Latest feedback identified the right-aligned, full-ink `ORDER RECEIPT` label as a P2 hierarchy mismatch. It was centered and changed to `UI.COLOR_NEUTRAL_40`; the post-fix compact capture and normalized focused comparison confirm the corrected alignment and metadata-level emphasis. +10. Latest feedback requested a success cue around the CoW mark. Two tiny one-bit Phosphor sparkles and a compact outlined check-square were added; desktop and 375 px captures confirm that the cow remains dominant and the title remains clear. +11. The cashier-receipt servo iteration retained the source's sharper, more transient character, split it around the then-current visual hold, and normalized it to match the prior sound's overall level. That 2.650 s timing was superseded by the subsequent teeth-first motion pass. +12. Latest feedback requested a genuine zero-height start, a visible tear-edge-first beat, a print to the supplied compact checkpoint, a pause, and then the full receipt. The reveal now compensates for the 13 px printer overlap, holds one exposed tear row, and uses a 3.4 s audio-synchronized timeline. Desktop and iPhone SE sequence captures show the requested order without content leakage. +13. Latest feedback questioned whether the icon-and-progress element was redundant inside the larger hardware shell. The receipt icon and progress line were removed, and the mouth was reduced to a slim recessed slot. Desktop and iPhone SE captures confirm that the paper still emerges clearly and the printer silhouette remains balanced. +14. Latest feedback proposed a removable top section anchored by the order ID. The first receipt divider became a full-bleed perforation with symmetric semicircle punch-outs and a long-dash guide. The first 375 px capture exposed the `YOU SOLD` label during the compact hold, so the narrow compact checkpoint was reduced from 292 px to 262 px; the post-fix capture contains only the complete detachable stub and no body-copy leakage. +15. Latest feedback requested a compact thank-you ending inspired by a retail barcode receipt. The footer was reordered into `THANK YOU!`, `SEE YOU NEXT SWAP`, the existing thermal CoW Swap lockup, and the muted protocol settlement line. Desktop and 375 px captures confirm that it remains centered, legible, and visually lighter than the transaction details. +16. Latest feedback identified the outlined square as reading like a checkbox and requested a filled, rounded, pixel-print success mark. It was replaced with the existing Phosphor filled circle-check and rendered with crisp-edge geometry at 22 px desktop/20 px narrow. The focused side-by-side comparison and both live viewport captures show a clearer completion stamp with no overlap or wrapping. +17. Latest feedback proposed a fictional hardware-maker label and the real app version beside the centered cow. The first iPhone SE pass inherited the mark's 58% opacity and reduced the text to 6/5 px, making the version too faint (P2). Opacity was separated from the cow, the plate was raised to 72%, and narrow text was increased to 7/6 px. The final desktop and 375 px captures show `CASHCOW SYSTEMS` and live `v3.24.0` legibly, without shifting the centered cow or colliding with the right grille. +18. Latest feedback identified the four-tier footer as a P2 density and authenticity mismatch against the simpler reference. `THANK YOU!`, `SEE YOU NEXT SWAP`, and the protocol caption were replaced by one restrained `THANKS FOR SWAPPING` line; the one-bit lockup was slightly enlarged from 190 to 204 px desktop and 174 to 186 px narrow. The final desktop and iPhone SE captures show two clear visual beats, no wrapping, and substantially more convincing receipt restraint. +19. Latest feedback identified the empty space between the dashed tear guide and the circular edge cuts as a P2 physical-ticket mismatch and asked for greyer ink. The rule offsets were reduced from 28/24 px to the exact 11/10 px notch radii and the ink token changed from neutral-40 to neutral-50. The focused comparison plus desktop and iPhone SE captures confirm that the rule now meets both cuts cleanly without crossing the removed paper areas. +20. Latest feedback identified the thick black capsule below the housing as an ambiguous P2 affordance. The intended paper-exit slot was shortened, moved 3 px inside the device face, flattened to 7/6 px, and stripped of its outline and external shadow. Focused desktop and iPhone SE captures confirm that it now reads as a recessed aperture while preserving the paper-emergence illusion. +21. Latest feedback identified the top-down page reveal as a P2 physical-motion mismatch. The reveal viewport now bottom-aligns a non-shrinking full receipt: the torn edge and footer emerge first, each later band enters at the slot, and the success header arrives last while the final receipt hierarchy remains unchanged. The combined source/implementation comparison plus desktop and iPhone SE sequences confirm the reversed feed direction, intact saw edge, and absence of horizontal clipping. +22. Latest feedback correctly identified that `shape-rendering: crispEdges` did not make the filled SVG circle genuinely pixelated. The vector was replaced by a 12 × 12 one-bit raster derivative of the same Phosphor icon, enlarged with nearest-neighbor rendering and held at an exact 24 px CSS size at both breakpoints. The combined source/live/grid comparison and both viewport captures confirm an obvious stepped silhouette with no blur, overlap, or responsive resampling. +23. Latest feedback identified the hardware identity as visually low and right-heavy because the cow alone was centered while the manufacturer text extended to its right, and both were anchored to the bottom slot stack. The slot was separated from the identity positioning and the cow-plus-label lockup became one intrinsic-width flex group centered at `50% / 50%`. Browser geometry confirms exact shared centers with the hardware at desktop and 375 px, while the grilles, aperture, and receipt origin remain unchanged. +24. Latest feedback identified the centered two-line lockup as still too busy. The live version line and its package import were removed, the official cow mark was reduced from 27 × 18 to 21 × 14 px desktop and from 24 × 16 to 18 × 12 px narrow, and `CASHCOW SYSTEMS` remains as the only label. Browser comparison confirms a quieter single-line silhouette, exact device centering at both widths, and no grille or slot movement. +25. Latest feedback proposed compressing the stacked `YOU SOLD` / down-arrow / `YOU RECEIVED` section into one horizontal exchange lockup. The amount section became a three-column grid and the existing Phosphor down arrow was replaced by its right-arrow sibling. Desktop and iPhone SE captures confirm readable values, centered labels, a clear left-to-right exchange direction, and no wrapping or page overflow. +26. Latest feedback requested a 15 px abbreviated order link. The receipt link token was increased from 11 px to 15 px; the focused source/live comparison and computed browser metrics confirm the requested size, a single line, and no clipping. +27. Latest feedback identified that the 300 px printer slit was narrower than the 392 px receipt. The opening now tracks the paper responsively: it is 414 px wide on desktop and 341 px wide at iPhone SE, extending 11/7 px beyond each paper edge while remaining centered, thin, and overflow-free. +28. Latest feedback identified that the widened slot still read as decoration because the paper began below the full hardware face. The receipt reveal was raised above the device layer and its top moved from 13 px behind the housing to the slit midpoint. Light, dark, and iPhone SE captures now show the paper covering the aperture's lower half while its upper black edge and side overhangs remain visible. +29. Latest feedback identified the footer's one-logical-pixel outlines as too light and digital beside the deposited-ink Bai Bai reference. The existing official CoW-derived raster was thickened in place to 2 × 2 logical ink cells, preserving its intrinsic size and layout. Desktop and iPhone SE captures confirm visibly heavier printed strokes, intact counters, clear cow/registration-bar geometry, and no wrapping or overflow. +30. Latest feedback approved the remaining realism recommendations and requested a real-swap-ready branch, automatic animation/sound, a fixed bottom-left Replay control, and temporary removal of the dense success extras. The new branch `feat/order-receipt-realism` adds the slit contact highlight/shadow, synchronized cutter kick, warmer thermal stock/ink, fixture auto-start plus Replay, and a success-only feature gate around rankings/surplus/share. Live DOM inspection confirms the receipt is the only successful-completion content; focused receipt, sound, failure-state, lint, typecheck, and diff checks pass. +31. Latest feedback exposed three P2 theme-context defects: white stock disappeared into the light modal, painted saw cutouts used the wrong dark color, and too little of the aperture remained visible. The receipt now owns a theme-specific stage surface, uses an alpha saw mask, and exposes a 12/10 px bordered recess. Post-fix browser captures show the light gutter, an exact dark stage/modal color match, clean transparent teeth, and no 375 px overflow. The previously missing hero/footer thermal density bands were also added at a deliberately subtle 2–3% delta. + +## Open question + +- The supplied cashier-servo MP3 contains no embedded author, attribution, copyright, or license metadata. Its macOS provenance points to the Pixabay CDN and Pixabay, but that is not itself a license record. Preserve the valid download/license evidence before this audio asset is merged into the open-source repository. + +## Implementation checklist + +- [x] Large one-bit official CoW hero leads the compact receipt. +- [x] `TRADE SUCCEEDED` uses the real printed mono bold face. +- [x] Full one-bit CoW Swap lockup signs off the footer. +- [x] Moving saw edge remains visible in compact and expanded phases. +- [x] User-supplied printer character is phase-aligned to the animation. +- [x] Production sound remains single-trigger and widget-configurable. +- [x] Standard completion confetti is removed. +- [x] Desktop, narrow, compact, expanded, and theme states are checked. +- [x] Receipt visibly emerges from a wider CoW-themed printer housing. +- [x] Housing depth comes from overlap, one shadow, and tokenized surfaces rather than heavy skeuomorphism. +- [x] Side grilles and centered slot remain intact at 375 px. +- [x] Printer mouth is a slim physical aperture without a nested receipt icon or progress control. +- [x] Success header reads as a detachable order stub with edge notches, a long-dash perforation guide, and the linked abbreviated order ID. +- [x] Narrow compact timing ends immediately below the perforation without exposing amount content. +- [x] Muted grey perforation reaches both circular cut edges at desktop and 375 px. +- [x] Sold and received amounts form one compact horizontal row with a right-pointing printed arrow. +- [x] Horizontal amount columns remain readable and overflow-free at 375 px. +- [x] Abbreviated order link renders at exactly 15 px without wrapping or clipping. +- [x] Printer opening is wider than the receipt at desktop and iPhone SE while remaining centered and recessed. +- [x] Receipt visibly originates at the slot's vertical midpoint in light, dark, and iPhone SE states. +- [x] Receipt ends with one quiet `THANKS FOR SWAPPING` line and the existing one-bit CoW Swap lockup. +- [x] Redundant thank-you, see-you, and settlement-caption layers are removed from the footer. +- [x] `ORDER RECEIPT` is centered and uses the same muted token as detail labels. +- [x] Two printed sparkles and a true 12 × 12 one-bit circle-check make the hero feel celebratory and explicitly successful. +- [x] Completion-stamp pixels remain on an exact 2 × 2 CSS grid at desktop and 375 px. +- [x] Success ornaments fit the desktop and 375 px compact states without crowding or wrapping. +- [x] `CASHCOW SYSTEMS` forms one minimal horizontal manufacturer lockup with the smaller cow; redundant version metadata is removed. +- [x] The complete cow-plus-manufacturer lockup is centered horizontally and vertically in the device face. +- [x] The plate remains legible and clear of both speaker grilles at desktop and 375 px. +- [x] Receipt begins at zero with no white paper visible below the slot. +- [x] One exposed 13 px tear row appears before any receipt copy. +- [x] Footer and lower metadata print first and hold before amounts and the success header enter at the slot. +- [x] The 3.4 s printer sound matches the compact print, silent hold, and full feed. +- [x] Focused tests, lint, typecheck, diff check, and browser interaction checks pass. +- [x] Paper-exit slit is visibly integrated into the hardware face rather than floating beneath it. +- [x] Integrated slit remains centered, recessed, and clear of the receipt at desktop and 375 px. +- [x] Footer CoW Swap mark uses heavier 2 × 2 logical ink blocks while retaining open counters and its original responsive footprint. +- [x] Successful completion renders the receipt without solver rankings, surplus/share content, or the share CTA; cancellation-failed states retain their previous extras. +- [x] Receipt paper and raster ink use restrained thermal variation without compromising token fallbacks or contrast. +- [x] Printer mouth includes a subtle contact highlight and shadow without becoming a floating capsule. +- [x] A reduced-motion-safe 1–2 px cutter kick lands on the final sound phase. +- [x] Finished fixture auto-starts and exposes one fixed bottom-left `Replay` button; production fulfillment remains the single automatic sound source. +- [x] Light modal state keeps a visible grey stage gutter around the warm-white receipt stock. +- [x] Dark stage background exactly matches the surrounding modal surface. +- [x] Saw teeth use alpha transparency and cannot leak a mismatched theme color. +- [x] Printer aperture is visibly deeper at desktop and 375 px without returning to a floating control treatment. +- [x] Hero and footer marks carry two extremely subtle horizontal density variations; all other receipt ink remains uniform. + +final result: passed