---
title: Payment primitives
description: Use shared checkout UI with your own Core SDK payment flow.
sidebar:
  order: 7
---

Use `LockerversePayment` for the complete checkout. Use these controlled components
when your website needs its own layout and payment flow. The ready-made checkout
uses the same fields and `PaymentSummary`; this entry does not import Stripe or
the ready-made payment controller.

```tsx
import {
  buildCustomFieldAnswers,
  createCustomFieldValues,
  CustomField,
  CustomFields,
  EmailField,
  getCheckoutCustomFields,
  PaymentSummary,
  reconcileCustomFieldValues,
  validateCustomFields,
} from "@lockerverse/react/payment-primitives";
import "@lockerverse/react/payment.css";
```

See the [live composition example](/examples/payment-primitives). It loads an
authoritative quote with the Core SDK and lets a website input and an SDK field
share the same React state. It previews submission fields and does not charge.

## Controlled custom fields

```tsx
const fields = getCheckoutCustomFields(catalog, checkout.items);
const [values, setValues] = useState(() =>
  createCustomFieldValues(fields, { [itemNameFieldId]: "Autographed basketball" })
);
const [errors, setErrors] = useState<Record<string, string>>({});

<CustomFields
  errors={errors}
  fields={fields}
  onChange={setValues}
  values={values}
/>
```

`CustomFields` renders all selected fields. Use `CustomField` for one field at a
time, so you can put your own UI between them. Pass its `field`, `value`,
`onChange`, and optional `error`. The caller owns state; a website input can
change the same state at any time. The components supply labels, control IDs,
required states, and error associations.

Use field IDs as keys. Text uses strings, a single checkbox uses a boolean,
single-choice fields use an option ID, and multiple-choice fields use an array
of option IDs. `createCustomFieldValues` filters initial values against the field
definitions. Call `reconcileCustomFieldValues(fields, values)` when the selected
products or field definitions change. This removes fields and options that no
longer apply. These helpers do not change the input object.

Before submission, call `validateCustomFields`. It returns messages keyed by
field ID for missing required values. Render those messages and focus the first
invalid field. Then use `buildCustomFieldAnswers(fields, values)` for the
Core SDK submission shape. The backend remains the final validation authority.

## Submit through the Core SDK

The UI components do not submit or approve payments. Keep the existing Core SDK
flow: obtain an authoritative quote for the final selection, email, fees, tip,
and promo code; show any changed total; configure Stripe for that quote; and
complete Stripe Elements validation and confirmation-token creation.

At the submission step, add the shared field values to the normal request:

```ts
const fieldErrors = validateCustomFields(fields, values);
if (Object.keys(fieldErrors).length > 0) {
  setErrors(fieldErrors);
  return;
}

// finalCheckout comes from client.calculateTotal() for the final pricing inputs.
// confirmationToken comes from Stripe after Elements validation. Omit it only
// for a free checkout. Include required phone/shipping fields in contactDetails.
const result = await client.submitPayment({
  checkout: finalCheckout,
  submission: {
    ...contactDetails,
    email,
    confirmationToken,
    customFieldAnswers: buildCustomFieldAnswers(fields, values),
  },
});
```

This is the submission step, not a complete payment controller. Keep Stripe
authentication, payment-status recovery, duplicate-submit protection, and
confirmed completion handling in your host flow. A returned result can need
more action; do not show success merely because this promise resolves. If an
error has `recoveryRecommended: true`, call
`client.getPaymentStatus(finalCheckout.paymentReference)` before allowing a
new payment attempt.

## Public components and styles

| Export | Purpose |
| --- | --- |
| `FormField`, `FieldError` | Label, content wrapper, and associated error message |
| `TextField`, `EmailField`, `Checkbox` | Standard controlled form inputs |
| `PhoneInput`, `getInternationalPhone` | Country-aware phone input and normalization before submission |
| `CustomField`, `CustomFields` | Widget-defined text and choice fields |
| `PaymentSummary` | The same order summary used by ready-made checkout |

Each component has a named `...Props` type. `CustomFieldValues` and
`PhoneInputValue` describe the controlled state.

Import `@lockerverse/react/payment.css` and use a `lockerverse-payment` wrapper
with `data-lockerverse-theme="light"` or `"dark"` to retain the checkout
appearance. For a custom design, omit that stylesheet and style the components
in your host. Their labels, input behavior, and error associations remain.

`PaymentSummary` needs only `quote`: `<PaymentSummary quote={checkout} />`.
Its optional `displayedBaseCents`, `displayedTipCents`, and `displayedTotalCents`
properties let a host show pending price changes, as the ready-made checkout
does. They default to the quote's total minus tip, tip, and total. Display
overrides affect the UI only; they never change the payable amount.
