Skip to main content

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

FieldTypeDescription
hasReservationbooleantrue when an active reservation token exists and prepare() can run.
orderIdstring | nullord_… ID of the order, set after prepare() succeeds.
orderStatusTokenstring | nullShort-lived (~10 min) bearer JWT for useOrderStatus() polling.
clientSecretstring | nullStripe PaymentIntent client secret. null for free events.
isFreeEventbooleantrue when ticketingConfig.enabled === true && ticketingConfig.stripe === null.
breakdownCustomerEvaluation | nullAuthoritative checkout breakdown from the server.
hasPaymentMethodbooleantrue after useCheckoutPayment().capture() stores a Stripe PaymentMethod.
pricing{ previousTotal: number; currentTotal: number; currency: string } | nullReservation-vs-checkout total pair for price-change warnings.
status"idle" | "preparing" | "capturing" | "confirming" | "success" | "error"Lifecycle across server preparation, Stripe capture, and confirmation.
errorRiftApiError | StripeError | nullMost 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 }) => voidUpdates local state after a Stripe redirect return page reports the result.

prepare

prepare(input: { email: string }): Promise<CheckoutResponse>
ParameterTypeRequiredDescription
emailstringYesBuyer'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. Call useReservation().createReservation() first.
  • RiftApiError for server-side failures. See Errors below for the codes this call surfaces.

confirm

confirm(input: { returnUrl: string }): Promise<void>
ParameterTypeRequiredDescription
returnUrlstringYesURL 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-payload header 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 so useOrderStatus() works without manual wiring.
  • Free event path. When ticketingConfig.enabled === true && ticketingConfig.stripe === null, the server returns payment: null. The hook flips status to "success" and fires on.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, and confirm() authorizes it with Stripe. on.paymentSucceeded fires from the confirmation step.
  • Window-closed signal. When the server returns code: "checkout_window_closed", the hook fires on.checkoutWindowClosed in addition to surfacing the error.
  • Error broadcast. Every RiftApiError is also forwarded to on.error for observability.
  • Re-entrancy. Calling prepare while status === "preparing" isn't blocked by the hook; the SDK doesn't enforce single-flight. Wrap the call site with a disabled flag 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.

StatusCodeWhenRecovery
400validation_errorEmail failed Zod validation server-side.Surface field errors from extensions.issues.
401reservation_token_tamperedToken signature failed verification.Restart the reservation flow.
403altcha_payload_missingCaptcha solution missing on the request.Mount <CaptchaWidget> and wait for useCaptcha().isReady.
403altcha_payload_invalidCaptcha solution failed verification.Widget auto-refreshes; retry.
404order_not_foundReferenced order does not exist (defensive guard; rare in flow).Restart the reservation flow.
409order_in_terminal_statePrevious attempt already completed; carries extensions.orderStatus.Route the user forward (success page / status page).
410reservation_token_expiredToken's TTL passed.Restart the reservation flow.
410reservation_no_longer_validOrder is already terminal — defense against enumeration.Route the user forward.
412checkout_window_closedPast checkoutDeadline; reservation may still be alive.Fires on.checkoutWindowClosed. Restart reservation.
429rate_limitedPer-identity rate limit exceeded; carries extensions.retryAfterSeconds.Wait retryAfterSeconds before retrying.
variesunknownTransport failure (status: 0) or unrecognized server code (carries the response status).Treat as generic; update the SDK.
0no_reservationprepare() was called without an active reservation token.Create a reservation first.
0not_preparedconfirm() ran before prepare/capture populated the required state.Call prepare(), then useCheckoutPayment().capture().
0stripe_unavailableStripe.js did not load for a paid checkout.Install Stripe peers and confirm the Ticketing config.
0payment_method_rejectedStripe rejected the captured payment method.Ask for a different card or payment method.
0payment_attempt_no_longer_validThe 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>
);
}
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") }));
}
}
};
}