@xpay/sdk-react
The React SDK. XPayProvider, hooks, and components built on top of @xpay/sdk-js.
The XPay React SDK is a thin wrapper around @xpay/sdk-js that exposes a provider, hooks, and a component for the payment element. The provider creates an Elements instance from a clientSecret, the hooks expose the live session state and action methods, and the components mount payment UI.
pnpm add @xpay/sdk-js @xpay/sdk-react
# Both packages are required. @xpay/sdk-react peer-depends on @xpay/sdk-js, react ^18 || ^19, react-dom ^18 || ^19.This page is the reference for every export. For walkthroughs, see Drop-in and Elements under Integrate.
Quick start
The recommended pattern is loadXPay at module level (or in a small loader file), then create a Checkout Session on your server, hold the returned clientSecret in state, and render <XPayProvider> once you have it. useCheckout() then reads the live session inside any child component.
"use client";
import { loadXPay } from "@xpay/sdk-js";
// Module-level: load once, share across all components.
export const xpayPromise = loadXPay(process.env.NEXT_PUBLIC_XPAY_PUBLISHABLE_KEY!);"use client";
import { useState } from "react";
import { XPayProvider, useCheckout, PaymentElement } from "@xpay/sdk-react";
import { xpayPromise } from "@/lib/xpay";
export default function CheckoutPage() {
const [clientSecret, setClientSecret] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const startCheckout = async () => {
setCreating(true);
const res = await fetch("/api/create-checkout", { method: "POST" });
const { clientSecret } = await res.json();
setClientSecret(clientSecret);
setCreating(false);
};
if (!clientSecret) {
return (
<button onClick={startCheckout} disabled={creating}>
{creating ? "Starting…" : "Pay now"}
</button>
);
}
return (
<XPayProvider xpay={xpayPromise} options={{ clientSecret }}>
<CheckoutForm />
</XPayProvider>
);
}
function CheckoutForm() {
const state = useCheckout();
if (state.type === "loading") return <p>Loading…</p>;
if (state.type === "error") return <p>{state.error.message}</p>;
const { checkout } = state;
if (checkout.status.type === "expired") return <p>This checkout has expired.</p>;
if (checkout.status.type === "complete") return <p>Payment complete. Thank you.</p>;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await checkout.confirm({
customerDetails: { email: "customer@example.com" },
});
if (result.type === "error") {
alert(result.error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!checkout.canConfirm}>
Pay {checkout.currency} {(checkout.amountTotal / 100).toFixed(2)}
</button>
</form>
);
}The clientSecret lives in state, so XPayProvider only mounts once and /api/create-checkout is called exactly once per checkout. If you create the session earlier in your flow (a click on a product page, a server component, a route param), pass the resulting string in any way you like: props, search params, context. The provider only needs the string.
useCheckout() updates reactively after every promo code, quantity change, or fee recalculation. There's no need for an onChange callback in React; the component re-renders with the latest checkout data on its own.
Exports at a glance
| Export | Kind | What it is |
|---|---|---|
XPayProvider | component | Wraps your checkout UI and provides context to all child components. |
useCheckout | hook | Returns the disjoint union state and the merged session-plus-actions object. |
useXPay | hook | Returns the underlying XPayInstance (or null). |
useElements | hook | Returns the underlying Elements instance (or null). |
useConfirmPayment | hook | Convenience hook that exposes confirmPayment from the active checkout. |
PaymentElement | component | Mounts the payment-method selector with the card form. |
CheckoutButton | component | A pre-built button that opens the drop-in checkout modal on click. |
Checkout | type | CheckoutSession & CheckoutActions. The merged shape useCheckout returns on success. |
UseCheckoutResult | type | The disjoint union returned by useCheckout. |
PaymentElementProps, CheckoutButtonProps | type | Component prop types. |
The package re-exports a few core types from @xpay/sdk-js for convenience: CheckoutSession, CheckoutActions, PaymentMethodInfo, Appearance, ActionResult, XPayError, ConfirmPaymentOptions, PaymentElementChangeEvent, ElementsOptions. Import additional types directly from @xpay/sdk-js when you need them.
<XPayProvider>
Wraps the part of your app that runs the checkout. Creates an Elements instance from options.clientSecret and provides three React contexts: the XPayInstance, the Elements, and the loading-state-aware checkout state.
<XPayProvider xpay={xpayPromise} options={{ clientSecret, appearance, locale }}>
{children}
</XPayProvider>| Prop | Type | Description |
|---|---|---|
xpay | XPayInstance | Promise<XPayInstance | null> | null | The instance from loadXPay(). Pass the Promise; the provider awaits it on the client. |
options.clientSecret | string | Promise<string> | The Checkout Session client secret. Required to create an Elements instance and load the session. |
options.appearance | Appearance | Optional. Branding overrides merged with the session's server-side branding. |
options.locale | "en" | "ar" | Optional. Defaults to "en". |
children | ReactNode | The subtree that calls useCheckout, useXPay, useElements, useConfirmPayment. |
The provider handles SSR gracefully: loadXPay returns null on the server, and the provider waits for the client to load the SDK before creating the Elements instance.
useCheckout()
The primary hook. Returns a disjoint union; narrow by state.type before reading session data or calling actions.
type UseCheckoutResult =
| { type: "loading" }
| { type: "error"; error: { message: string } }
| { type: "success"; checkout: Checkout };
type Checkout = CheckoutSession & CheckoutActions;const state = useCheckout();
if (state.type === "loading") return <Spinner />;
if (state.type === "error") return <p>{state.error.message}</p>;
const { checkout } = state;After narrowing to success, checkout carries every session field plus every action method.
Session fields
The session-data fields on checkout come straight from CheckoutSession. See the @xpay/sdk-js reference for the full schema. Most-used fields:
| Field | Type | Description |
|---|---|---|
id | string | Session ID. |
amountTotal | number | Total in minor units. |
amountSubtotal | number | Subtotal in minor units. |
currency | string | Currency code. |
merchantName | string | Your business name. |
livemode | boolean | Live vs test mode. |
status | SessionStatus | {type:"open"} | {type:"expired"} | {type:"complete", paymentStatus} |
canConfirm | boolean | Whether the session is ready to confirm. |
paymentMethods | PaymentMethodInfo[] | Available methods on this session. |
lineItems | CheckoutLineItem[] | Line items. |
totalDetails | CheckoutTotalDetails | Subtotal, tax, shipping, discount, fee breakdown. |
discounts | CheckoutDiscount[] | Applied promotion codes. |
Action methods
Action methods come from CheckoutActions. All return a Promise<ActionResult> (a tagged union of success or error) except where noted.
| Method | Description |
|---|---|
confirm(options?: Omit<ConfirmPaymentOptions, "elements">) | Confirms the payment. Handles 3DS challenges and BNPL redirects internally. |
applyPromotionCode(code: string) | Applies a promo code; the session re-renders with the new total. |
removePromotionCode() | Removes the applied code. |
updateLineItemQuantity({ lineItem, quantity }) | Updates a line item's quantity. |
submit() | Validates fields. Returns { error?, selectedPaymentMethod? }. |
fetchUpdates() | Re-fetches the session from the server. |
changeAppearance(appearance: Appearance) | Updates branding at runtime; returns void. |
on(event, handler) | Listens for "change", "error", or a custom event. Returns void. |
getElements() | Returns the underlying Elements instance for low-level access. |
The hook listens for the underlying change and loaderror events itself, so when promo codes are applied, line items update, or fees recalculate, the component re-renders with the latest data without you wiring an effect.
Promo codes and quantities (example)
function LineItems() {
const state = useCheckout();
if (state.type !== "success") return null;
const { checkout } = state;
return (
<ul>
{checkout.lineItems?.map((item) => (
<li key={item.id}>
<span>{item.description}</span>
<button
onClick={() =>
checkout.updateLineItemQuantity({
lineItem: item.id,
quantity: item.quantity + 1,
})
}
>
+
</button>
<button
onClick={() =>
checkout.updateLineItemQuantity({
lineItem: item.id,
quantity: Math.max(1, item.quantity - 1),
})
}
>
-
</button>
</li>
))}
</ul>
);
}
function PromoInput() {
const state = useCheckout();
const [code, setCode] = useState("");
const [error, setError] = useState("");
if (state.type !== "success") return null;
const { checkout } = state;
const apply = async () => {
setError("");
const result = await checkout.applyPromotionCode(code);
if (result.type === "error") setError(result.error.message);
};
return (
<div>
<input value={code} onChange={(e) => setCode(e.target.value)} />
<button onClick={apply}>Apply</button>
<button onClick={() => checkout.removePromotionCode()}>Remove</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}Listening for unsolicited errors
change events drive React state automatically. The error event covers errors that fire outside any merchant action (e.g. session expired during a fee recalculation, BIN detection failure):
useEffect(() => {
if (state.type !== "success") return;
state.checkout.on("error", (err) => {
console.error("[XPay] unsolicited error:", err.code, err.message);
});
}, [state]);useXPay() and useElements()
Lower-level hooks that return the underlying instances. Useful when you need to call methods that aren't exposed through useCheckout (for example, mounting an extra element programmatically).
function useXPay(): XPayInstance | null;
function useElements(): Elements | null;Both return null while the SDK is loading or before a clientSecret has been provided. Both must be called inside <XPayProvider>.
useConfirmPayment()
Convenience hook around useCheckout. Returns the same confirm function plus an isConfirming flag. Useful when you want a tiny API for a leaf component that only needs to submit:
const { confirmPayment, isConfirming } = useConfirmPayment();If the checkout isn't ready yet, confirmPayment() returns { type: "error", error: { message: "Checkout not ready" } } immediately rather than throwing.
<PaymentElement />
Renders the payment-method selector and card form. Must be inside <XPayProvider> with a session.
<PaymentElement
onChange={(e) => setReady(e.complete)}
onReady={() => console.log("element ready")}
/>| Prop | Type | Description |
|---|---|---|
onReady | () => void | Optional. Fires when the element's iframe is initialized. |
onChange | (event: PaymentElementChangeEvent) => void | Optional. Fires on payment-method selection and field changes. |
onLoaderStart | () => void | Optional. Fires synchronously when the iframe is created. |
onLoadError | (event: ElementsLoadErrorEvent) => void | Optional. Fires when the element fails to load. |
className | string | Optional. CSS class for the container <div>. |
id | string | Optional. ID for the container <div>. |
The component renders a single <div> and mounts the underlying PaymentElement (from @xpay/sdk-js) into it. On unmount, the element is torn down automatically.
<CheckoutButton />
Pre-built button that opens the drop-in checkout modal on click. Wraps xpay.checkout({ mode: "modal" }) so you don't have to wire it yourself.
<CheckoutButton
clientSecret="cs_test_..."
checkoutOptions={{
onComplete: (result) => router.push(`/orders/${result.paymentIntentId}`),
onClose: () => console.log("Customer closed checkout"),
}}
>
Pay Now
</CheckoutButton>| Prop | Type | Description |
|---|---|---|
clientSecret | string | Required. The Checkout Session client secret. |
children | ReactNode | Button label. Defaults to "Pay". |
checkoutOptions | Omit<CheckoutOptions, "clientSecret" | "mode"> | Optional. Callbacks (onComplete, onClose, onReady, onConfirmed, onError), appearance, locale. |
className | string | Optional. CSS class for the button. |
disabled | boolean | Optional. Disables the button. |
The button is also disabled automatically when the SDK isn't loaded yet (useXPay() returns null). Must be inside <XPayProvider>.
Common patterns
Theme-aware appearance
Sync XPay's color mode with your app's theme using changeAppearance at runtime:
function ThemeAwareCheckout() {
const state = useCheckout();
const { theme } = useTheme();
useEffect(() => {
if (state.type === "success") {
state.checkout.changeAppearance({ colorMode: theme as "light" | "dark" });
}
}, [theme, state]);
return <PaymentElement />;
}Pre-validating before confirming
submit() validates all fields before confirm(). Useful when you want to gate the confirm step behind another UI step (a confirmation dialog, a Terms checkbox, etc.):
const handlePay = async () => {
const { error, selectedPaymentMethod } = await checkout.submit();
if (error) return setError(error.message);
const ok = await showConfirmDialog(selectedPaymentMethod);
if (!ok) return;
const result = await checkout.confirm();
if (result.type === "error") setError(result.error.message);
};Redirect after success
By default confirm() returns the result to your code. Pass redirect: "always" to send the customer to your afterCompletion.redirect.url after success:
await checkout.confirm({
customerDetails: { email },
redirect: "always",
});
// On success, the page navigates away. Any code below only runs on error.You can override the server-side returnUrl from the client by passing it explicitly: redirect: "always", returnUrl: "https://example.com/custom".
Where to next
@xpay/sdk-js
The underlying JavaScript SDK. Reference for loadXPay, XPayInstance, Elements, drop-in checkout, and every type.
Drop-in pattern
Walkthrough for the modal/inline checkout via <CheckoutButton /> or xpay.checkout().
Elements pattern
Walkthrough for <XPayProvider> + <PaymentElement /> + useCheckout.