@xpayeg/react
The React SDK. XPayProvider, hooks, and components built on top of @xpayeg/sdk.
The XPay React SDK is a thin wrapper around @xpayeg/sdk 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 @xpayeg/sdk @xpayeg/react
# Both packages are required. @xpayeg/react peer-depends on @xpayeg/sdk, react ^18 || ^19, react-dom ^18 || ^19.This page is the reference for every export. For walkthroughs, see Drop-in and Elements under Integrate.
See it running: Next.js example store
A working storefront built with this SDK: hosted checkout, drop-in, inline, and Elements, plus a signature-verified webhook receiver. Clone, add your test keys, run.
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 "@xpayeg/sdk";
// 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 { xpayPromise } from "@/lib/xpay";
import { PaymentElement, useCheckout, XPayProvider } from "@xpayeg/react";
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 @xpayeg/sdk for convenience: CheckoutSession, CheckoutActions, PaymentMethodInfo, Appearance, ActionResult, XPayError, ConfirmPaymentOptions, PaymentElementChangeEvent, ElementsOptions. Import additional types directly from @xpayeg/sdk 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.
Deferred mode
options also accepts the deferred form, with no clientSecret:
<XPayProvider xpay={xpayPromise} options={{ mode: "payment", amount: 149900, currency: "EGP" }}>
{children}
</XPayProvider>The element renders from the amount alone; your server creates the session when the customer pays, and you confirm with xpay.confirmPayment({ elements, clientSecret }). Changing options.amount or options.currency updates the mounted element in place; options.paymentMethodTypes (an optional narrow-only render filter, e.g. ["card"]) is fixed for the element's lifetime. In deferred mode use useElements() and <PaymentElement />; useCheckout() is for session-first integrations. Full walkthrough: Elements: deferred mode.
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 @xpayeg/sdk 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 and returns the fresh data. |
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 |
|---|---|---|
options | PaymentElementOptions | Optional. Element options, notably layout (see below). |
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>. |
options.layout picks how the method chooser is presented: "accordion" (default, vertical radio list) or "tabs" (a wrapping tile grid). With exactly one payment method the accordion shows a static logo-and-name header instead of a chooser (a single card method renders the bare card form), and tabs render the content alone with no chrome, for pages that already display the method's identity themselves. Changing the prop updates the mounted element in place.
<PaymentElement options={{ layout: "tabs" }} onChange={(e) => setReady(e.complete)} />The component renders a single <div> and mounts the underlying PaymentElement (from @xpayeg/sdk) 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.Set the destination on the session as afterCompletion.redirect.url. XPay navigates to it unchanged, appending nothing. Put {CHECKOUT_SESSION_ID} in the URL if your return page needs to know which session it is.
Where to next
@xpayeg/sdk
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.