BNPL Schedule Check

Lays out buy-now-pay-later payments on a calendar with paydays and other plans, so an instalment doesn't fail on an expired card or before payday.

StopsInstalment fails on an expired card

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/bnpl-schedule-check.json

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

Usage

bnpl-schedule-check-usage.tsx
import { BnplScheduleCheck, payInN, paydaysEvery, type BnplPlan } from "@/components/ui/bnpl-schedule-check"

<BnplScheduleCheck
  instalments={PLAN}
  currency="USD"
  locale="en-US"
  weekStartsOn={0}
  paydays={PAYDAYS}
  others={OTHERS}
  card={{ label: "Visa •• 4242", expires: "10/26" }}
  bankLabel="checking account"
  lateFee={{ perMissed: 8, capPercent: 25 }}
  onConfirm={async (v) => {
    await new Promise((r) => setTimeout(r, 700))
    setDone(`Plan confirmed, paying from ${v.source === "bank" ? "checking" : "Visa •• 4242"}. Last payment ${v.dates.at(-1)}.`)
  }}
/>

What it catches

  • A card that expires before a scheduled payment
  • Payments due shortly before payday (gap set by payGapDays)
  • Several plans charging in the same week
  • The late fee and its cap, shown in money

Props

BnplScheduleCheck props
PropTypeDefault
instalments* (required)

The plan being offered, payment by payment. `payInN()` builds a pay-in-4.

BnplInstalment[]—
currency* (required)

ISO currency code for every amount.

string—
others

Plans already running, drawn on the same calendar.

BnplPlan[][]
paydays

Paydays, "YYYY-MM-DD". `paydaysEvery()` builds a fortnightly list.

string[][]
card

The card on file. Leave out to pay from the bank only.

BnplCard | null—
bankLabel

Name of the bank-account option.

string"bank account"
lateFee

The provider's late-fee rule, shown in money with its cap.

BnplLateFee—
value

Controlled source and dates.

BnplValue—
defaultValue

Initial source and dates when uncontrolled. Defaults to the card (if any) and the plan's dates.

BnplValue—
onValueChange

Called when the payment source or a payment date changes.

(value: BnplValue) => void—
onUpdateCard

Offer "Update card" next to the expiry warning.

() => void—
onConfirm

Called with the final schedule. May return a Promise; the button shows a pending state.

(value: BnplValue) => void | Promise<unknown>—
payGapDays

Flag payments this many days or fewer before payday. Default 3.

number—
maxShiftDays

Furthest a payment may be moved later. Default 7.

number—
firstPaidNow

The first payment is taken at checkout: it is labelled "today" and never offered a move. Default true.

booleantrue
weekStartsOn

First weekday of the calendar: 0 Sunday, 1 Monday. Default 1.

0 | 11
locale

BCP 47 locale for dates and money.

string—
messages

Override any visible string.

Partial<BnplScheduleCheckMessages>—

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

Accessibility

  • The card problem is announced with role="alert"
  • Payment source is a role="radiogroup" with arrow-key navigation
  • Confirm sets aria-busy while pending; status is announced in a role="status" region
  • Issues use text and icons; motion respects prefers-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: The provider's schedule, fees and card on file are the source of truth; re-check them on the server before confirming.

  • Paydays, other plans, the card expiry and the late-fee rule all come from your props
  • Moving a payment only changes the proposed date; the provider must allow it

What it stops

Instalment fails on an expired card

Also known as

buy now pay later · bnpl · pay in 4 · klarna · afterpay · affirm · instalments · installment plan

Dependencies

Pairs well with