@lockerverse/react
v0.2.97
Published
Tree-shakable React payment and signup UI for Lockerverse
Readme
@lockerverse/react
Tree-shakable React UI for Lockerverse payment and signup widgets. The Core SDK is installed automatically as a normal package dependency.
Install
npm install @lockerverse/react react react-domFor payment UI, also install the optional Stripe peers:
npm install @stripe/react-stripe-js @stripe/stripe-jsUse
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);
console.info("Customer", payment.email);
console.info("Authoritative items", payment.lineItems);
}}
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:
<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.
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:
<LockerversePayment
selection={[{ productSlug: "monthly-supporter" }]}
widget={widget}
/>Recurring checkout accepts one fixed Product. It does not accept quantities, custom amounts, or multiple Products.
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.
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.
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.
For a development HTTPS backend, provide apiBaseUrl to the hook. The backend
supplies the correct Stripe public key.
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}
/>Styling
Use theme="dark" or theme="light". Lockerverse applies the community branding returned by the widget API. Pass the same LockerverseStyle object to payment, signup, and donate launcher components.
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:
pendingaction_requiredrecovery_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.
Persist onPaymentRecoveryChange synchronously before navigation and pass the saved value back through resumePayment after a refresh. The recovery value contains a Lockerverse reference and public connected-account ID, never a Stripe secret.
import type { LockerversePaymentRecovery } from "@lockerverse/react";
const recoveryKey = "lockerverse:tailgate-party:payment";
const savedRecovery = sessionStorage.getItem(recoveryKey);
const recovery = savedRecovery
? (JSON.parse(savedRecovery) as LockerversePaymentRecovery)
: null;
<LockerversePayment
selection={selection}
resumePayment={recovery}
onPaymentRecoveryChange={(nextRecovery) => {
if (nextRecovery) {
sessionStorage.setItem(recoveryKey, JSON.stringify(nextRecovery));
} else {
sessionStorage.removeItem(recoveryKey);
}
}}
widget={widget}
/>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.
