react-formbridge
Browse documentation
Interactives Tutorialsv1.0.2

Tutorial: checkout flow

Use checkout when a form has to prove it can handle real business density: contact details, masks, selects, live previews, and saved progress.

  • Masked fields cover card number, expiry, CVV, and internal business codes
  • select() keeps option metadata in the schema for both web and native
  • Draft persistence helps longer flows survive reloads or app restarts
import { field, MASKS, useFormBridge } from '@runilib/react-formbridge'

const checkoutSchema = {
  firstName: field.text('First name').required().trim(),
  lastName: field.text('Last name').required().trim(),
  email: field.email('Email').required().trim().lowercase(),
  phone: field.tel('Phone').required(),
  department: field.select('Department').options([
    { label: 'Sales', value: 'sales' },
    { label: 'Finance', value: 'finance' },
    { label: 'Operations', value: 'ops' },
  ]).required(),
  customerCode: field
    .masked('LL-9999')
    .tokens({ L: /[A-Z]/ })
    .required()
    .showMaskInPlaceholder()
    .uppercase()
    .validateComplete('Complete the customer code.'),
  cardNumber: field.masked(MASKS.CARD_16).required().showMaskInPlaceholder(),
  expiry: field.masked(MASKS.EXPIRY).required().showMaskInPlaceholder(),
  cvv: field.masked(MASKS.CVV).required().showMaskInPlaceholder(),
}

export function CustomerCheckout() {
  const { Form, fields, watchAll } = useFormBridge(checkoutSchema, {
    validateOn: 'onBlur',
    revalidateOn: 'onChange',
    persist: { key: 'customer-checkout', storage: 'local', exclude: ['cvv'] },
  })

  const liveValues = watchAll()

  return (
    <Form onSubmit={async (values) => api.saveCustomer(values)}>
      <fields.firstName />
      <fields.lastName />
      <fields.email />
      <fields.phone />
      <fields.department />
      <fields.customerCode />
      <fields.cardNumber />
      <fields.expiry />
      <fields.cvv />
      <aside>Preview •••• {String(liveValues.cardNumber ?? '').slice(-4)}</aside>
      <Form.Submit>Save customer</Form.Submit>
    </Form>
  )
}

const api = new Proxy({}, {
  get: (_target, methodName) => async (values) => {
    console.log('[doc-playground]', String(methodName), values)
    return { methodName, values }
  },
})

export default CustomerCheckout

Persist the draft

Checkout, onboarding, and support forms are much safer when the user can leave and come back.

  • Use persist.key to scope the draft to the business flow
  • Pick the storage target that matches the platform: local storage on web, async storage on native
  • Add exclude when some values should never be cached locally, such as ephemeral tokens
CheckoutPersistence.tsts
1const form = useFormBridge(checkoutSchema, {
2 persist: {
3 key: 'customer-checkout',
4 storage: 'local',
5 debounce: 250,
6 exclude: ['cvv'],
7 },
8})

Layout without forking the runtime

The shape of the form can change a lot between screens, but the schema does not have to.

  • Put layout concerns in your host components and field wrappers
  • Keep the field semantics in the builders
  • Use generated fields for most inputs, then style or group them in rows, cards, or side panels as needed