Quote Lock

Shows a held FX, swap or fare quote counting down and makes any worse re-quote an explicit Accept step, so nobody pays a price they never saw.

StopsHelps prevent paying a re-quoted price nobody saw

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/quote-lock.json

The files land in your project. They're yours to edit.

Usage

quote-lock-usage.tsx
import { QuoteLock, type Quote } from "@/components/ui/quote-lock"

<QuoteLock
  label="You pay to send $180.00 to Maya"
  quote={opened}
  tolerance={0.5}
  locale="en-IN"
  onRequote={requote}
  onConfirm={(q) => new Promise<void>((resolve) => setTimeout(() => (setLog(`Paid with quote ${q.id}.`), resolve()), 600))}
/>

What it catches

  • Paying after the hold expired
  • New price worse than tolerance: Pay becomes Accept first
  • Pay pressed in the last seconds: re-quoted
  • Out-of-order quote responses and double presses
  • Client clock skew, when validFor is used

Props

QuoteLock props
PropTypeDefault
quote* (required)

The quote to show. A quote with a new `id` from the parent replaces the current one.

Quote—
onRequote* (required)

Fetch a fresh quote. Called when the hold ends (with `autoRequote`) or the user asks.

() => Quote | Promise<Quote>—
onConfirm* (required)

Pay with this exact quote. Reject to keep the user on the page with an error.

(quote: Quote) => void | Promise<void>—
onQuoteChange

Called with every quote the component adopts.

(quote: Quote) => void—
tolerance

Worse-by-more-than-this percent needs an explicit accept. 0 means any increase does.

number0
direction

"pay": higher is worse. "receive": lower is worse (what the recipient gets).

QuoteDirection"pay"
autoRequote

Fetch a new quote by itself when the hold runs out.

booleantrue
minTimeToPay

A Pay press with less hold than this (ms) gets a fresh quote first, since the payment may land after expiry.

number3000
label

Label above the amount, e.g. "You send".

React.ReactNode—
locale

BCP 47 locale for amounts and times.

string—
messages

Override any visible string.

Partial<QuoteLockMessages>—

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • The amount is always in the Pay label as text
  • Errors use role=alert; Pay uses aria-disabled outside the hold
  • Animations are skipped under reduced motion

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Honour only server-side quote IDs within their validity and reject a confirm against an expired or replaced quote; make confirm idempotent.

  • The server's quote and its expiry are the real truth
  • Tolerance and direction come from your props
  • Animated amounts need @number-flow/react and motion

What it stops

Helps prevent paying a re-quoted price nobody saw

Also known as

quote · fx rate · price lock · fare hold · swap · crypto · checkout · remittance

Field type

Dependencies

Pairs well with