useCheckout()
Drives the checkout step. Picks up the active reservation token,
calls POST /events/{eventId}/ticketing/checkout, branches on the
event's Ticketing config, and coordinates the paid flow across
server preparation and Stripe confirmation. Powers <CheckoutForm> —
consume this hook directly when you need a custom checkout UI.
Import
import { useCheckout } from "@feelrift/react";
Basic usage
import { useCheckout } from "@feelrift/react";
function ConfirmButton({ email }: { email: string }) {
const { prepare, status, error } = useCheckout();
const onClick = async () => {
try {
await prepare({ email });
} catch {
// useCheckout already wrote the error into `error`.
}
};
return (
<>
<button onClick={onClick} disabled={status === "preparing"}>
{status === "preparing" ? "Preparing…" : "Confirm"}
</button>
{error ? <p>{error.detail}</p> : null}
</>
);
}
Parameters
The hook takes no arguments. State comes from the <RiftProvider>
context and the persisted store (reservation token, Ticketing config).
Returns
| Field | Type | Description |
|---|---|---|
hasReservation | boolean | true when an active reservation token exists and prepare() can run. |
orderId | string | null | ord_… ID of the order, set after prepare() succeeds. |
orderStatusToken | string | null | Short-lived (~10 min) bearer JWT for useOrderStatus() polling. |
clientSecret | string | null | Stripe PaymentIntent client secret. null for free events. |
isFreeEvent | boolean | true when ticketingConfig.enabled === true && ticketingConfig.stripe === null. |
breakdown | CustomerEvaluation | null | Authoritative checkout breakdown from the server. |
hasPaymentMethod | boolean | true after useCheckoutPayment().capture() stores a Stripe PaymentMethod. |
pricing | { previousTotal: number; currentTotal: number; currency: string } | null | Reservation-vs-checkout total pair for price-change warnings. |
status | "idle" | "preparing" | "capturing" | "confirming" | "success" | "error" | Lifecycle across server preparation, Stripe capture, and confirmation. |
error | RiftApiError | StripeError | null | Most recent checkout failure. |
prepare | (input: { email: string }) => Promise<CheckoutResponse> | Mints the order and, for paid events, the PaymentIntent client secret. |
confirm | (input: { returnUrl: string }) => Promise<void> | Confirms the stored PaymentMethod against the prepared PaymentIntent. |
reset | () => Promise<void> | Clears checkout state without clearing the reservation. |
reportPaymentResult | (result: { kind: "success" } | { kind: "error"; message: string }) => void | Updates local state after a Stripe redirect return page reports the result. |
prepare
prepare(input: { email: string }): Promise<CheckoutResponse>
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Buyer's email — used to identify the attendee row for receipts and post-purchase lookup. Not authentication. |
Returns the full CheckoutResponse from the server
(orderId, orderStatusToken, breakdown, optional payment).
The SDK also persists these so most consumers read them from the
return fields above instead of the response.
Throws:
Error(plain) if called without an active reservation token. CalluseReservation().createReservation()first.RiftApiErrorfor server-side failures. See Errors below for the codes this call surfaces.
confirm
confirm(input: { returnUrl: string }): Promise<void>
| Parameter | Type | Required | Description |
|---|---|---|---|
returnUrl | string | Yes | URL Stripe redirects back to after redirect-required payment confirmation. |
confirm() requires a prepared checkout session and a captured
PaymentMethod. Call prepare({ email }), then
useCheckoutPayment().capture(), then confirm({ returnUrl }).
Behavior
- Captcha is automatic. The SDK picks up the current captcha
solution and attaches the
altcha-payloadheader on the outbound request. Mount<CaptchaWidget>in the subtree; no manual handling. - Side effects on prepare success. Persists
orderId,orderStatusToken,clientSecret, and the authoritative checkout breakdown souseOrderStatus()works without manual wiring. - Free event path. When
ticketingConfig.enabled === true && ticketingConfig.stripe === null, the server returnspayment: null. The hook flipsstatusto"success"and fireson.paymentSucceeded(orderId, orderStatusToken)from the<RiftProvider>callbacks. - Paid event path. The paid flow is three-step:
prepare()creates or resumes the PaymentIntent,useCheckoutPayment().capture()collects a Stripe PaymentMethod, andconfirm()authorizes it with Stripe.on.paymentSucceededfires from the confirmation step. - Window-closed signal. When the server returns
code: "checkout_window_closed", the hook fireson.checkoutWindowClosedin addition to surfacing the error. - Error broadcast. Every
RiftApiErroris also forwarded toon.errorfor observability. - Re-entrancy. Calling
preparewhilestatus === "preparing"isn't blocked by the hook; the SDK doesn't enforce single-flight. Wrap the call site with adisabledflag if double-submit matters.
Errors
Errors thrown by prepare() or confirm(). Server errors are
RiftApiError instances; Stripe confirmation can surface Stripe-side
errors through error. Narrow with isRiftApiError(err, "<code>")
for Rift codes. See the errors reference for the
typed extensions each code carries.
| Status | Code | When | Recovery |
|---|---|---|---|
400 | validation_error | Email failed Zod validation server-side. | Surface field errors from extensions.issues. |
401 | reservation_token_tampered | Token signature failed verification. | Restart the reservation flow. |
403 | altcha_payload_missing | Captcha solution missing on the request. | Mount <CaptchaWidget> and wait for useCaptcha().isReady. |
403 | altcha_payload_invalid | Captcha solution failed verification. | Widget auto-refreshes; retry. |
404 | order_not_found | Referenced order does not exist (defensive guard; rare in flow). | Restart the reservation flow. |
409 | order_in_terminal_state | Previous attempt already completed; carries extensions.orderStatus. | Route the user forward (success page / status page). |
410 | reservation_token_expired | Token's TTL passed. | Restart the reservation flow. |
410 | reservation_no_longer_valid | Order is already terminal — defense against enumeration. | Route the user forward. |
412 | checkout_window_closed | Past checkoutDeadline; reservation may still be alive. | Fires on.checkoutWindowClosed. Restart reservation. |
429 | rate_limited | Per-identity rate limit exceeded; carries extensions.retryAfterSeconds. | Wait retryAfterSeconds before retrying. |
| varies | unknown | Transport failure (status: 0) or unrecognized server code (carries the response status). | Treat as generic; update the SDK. |
0 | no_reservation | prepare() was called without an active reservation token. | Create a reservation first. |
0 | not_prepared | confirm() ran before prepare/capture populated the required state. | Call prepare(), then useCheckoutPayment().capture(). |
0 | stripe_unavailable | Stripe.js did not load for a paid checkout. | Install Stripe peers and confirm the Ticketing config. |
0 | payment_method_rejected | Stripe rejected the captured payment method. | Ask for a different card or payment method. |
0 | payment_attempt_no_longer_valid | The PaymentIntent was canceled after failed attempts. | Restart the reservation flow. |
Examples
Free event — prepare and route
function FreeCheckout({ email }: { email: string }) {
const { prepare, status, error } = useCheckout();
const submit = async () => {
try {
const response = await prepare({ email });
router.push(`/thanks?o=${response.orderId}`);
} catch {
// error state already set
}
};
return (
<button onClick={submit} disabled={status === "preparing"}>
Get tickets
</button>
);
}
Paid event — wired with Stripe Elements
import { useCheckout, useCheckoutPayment } from "@feelrift/react";
function PaidCheckout({
email,
returnUrl,
}: {
email: string;
returnUrl: string;
}) {
const { prepare, confirm } = useCheckout();
const { capture } = useCheckoutPayment();
const submit = async () => {
const response = await prepare({ email });
if (!response.payment) return; // server flipped to free; abort paid path
await capture();
await confirm({ returnUrl });
};
return <button onClick={submit}>Pay</button>;
}
Typed error handling
import { isRiftApiError, useCheckout } from "@feelrift/react";
function withTypedErrors() {
const { prepare } = useCheckout();
return async (email: string) => {
try {
await prepare({ email });
} catch (err) {
if (isRiftApiError(err, "checkout_window_closed")) {
showBanner(t("error.checkoutWindowClosed"));
return;
}
if (isRiftApiError(err, "rate_limited")) {
showBanner(
t("error.rateLimited", {
seconds: err.extensions.retryAfterSeconds,
}),
);
return;
}
if (isRiftApiError(err)) {
showBanner(t(`error.${err.code}`, { fallback: t("error.generic") }));
}
}
};
}
Related
<CheckoutForm>— the drop-in component built on this hook.useOrderStatus— polls the order afterconfirmresolves.- Error handling — the narrowing pattern this hook expects consumers to use.
- Errors reference — typed extensions per code.