Spend Cap Button

Asks for a second click before buying the same item again and shows each purchase against daily or monthly caps before it goes through.

StopsSame item bought twice by mistake

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/spend-cap-button.json

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

What it catches

  • A repeat purchase of the same item within duplicateWindow, including a double-click
  • Purchases that would break a daily or monthly cap, with the overage and reset time
  • This purchase as a slice of each cap
  • Held Enter (auto-repeat) firing a second purchase

Props

SpendCapButton props
PropTypeDefault
amount* (required)

Price of this purchase.

number—
currency

ISO currency code.

string"INR"
locale

Locale for money and times.

string"en-IN"
itemId* (required)

Identifies what is being bought, for duplicate detection.

string—
purchases

Earlier purchases, in any order. Purchases made through this button are remembered too, so a double-buy is caught before the parent refreshes.

SpendPurchase[][]
duplicateWindow

A purchase of the same item within this many ms counts as a likely duplicate.

number3_600_000
caps

Spending caps; a purchase that breaks any of them is blocked.

SpendCap[][]
onPurchase* (required)

Charges. Return a promise to show a pending state; a rejection re-enables the button.

() => void | Promise<void>—
onViewOrder

Shown as "View order" on the duplicate warning.

(orderId: string) => void—
label

Templates: `{amount}`, `{when}`, `{cap}`, `{over}`, `{reset}`, `{left}` and `{after}` are filled in.

string"Pay {amount}"
duplicateText

Shown when the item was bought recently. `{ago}` is filled in.

string"You bought this {when}."
againLabel

Button label asking for the second click.

string"Buy again · {amount}"
blockedText

Shown when a cap would be broken.

string"Blocked: {over} over your {cap}. Resets in {reset}."
capText

Accessible label of a cap meter.

string"{left} left → {after} after"
capOverText

Cap line when this purchase doesn't fit.

string"{left} left · {over} short"
viewOrderLabel

Label of the view-order link.

string"View order"
pendingLabel

Button label while onPurchase runs.

string"Paying…"
doneLabel

Button label after a purchase.

string"Paid"

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

Accessibility

  • aria-disabled when a cap would break, aria-busy while pending, described by a polite hint
  • Escape disarms the second-click state; held Enter is ignored
  • Blocked reason uses role=alert
  • Each cap is a role=meter with value text; animations respect 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: Enforce spending caps and dedupe purchases (idempotency key, recent-purchase check) on the server.

  • Caps and recent purchases come from props; another tab or device isn't seen until you update them
  • The block is UI-only; the purchase API must enforce caps

What it stops

Same item bought twice by mistake

Also known as

buy now · spending limit · budget · in-app purchase · credits · top-up · wallet · duplicate purchase

Field type

Dependencies

Pairs well with