From fb96a17009886cb12bdf9f8b48c831486e575451 Mon Sep 17 00:00:00 2001 From: 0xDeon <110722148+0xDeon@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:44:24 +0100 Subject: [PATCH 1/2] feat(dapp): give the swap and step cards the payout card's look The swap card now leads with the venue's logo and a status, shows the amounts with token icons in a panel, the rate as a row, and folds the risks away once sent. The step list becomes the payout card's progress bars. A chain's own coin is drawn with the chain's mark. --- .../intents/chain-action-confirm.tsx | 244 ++++++++++++++---- .../intents/chain-sequence-steps.tsx | 79 +++--- apps/dapp/components/ui/token-icon.tsx | 23 ++ apps/dapp/e2e/chain-action.spec.ts | 3 +- 4 files changed, 268 insertions(+), 81 deletions(-) diff --git a/apps/dapp/components/intents/chain-action-confirm.tsx b/apps/dapp/components/intents/chain-action-confirm.tsx index b3991caa..041ed7f0 100644 --- a/apps/dapp/components/intents/chain-action-confirm.tsx +++ b/apps/dapp/components/intents/chain-action-confirm.tsx @@ -1,13 +1,16 @@ 'use client' -import { Button, Card } from '@intent/ui' -import { ExternalLink, Loader2 } from 'lucide-react' +import { Button, cn } from '@intent/ui' +import { ArrowRight, ChevronDown, ExternalLink, Loader2 } from 'lucide-react' +import { useState } from 'react' import type { ChainAction } from '../../hooks/use-chain-action' import { stepErrorMessage } from '../../lib/agents/chain-race' import { limitSettledNote } from '../../lib/chains/orders-view' import { useChain } from '../../providers/chain-provider' import type { PreparedAction } from '../../lib/chains/types' +import { TokenIcon } from '../ui/token-icon' +import { toolCard, toolPanel } from './bubble' function cardLine({ kind, pay, venueName }: PreparedAction): string { const what = `${pay.amount} ${pay.symbol}` @@ -34,6 +37,98 @@ function cardLine({ kind, pay, venueName }: PreparedAction): string { } } +const VERB: Partial> = { + swap: 'Swap', + supply: 'Deposit', + stake: 'Stake', + limit: 'Limit order', + withdraw: 'Withdraw', + unstake: 'Unstake', + borrow: 'Borrow', + repay: 'Repay', + stream: 'Stream', + invoice: 'Invoice', + pay_invoice: 'Pay invoice', + claim: 'Claim', + cancel: 'End stream', +} + +function cardTitle({ kind, venueName }: PreparedAction): string { + if (kind === 'send') return 'Send' + return `${VERB[kind] ?? 'Pay'} on ${venueName}` +} + +/** Kinds whose sentence says what the amounts alone don't: a debt, a commitment, nothing moved. */ +const SAYS_MORE: ReadonlySet = new Set([ + 'borrow', + 'stream', + 'invoice', + 'cancel', +]) + +/** What one unit of the paid asset fetches, for a swap between two different assets. */ +function rateLine({ pay, receive }: PreparedAction): string | undefined { + if (receive === undefined || receive.symbol === pay.symbol) return undefined + const paid = Number(pay.amount) + const got = Number(receive.amount) + if (!(paid > 0) || !(got > 0)) return undefined + const each = got / paid + const shown = Number(each >= 1 ? each.toFixed(4) : each.toPrecision(4)) + return `${receive.approximate ? '≈ ' : ''}${shown} ${receive.symbol} per ${pay.symbol}` +} + +const short = (hash: string): string => `${hash.slice(0, 6)}…${hash.slice(-4)}` + +/** The venue's own logo, or its initial when there is none. */ +function VenueMark({ id, name }: { id: string; name: string }): JSX.Element { + const [failed, setFailed] = useState(false) + if (failed) { + return ( + + {name.charAt(0)} + + ) + } + return ( + // eslint-disable-next-line @next/next/no-img-element + setFailed(true)} + className="ring-border/60 h-10 w-10 shrink-0 rounded-full object-cover ring-1" + /> + ) +} + +function Amount({ + amount, + symbol, + approximate = false, +}: { + amount: string + symbol: string + approximate?: boolean +}): JSX.Element { + return ( + + + + {approximate ? ≈ : null} + {amount} {symbol} + + + ) +} + +function statusLine(phase: ChainAction['phase'], canContinue: boolean): string { + if (phase === 'settled') return canContinue ? 'Done. One more signature finishes this.' : 'Sent' + if (phase === 'signing') return 'Waiting for your wallet' + if (phase === 'submitting') return 'Sending' + return 'Review, then sign in your wallet' +} + /** Review, sign and result for an action on a chain other than Stellar. */ export function ChainActionConfirm({ action, @@ -50,9 +145,10 @@ export function ChainActionConfirm({ const { slug } = useChain() const error = stepErrorMessage(action.error, sequenceSettled, typedStep) const busy = phase === 'signing' || phase === 'submitting' + const rate = prepared !== undefined ? rateLine(prepared) : undefined return ( - +
{phase === 'building' ? (

- {prepared.summary} - - {cardLine(prepared)} - {prepared.receive !== undefined - ? `, and receive ${prepared.receive.approximate ? 'about ' : ''}${prepared.receive.amount} ${prepared.receive.symbol}` - : ''} - - {prepared.apy !== undefined ? ( - - Current rate {prepared.apy.basePercent.toFixed(2)}% - {prepared.apy.rewardPercent !== undefined - ? `, plus ${prepared.apy.rewardPercent.toFixed(2)}% in rewards that can end` - : ''} - . Rates change. +
+ +
+ {cardTitle(prepared)} + + {statusLine(phase, action.canContinue)} +
+
+ +
+ + {prepared.receive !== undefined ? ( + <> + + + ) : null}
+ {prepared.receive === undefined || SAYS_MORE.has(prepared.kind) ? ( +

+ {prepared.receive === undefined ? prepared.summary : `${cardLine(prepared)}.`} +

+ ) : null} + + {rate !== undefined || prepared.apy !== undefined ? ( +
+ {rate !== undefined ? ( +
+
Rate
+
{rate}
+
+ ) : null} + {prepared.apy !== undefined ? ( +
+
Current rate
+
+ {prepared.apy.basePercent.toFixed(2)}% + {prepared.apy.rewardPercent !== undefined + ? ` + ${prepared.apy.rewardPercent.toFixed(2)}% rewards that can end` + : ''} +
+
+ ) : null} +
+ ) : null} + {prepared.notes.length > 0 ? ( -
    - {prepared.notes.map((note) => ( -
  • {note}
  • - ))} -
+
+ + {phase === 'settled' ? 'What you agreed to' : 'Before you sign'} + +
    + {prepared.notes.map((note) => ( +
  • +
  • + ))} +
+
) : null} {phase === 'review' && earlier !== undefined ? ( @@ -127,7 +283,7 @@ export function ChainActionConfirm({ rel="noreferrer" className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs underline underline-offset-2" > - {`${earlier.hash.slice(0, 6)}…${earlier.hash.slice(-4)}`} + {short(earlier.hash)}