# `@lockerverse/react`

Tree-shakable React UI for Lockerverse payments, signup, auctions, and public Events. The Core SDK is installed automatically as a normal package dependency.

This README ships with the package and describes that release. For an installed
project, use its local README, `package.json` exports, and public `.d.ts` files.
Online examples can describe a newer release. Do not import private `dist`
modules or copy API details from a different SDK version.

## Usage guide

- [Install](#install) and [payment quick start](#use)
- [Configuration and environment](#configuration-and-environment)
- [Server rendering](#server-rendering) and [resource identity](#resource-identity)
- [Custom product UI](#custom-product-ui) and [live availability](#live-product-availability)
- [Payment primitives](#payment-primitives) for custom Core SDK checkout
- [Donate launcher](#donate-launcher)
- [Signup](#signup)
- [Styling](#styling)
- [Payment outcomes](#payment-outcomes) and [recovery](#payment-recovery)
- [Auctions](#auctions) and [direct auction pages](#direct-auction-pages)
- [Public event listings](#public-event-listings)

For custom non-React clients, use the README shipped with `@lockerverse/sdk`.

## Install

```sh
npm install @lockerverse/react react react-dom
```

For payment UI, also install the optional Stripe peers:

```sh
npm install @stripe/react-stripe-js @stripe/stripe-js
```

## Use

```tsx
import {
  LockerversePayment,
  type LockerversePaymentCompletion,
  type LockerversePaymentSelectionItem,
} from "@lockerverse/react/payment";
import "@lockerverse/react/payment.css";
import { useLockerverseWidget } from "@lockerverse/react/widget";

export function Checkout() {
  const widget = useLockerverseWidget({
    communitySlug: "auburn",
    widgetSlug: "tailgate-party",
  });
  return (
    <LockerversePayment
      metadata={{ source: "community-site" }}
      onPaymentComplete={(payment: LockerversePaymentCompletion) => {
        console.info("Payment complete", payment.checkoutId);
      }}
      onPaymentUncertain={({ paymentReference, reason }) => {
        console.info("Payment needs status recovery", paymentReference, reason);
      }}
      selection={[{ productSlug: "adult", quantity: 1 }]}
      theme="dark"
      widget={widget}
    />
  );
}
```

The original `@lockerverse/react` and `@lockerverse/react/styles.css` payment imports remain supported.

## Resource identity

A mounted hook or widget keeps one resource identity. Do not change its API,
environment, community slug, widget slug, or signup slug. To show a different
resource, remount the owning component with a new React `key`:

```tsx
<Checkout
  key={`${communitySlug}:${widgetSlug}`}
  communitySlug={communitySlug}
  widgetSlug={widgetSlug}
/>
```

Selection, theme, callbacks, and other view properties can change without a
remount. `refresh()` reloads the same resource.

## Custom Product UI

Use `useLockerverseWidget` to build custom Product presentation from the live
Lockerverse catalog. This entry point does not import the payment UI or Stripe.

```tsx
import {
  LockerversePayment,
  preloadLockerversePayment,
  type LockerversePaymentSelectionItem,
} from "@lockerverse/react/payment";
import "@lockerverse/react/payment.css";
import { useLockerverseWidget } from "@lockerverse/react/widget";
import { useState } from "react";

const widgetOptions = {
  communitySlug: "auburn",
  widgetSlug: "tailgate-party",
};

export function CustomProductCheckout() {
  const widget = useLockerverseWidget(widgetOptions);
  const [selection, setSelection] =
    useState<LockerversePaymentSelectionItem | null>(null);

  if (widget.loading) {
    return <p>Loading checkout...</p>;
  }
  if (widget.error) {
    return <button onClick={widget.refresh}>Try again</button>;
  }
  if (!widget.catalog) {
    return null;
  }
  if (selection) {
    return (
      <LockerversePayment
        selection={[selection]}
        widget={widget}
      />
    );
  }

  return widget.catalog.products
    .filter((product) => product.pricingMode === "fixed")
    .map((product) => (
      <button
        key={product.id}
        onFocus={() => {
          void preloadLockerversePayment(widget.catalog);
        }}
        onClick={() =>
          setSelection(
            product.paymentOption === "one-time"
              ? { productSlug: product.slug, quantity: 1 }
              : { productSlug: product.slug }
          )
        }
        onPointerEnter={() => {
          void preloadLockerversePayment(widget.catalog);
        }}
      >
        {product.name}
      </button>
    ));
}
```

The hook returns immutable `catalog` data together with `loading`, `error`, and
`refresh`. The host owns presentation and selection state. Pass the
selected Product slugs to `LockerversePayment`; Lockerverse remains
authoritative for availability, pricing, and payment.

Pass the hook result to `LockerversePayment` with `widget={widget}`. The result
contains the loaded catalog and its client, so the component requests
only the authoritative quote without repeated configuration. The host can show
a local catalog total before checkout. The component uses the same local
estimate to mount the payment form and Stripe immediately, then replaces it
with the authoritative quote in the background. Payment confirmation still
requires the current authoritative quote.

This small example shows fixed-price Products. For a custom-price Product, ask
for the amount in the host UI and pass `amountCents` with `productSlug`.

For a monthly or annual Product, omit quantity. The component shows the
cadence and configures Stripe Elements for a subscription:

```tsx
<LockerversePayment
  selection={[{ productSlug: "monthly-supporter" }]}
  widget={widget}
/>
```

Recurring checkout accepts one fixed Product. It does not accept quantities,
custom amounts, or multiple Products.

### Live product availability

Use the live `widget.catalog` for names, prices, and limits. Find the requested
product by its `listingId` or `slug`; do not keep a separate static price list.
Amounts are integer cents. Format them with `catalog.payment.currency`, not a
currency field on the product.

Before enabling a new purchase, check the selected product's `minimumQuantity`,
`maximumQuantity`, and `inventoryQuantity`; nullable limits mean no limit.
For custom amounts, use `getLockerverseMinimumCustomAmountCents(product)` from
`@lockerverse/sdk/checkout` to include the SDK minimum and the product minimum.
Also check:

- The matching `catalog.availability.listings` entry: `status`,
  `maxPurchasableQuantity`, and `remainingQuantity`.
- Each `catalog.availability.sharedLimits` entry whose `listingIds` includes
  the selected product's listing: `status` and `remainingQuantity`. Add the
  quantities of all selected listings in that shared group before checking its
  remaining quantity.

A missing listing availability entry means no listing-specific limit was
reported. Other product and shared limits still apply. These checks help the
host show an unavailable state; the authoritative quote decides whether the
selection can be purchased. Show loading, retry, and empty states. Keep an
unresolved payment available for recovery even if a new purchase is unavailable.

If an existing button already selects one product, open its checkout directly.
Use the donate launcher when the visitor needs a product picker. For a custom
dialog, use an accessible title, close button, focus handling, and mobile scroll
area. Keep recovery state in an owner that remains mounted when the dialog
closes. Closing a dialog does not complete or cancel a payment.

## Donate launcher

`LockerverseDonateLauncher` turns an existing payment widget into a native
React dialog. The launcher selects its controls from each Product's existing
capabilities. Visitors can set quantities for fixed one-time Products and add
an inline amount for custom one-time Products. The launcher combines these
one-time choices in one checkout. Monthly and annual Products are exclusive:
the visitor selects one tier before the same `LockerversePayment` form opens in
subscription mode.

```tsx
import { LockerverseDonateLauncher } from "@lockerverse/react/donate-launcher";
import { useLockerverseWidget } from "@lockerverse/react/widget";
import "@lockerverse/react/payment.css";

export function Donate() {
  const widget = useLockerverseWidget({
    communitySlug: "auburn",
    widgetSlug: "general-donations",
  });
  return (
    <LockerverseDonateLauncher
      buttonLabel="Donate"
      position="bottom-right"
      widget={widget}
    />
  );
}
```

Use `position="inline"` to place the launcher in the normal page layout. Use
`bottom-left` or `bottom-right` for a fixed launcher. Payment callbacks,
branding, themes, metadata, and recovery properties are the same as
`LockerversePayment`. API and transport options belong to
`useLockerverseWidget`.

The launcher starts loading Stripe when the visitor opens it. Catalog-only
pages do not load Stripe.

## Signup

The signup entry does not import payment or Stripe code.

```tsx
import {
  LockerverseSignup,
  useLockerverseSignup,
} from "@lockerverse/react/signup";
import "@lockerverse/react/signup.css";

export function Signup() {
  const widget = useLockerverseSignup({
    communitySlug: "auburn",
    signupSlug: "tailgate-guests",
  });
  return (
    <LockerverseSignup
      branding={{
        accentColor: "#5751f2",
        imageUrl: "https://cdn.example.com/community.png",
        name: "Auburn",
      }}
      onSignupComplete={(submission) => {
        console.info("Signup complete", submission.id);
      }}
      widget={widget}
    />
  );
}
```

Pass `googlePlacesApiKey` to enable US address suggestions when the signup asks for an address. Manual address entry always remains available.

## Configuration and environment

Production is the default. Payment, signup, auction, and Events hooks accept
`apiBaseUrl` and `environment` together. A custom URL requires an explicit
environment; `environment: "development"` also requires a URL. Include `/api`
in the API base URL. Do not infer the environment from the host website's URL.
Use the public community and resource slugs from the intended integration.
Signup uses `signupSlug`; payment uses `widgetSlug`.

The backend supplies the Stripe publishable key and connected account. Do not
hard-code either value or pass a secret key. The default Stripe loader handles
initialization. If a host supplies `stripeLoader={loadStripe}`, preserve all
loader arguments, including the connected-account options.

```tsx
const widget = useLockerverseWidget({
  apiBaseUrl: "https://portal-dev.lockerverse.com/api",
  communitySlug: "luis-c",
  environment: "development",
  widgetSlug: "new-payment-widget",
});

<LockerversePayment
  selection={[{ productSlug: "adult", quantity: 1 }]}
  widget={widget}
/>
```

## Server rendering

Load public data with the matching core SDK client in a server route or loader,
then pass that validated snapshot to the React hook. The second argument is
optional; without it, the hook keeps its loading state until the browser read
completes. The seed is read once on mount and is retained if revalidation
fails. A later seed prop change does not replace live data. Remount with a new
React `key` when the resource identity changes.

```tsx
import { createLockerversePublicEventsClient } from "@lockerverse/sdk/public-events";

// Call this from a server route or loader.
export async function loadPublicEvents() {
  return createLockerversePublicEventsClient({ communitySlug: "auburn" }).list();
}
```

```tsx
import type { LockerversePublicEvent } from "@lockerverse/sdk/public-events";
import { useLockerversePublicEvents } from "@lockerverse/react/public-events";

export function CommunityEvents({ initialEvents }: { initialEvents: LockerversePublicEvent[] }) {
  const events = useLockerversePublicEvents(
    { communitySlug: "auburn" },
    { initialEvents }
  );
  return <h1>{events.events?.[0]?.name ?? "Events"}</h1>;
}
```

Payment uses `createLockerverseCheckoutClient(options).load()` on the server
and `useLockerverseWidget(options, { initialCatalog })` in the React component.
Signup uses `createLockerverseSignupClient(options).load()` and
`useLockerverseSignup(options, { initialSignup })`. Use matching options on
both sides. An empty `initialEvents: []` is valid. Use the exact public SDK
client result, not unvalidated server JSON or a stored payment quote. Do not
put personal information, secret keys, or payment recovery data in the seed.
The browser still requests current public data after mount. A failed refresh
retains the last good snapshot and exposes `error` and `refresh`; loading means
a read is in progress, not that the snapshot is unavailable. Signup and event
content remain visible during refresh. Payment shows the seeded catalog heading
and public base product prices, but keeps submit controls behind a loading state
until the catalog refresh finishes. Payment quotes remain authoritative from
the checkout client. A refreshed signup definition removes obsolete custom
field choices from the form and submission while preserving valid answers.

In Next.js, use a request-local server loader (for example, `React.cache`) to
avoid duplicate reads in one request. Use `no-store` for changing public
auction data and allow the auction hook to refresh after mount. A static site
can refresh after hydration but cannot produce item-specific first-response
metadata without a build or server response for each item.

Keep browser storage and payment recovery behind a client-only boundary where
the host needs one. Keep those accesses in effects, not at module scope or
during server rendering. For TanStack Router, import `ClientOnly` from
`@tanstack/react-router` and put the recovery owner and checkout interaction
inside it:

```tsx
import { ClientOnly } from "@tanstack/react-router";

<ClientOnly fallback={<p>Loading checkout...</p>}>
  <Checkout />
</ClientOnly>
```

The host supplies this boundary; TanStack Router is not an SDK dependency.
Public catalog and item content can remain outside it. Wait for saved recovery
state to load before mounting payment. Remount the recovery owner when its
resource or recovery key changes.

## Styling

Auction checkout, payment, and signup use common email, phone, field-error, and checkbox controls. Payment and signup share the custom-field renderer. Phone fields use the lightweight payment/signup country list, normalization, and Valibot format validation. The shared phone control is imported directly, with no separate phone chunk or metadata library. Each flow keeps its existing field configuration and backend rules.

Payment and auction Stripe forms resolve the rendered SDK font and theme tokens before passing them to Stripe. Custom font files are not loaded into Stripe by a font family name alone. See [the shared form foundation](https://github.com/Lockerverse/lockerverse-sdk/blob/main/docs/shared-form-foundation.md) for module boundaries and bundle measurement.

Use `theme="dark"` or `theme="light"` to match the host background. The outer payment, signup, auction, and Events widgets are transparent, so the host controls their canvas. Lockerverse applies the community branding returned by the widget API. Pass the same `LockerverseStyle` object to payment, signup, and donate launcher components.

```tsx
import type { LockerverseStyle } from "@lockerverse/react";

const lockerverseStyle = {
  "--lockerverse-accent": "#ff6b35",
  "--lockerverse-bg": "#fffaf0",
  "--lockerverse-control": "#f5eddf",
  "--lockerverse-radius": "14px",
  "--lockerverse-text": "#211c17",
} satisfies LockerverseStyle;

<LockerversePayment
  selection={selection}
  style={lockerverseStyle}
  widget={widget}
/>
```

The canonical `--lockerverse-*` tokens control every component. Existing
`--lockerverse-signup-*` tokens remain supported as signup-only aliases.

The package keeps React, React DOM, and Stripe's React libraries as peer dependencies so it does not ship duplicate framework code. `LockerversePayment` leaves product selection to its host. `LockerverseDonateLauncher` provides selection from the existing widget catalog. Neither component depends on Lockerverse's Next.js application components.

## Payment outcomes

`onPaymentComplete` runs only after Lockerverse reports `success`. A successful payment is terminal, even if the host callback throws.

A direct completion includes the normalized customer `email` and authoritative
`lineItems`. A completion recovered after reload has `null` for both fields.

If the authoritative total, discount, currency, line items, or connected Stripe account changes, the component displays the new quote before creating a Stripe token. The customer must explicitly submit again.

An authoritative `failed` result is retryable and the next submit receives a new payment reference. `pending`, unresolved `action_required`, and failed same-reference recovery stay locked so the customer cannot accidentally create a second payment. The optional `onPaymentUncertain` callback receives the retained `paymentReference` and one of these reasons:

- `pending`
- `action_required`
- `recovery_failed`

To abandon a locked attempt and intentionally start a new checkout, the host must remount `LockerversePayment` with a new React `key` after resolving the payment status through its own workflow.

### Payment recovery

Persist `onPaymentRecoveryChange` synchronously before navigation and pass the
saved value back through `resumePayment` after a refresh. Do not defer the save
to an effect. A recovery record has only `paymentReference: string` and
`connectedAccountId: string | null`; it contains no Stripe secret. Scope its
storage key to the environment, community, widget, and selection.

This hook reads storage after mount, validates the saved record, and keeps an
in-memory record if storage is unavailable:

```tsx
import { useEffect, useState } from "react";
import type { LockerversePaymentRecovery } from "@lockerverse/react/payment";

function usePaymentRecovery(recoveryKey: string) {
  const [recovery, setRecovery] = useState<LockerversePaymentRecovery | null>(null);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    try {
      const raw = sessionStorage.getItem(recoveryKey);
      const saved: unknown = raw ? JSON.parse(raw) : null;
      if (
        saved !== null && typeof saved === "object" &&
        "paymentReference" in saved && typeof saved.paymentReference === "string" &&
        "connectedAccountId" in saved &&
        (saved.connectedAccountId === null || typeof saved.connectedAccountId === "string")
      ) {
        setRecovery({
          paymentReference: saved.paymentReference,
          connectedAccountId: saved.connectedAccountId,
        });
      }
    } catch {
      // Invalid or unavailable storage must not break checkout.
    }
    setLoaded(true);
  }, [recoveryKey]);

  function saveRecovery(next: LockerversePaymentRecovery | null) {
    setRecovery(next);
    try {
      if (next) sessionStorage.setItem(recoveryKey, JSON.stringify(next));
      else sessionStorage.removeItem(recoveryKey);
    } catch {
      // Keep the in-memory record if storage is unavailable.
    }
  }

  return { loaded, recovery, saveRecovery };
}
```

Use it in the payment owner, keyed by the recovery key. Render checkout only
after `loaded` is true:

```tsx
const { loaded, recovery, saveRecovery } = usePaymentRecovery(recoveryKey);

return loaded ? (
  <LockerversePayment
    selection={selection}
    resumePayment={recovery}
    onPaymentRecoveryChange={saveRecovery}
    widget={widget}
  />
) : <p>Loading checkout...</p>;
```

Retain the record on `onPaymentUncertain` and let the component recover the
same payment. Do not create a replacement charge for an unknown result.
`onPaymentComplete` confirms success; its `paymentRequired` distinguishes a
paid purchase from a no-charge order. This browser callback does not grant paid
access; access checks require server verification. Keep private contact and
payment data out of URLs, analytics, logs, and public metadata.

Malformed host selections and invalid custom tips render customer-safe validation messages and do not call the Lockerverse API. Product selection remains owned by the host application.

## Auctions

For a page containing one auction, use `LockerverseAuction` from
`@lockerverse/react/auction` and `@lockerverse/react/auction.css`. Its header,
filters, and lineup can be hidden independently; `renderItem` can keep the
built-in card or supply a custom card. See the [auction composition reference](https://sdk.lockerverse.com/react/auctions)
and [complete example](https://sdk.lockerverse.com/examples/auctions).
The [State Lab](https://sdk.lockerverse.com/examples/state-lab) provides visibly
labelled local simulations of bid uncertainty and purchase recovery.

Browse auctions and open the built-in item checkout. The catalog entry does not
load Stripe. The checkout entry uses the optional Stripe peers listed above.
Import `auctions.css` for both components. Built-in auction checkout accepts
cards only. The core SDK supports the backend confirmation-token flows for
custom payment UIs.

```tsx
import { useState } from "react";
import type { LockerverseAuction } from "@lockerverse/sdk/auctions";
import { LockerverseAuctions, useLockerverseAuctions } from "@lockerverse/react/auctions";
import { LockerverseAuctionItem } from "@lockerverse/react/auction-item";
import "@lockerverse/react/auctions.css";

export function AuctionPage() {
  const auctions = useLockerverseAuctions({ communitySlug: "auburn" });
  const [selected, setSelected] = useState<{
    auction: LockerverseAuction;
    itemId: string;
  } | null>(null);
  const [locked, setLocked] = useState(false);
  const [message, setMessage] = useState("");

  return <>
    <div hidden={Boolean(selected)}>
      <LockerverseAuctions
        auctions={auctions}
        onSelectItem={(auction, item) => {
          if (!locked) setSelected({ auction, itemId: item.id });
        }}
        theme="light"
      />
    </div>
    {selected && <LockerverseAuctionItem
      key={`${selected.auction.id}:${selected.itemId}`}
      auction={selected.auction}
      client={auctions.client}
      itemId={selected.itemId}
      onBack={locked ? undefined : () => setSelected(null)}
      onInteractionLockChange={setLocked}
      onBidSubmitted={() => { setMessage("Bid submitted; this is not a winning bid."); auctions.refresh(); }}
      onPurchaseComplete={() => { setMessage("Payment complete."); auctions.refresh(); }}
      onUncertain={(kind) => setMessage(`The ${kind} result is unknown. Keep this page open; do not submit another bid.`)}
      theme="light"
    />}
    <p role="status">{message}</p>
  </>;
}
```

Production is the default. For development, set both `apiBaseUrl` and
`environment: "development"` as shown in [configuration](#configuration-and-environment). Auction data supplies the
publishable key and connected account for Stripe initialization. The backend
must include the auction `payment` configuration to use built-in checkout.

Both components accept `theme` and the existing `LockerverseStyle` CSS-variable
object. Use the hook data for your own item cards and open `LockerverseAuctionItem`
for checkout. No general headless workflow API is required.

`onBidSubmitted` means the bid request and any required Stripe action completed;
it does not mean the bid won or a charge occurred. `onPurchaseComplete` runs only
when Lockerverse reports `paid`. `onUncertain` means the result remains unknown.
Keep that state visible and use the checkout recovery action for a purchase.
Status checks call `client.getPurchaseStatus` without creating another payment;
the backend can reconcile Stripe status and send the purchase notification.
A `null` status lookup is not proof that an original in-flight request ended.
Any retry must keep the original `clientRequestId` and purchase details.
If a bid result is unknown, block a new bid and keep the support message visible.
Connect `onInteractionLockChange` to all host navigation, resource selectors, and
reset controls. A true lock covers submission and unresolved outcomes. Keep host
callback state above checkout. If checkout is unexpectedly removed after send,
known completion or uncertainty is still reported once for that operation;
no new Stripe action starts after unmount. Full page reload or browser shutdown
cannot guarantee callback delivery.
Never automatically retry a bid: the backend has no public bid recovery API.

For a host that needs recovery after navigation, connect
`onPurchaseRecoveryChange` and later pass its record as `resumePurchase`:

```tsx
<LockerverseAuctionItem
  auction={auction}
  client={client}
  itemId={itemId}
  onPurchaseRecoveryChange={savePrivateRecoveryRecord}
  resumePurchase={previousRecoveryRecord}
/>
```

The host owns storage. A recovery record captures the original item, payment
configuration, pricing, buyer contact data, and Stripe confirmation token. Keep it private; do not put it in URLs, analytics, logs, or
shared caches. Clear the stored record when the callback supplies `null`.
Resume only with the same auction, item, client, and payment environment. Keep
the original `clientRequestId` and purchase details. The public example stores
recovery in memory only and asks the customer to keep the page open.

The raw receipt endpoint returns amounts, not payment status. Do not use a
receipt response alone to show a payment as complete.

### Direct auction pages

Import `useLockerverseAuction` from `@lockerverse/react/auction-data` and call
`useLockerverseAuction({ communitySlug }, auctionSlug)` to load one auction.
This uses the detail endpoint; it never fetches the full community list.
Render `LockerverseAuction` from `@lockerverse/react/auction` with the result,
and import `@lockerverse/react/auction.css`. Use `auction-item` and its stylesheet
when opening built-in item details and checkout.

The singular hook returns `client`, `auction`, `loading`, `error`, and `refresh`.
Refresh retains the last good data. Keep checkout mounted across refresh and
errors. The item component applies new same-ID data while idle and preserves
its payment snapshot while payment is open. Connect `onInteractionLockChange`
to host navigation. Resource identity changes require a new React key.

[Complete direct-auction example](https://sdk.lockerverse.com/examples/auctions?community=lckrvrs-internal&auction=auction).
Built-in styled UI is the default for Lovable. There is no automatic headless switch.

### Initial server data

Pass a complete SDK snapshot to the React hook when a server route has already
loaded it. The plural hook accepts `initialAuctions` in its second argument;
the singular hook accepts `initialAuction` in its third argument.

```tsx
import type { LockerverseAuction } from "@lockerverse/sdk/auctions";
import {
  useLockerverseAuction,
  useLockerverseAuctions,
} from "@lockerverse/react/auction-data";

function useServerAuctions(
  communitySlug: string,
  initialAuctions: LockerverseAuction[]
) {
  return useLockerverseAuctions(
    { communitySlug },
    { initialAuctions }
  );
}

function useServerAuction(
  communitySlug: string,
  auctionSlug: string,
  initialAuction: LockerverseAuction
) {
  return useLockerverseAuction(
    { communitySlug },
    auctionSlug,
    { initialAuction }
  );
}
```

The seed is read once on mount. An empty `initialAuctions: []` is valid data.
The seed must belong to the requested community; a singular seed with a
different auction slug throws a `TypeError`. Later seed prop changes do not
replace live data. The hook does not mutate the supplied snapshot.

The first browser request still runs, and `loading` remains true while it is
pending. Render available data during loading; show an initial skeleton only
when data is null. A failed refresh retains the seed or last successful data.
Keep checkout mounted during refresh and errors. `refreshOnReturn` remains
available in the same argument. The named `UseLockerverseAuctionOptions` and
`UseLockerverseAuctionsOptions` types are exported from `auction-data`; the
plural options type is also exported from `auctions`.

## Public event listings

Use the public Events entry to show publicly listed Events that are happening
now, start in the future, or have ended. It does not load ticket or payment UI.

```tsx
import {
  LockerversePublicEvents,
  useLockerversePublicEvents,
} from "@lockerverse/react/public-events";
import "@lockerverse/react/public-events.css";

function CommunityEvents() {
  const events = useLockerversePublicEvents({ communitySlug: "auburn" });
  return <LockerversePublicEvents events={events} title="Events" theme="light" />;
}
```

The hook returns `client`, `events`, `loading`, `error`, and `refresh`. The
component takes the full hook result so it can show loading, empty, and retry
states. `title` sets the visible heading, `showHeading` can hide it, and
`timeZone` controls date display. The default time zone is
`America/New_York`. Use `style` with `LockerverseStyle` tokens or `className`
to match the host site. Links open in a new tab when they use HTTP or HTTPS.
Events can include an optional `imageUrl`. The list uses image cards when an
image is present and a date tile when it is absent. The default view groups
events into "Happening now" and "Upcoming". The "Past Events" view loads on
demand and offers "Load more" for older events. Each card has a share action.
Sharing uses the event link when one exists; otherwise it uses a link to the
event card on the host page. For a one-list view, a card link can load up to
five more pages to find its Event. The visitor can still use Load more.

Pass one configuration object to the hook for a different list. The component
reads that configuration from the hook result. With `showTabs: false`, it shows
one full-width list; `includePast` selects whether that list includes ended
Events. `from`, `to`, and `sort` set the server-side date range and order.
Without `from` or `to`, a one-list view reads all publicly listed Event
history. It includes past Events by default and sorts by descending start
time, so newer Events appear first. With `includePast: false`, the default is
ascending start time for current and future Events. Each page has 20 Events.

```tsx
const config = {
  showTabs: false,
  includePast: true,
  from: "2026-09-01T00:00:00.000Z",
  to: "2026-10-01T00:00:00.000Z",
  sort: "startAtAsc",
} as const;
const events = useLockerversePublicEvents(
  { communitySlug: "auburn" },
  { config, initialPage }
);
return <LockerversePublicEvents events={events} />;
```

For server rendering, obtain `initialPage` with the SDK client's `search()`
and the same date, scope, sort, and page size. Without this configuration,
`initialEvents` from `list()` keeps the original Upcoming and Past views.

## Prefill checkout custom fields

`LockerversePayment` and `LockerverseDonateLauncher` accept
`initialCustomFieldValues`, keyed by configured field ID:

```tsx
<LockerversePayment
  widget={widget}
  selection={selection}
  initialCustomFieldValues={{ field_muw2q7vx_sf7r94: "Autographed basketball" }}
/>
```

Values are read once when the checkout form starts. Buyers can edit them;
price updates and parent renders do not overwrite edits. A new component `key`
starts a new form and resets all its inputs. Text uses strings, checkboxes use
booleans, and choice fields use option IDs (arrays for multiple choices).
Unknown fields, invalid types, and unknown options are ignored. Only custom
fields for the selected products are included. Required-field and server
validation still apply. For controlled updates, use the primitives below.

## Payment primitives

For a custom Core SDK checkout, import the same controlled fields and summary
used by `LockerversePayment`:

```tsx
import {
  CustomField, CustomFields, EmailField, TextField, FormField, FieldError,
  Checkbox, PhoneInput, PaymentSummary,
  createCustomFieldValues, reconcileCustomFieldValues,
  getCheckoutCustomFields, validateCustomFields, buildCustomFieldAnswers,
  getInternationalPhone,
  type CustomFieldValues,
} from "@lockerverse/react/payment-primitives";
import "@lockerverse/react/payment.css";
```

Each component also exports a named `...Props` type. The entry has no Stripe
or ready-made payment-controller dependency. Components are controlled: your
website owns the values and can mix its own inputs with the shared components.

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

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

Use real catalog field IDs. Text values are strings, checkbox values are
booleans, and choice values are option IDs (arrays for multiple choices).
Use `CustomField` with `field`, `value`, `onChange`, and optional `error` to
place individual fields in your own layout. Reconcile values with
`reconcileCustomFieldValues` when selected products or definitions change.
Before submission, pass the output of `validateCustomFields` to `errors` and
focus the first invalid field. Build the payload with
`buildCustomFieldAnswers(fields, values)` and supply it as
`submission.customFieldAnswers` to Core SDK `client.submitPayment()`.

The host still owns authoritative quotes, final-total display, Stripe Elements
validation and confirmation, required contact/shipping details, authentication,
and payment recovery. Do not show success until payment completion is confirmed.
See the Core SDK README for that contract.

Use `lockerverse-payment` with `data-lockerverse-theme="light"` or `"dark"`
on a wrapper for the existing appearance. Omit the stylesheet to supply your
own design. Use `<PaymentSummary quote={checkout} />` for a current quote.
Optional `displayedBaseCents`, `displayedTipCents`, and `displayedTotalCents`
overrides support pending price changes. They default to the quoted total minus
tip, tip, and total. These display properties never change the payable amount.
