Components
Preview every buyer component, copy its code, and inspect only the Cipay-specific props.
Cipay components render semantic HTML and fetch through CipayProvider. The code tabs keep preview styling behind short semantic selectors so utility-class noise does not hide the SDK usage. The styling guide shows className, CSS, accessibility, and RTL customization separately.
The docs inject query data only to keep live previews deterministic. In an application, the provider calls your mounted buyer handler and every product shown below comes from Cipay.
ProductsList
ProductsList fetches a catalog page, handles request states, and renders one ProductCard per result. The SDK calls renderProduct(product) for each item returned by Cipay, so UI code never defines a product array.
import { CipayProvider, ProductsList } from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export default function ProductsListPreview() {
return (
<CipayProvider endpoint="/api/cipay" queryClient={componentPreviewClient}>
<div data-preview="catalog">
{/* ProductsList fetches published products; no product array is defined in UI code. */}
<ProductsList heading="Choose a plan" />
</div>
</CipayProvider>
);
}
ProductsList props
input?ProductsListInput
Search and cursor pagination sent to Cipay.
ProductsListInputheading?ReactNode
Optional section heading.
ReactNoderenderProduct?(product: BuyerProduct) => ReactNode
Replaces the default card for every returned product.
(product: BuyerProduct) => ReactNodeloading?() => ReactNode
Replaces the loading state.
() => ReactNodeempty?() => ReactNode
Replaces the empty result state.
() => ReactNodeerror?(error: Error) => ReactNode
Replaces the request error state.
(error: Error) => ReactNodeProductCard
ProductCard shows the first product image, name, description, offer picker, and optional footer. Set renderMedia={false} to hide the image or provide a callback to replace it.
import {
CipayProvider,
ProductCard,
useProduct,
} from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export function ProductCardExample() {
// Fetch the product created in Cipay instead of defining it in this component.
const product = useProduct("coffee");
if (product.isPending) return <p>Loading product…</p>;
if (product.isError) return <p role="alert">{product.error.message}</p>;
return (
<div data-preview="product-card">
<ProductCard
product={product.data}
priceLabel={<strong>Billing plan</strong>}
footer={<button>Continue to checkout</button>}
/>
</div>
);
}
export default function ProductCardPreview() {
return (
<CipayProvider endpoint="/api/cipay" queryClient={componentPreviewClient}>
<ProductCardExample />
</CipayProvider>
);
}
ProductCard props
productBuyerProduct
Product returned by useProductsList or useProduct.
BuyerProductselectedOfferId?string
Controlled selected offer ID.
stringonOfferChange?(offerId: string) => void
Runs when the buyer selects another offer.
(offerId: string) => voidrenderMedia?false | (asset) => ReactNode
Hides or replaces the first product image.
false | (asset) => ReactNoderenderOffer?(offer: BuyerOffer) => ReactNode
Replaces each option label.
(offer: BuyerOffer) => ReactNodepriceLabel?ReactNode
Labels the offer selector.
ReactNodefooter?ReactNode
Content after the selector, usually checkout.
ReactNodePricing plans
A pricing section uses ProductsList to fetch published plans. Cipay supplies product and offer data; your layout decides the responsive card arrangement and which plan is featured.
import {
CipayProvider,
ProductCard,
ProductsList,
} from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export function PricingPlans() {
return (
<div data-preview="pricing-plans">
{/* Every card receives a product fetched from Cipay by ProductsList. */}
<ProductsList
input={{ pageSize: 3 }}
aria-label="Pricing plans"
renderProduct={(product) => (
<ProductCard
product={product}
priceLabel={<strong>Billing</strong>}
footer={<button>Choose {product.name}</button>}
/>
)}
/>
</div>
);
}
export default function PricingPlansPreview() {
return (
<CipayProvider
endpoint="/api/cipay"
queryClient={componentPreviewClient}
>
<PricingPlans />
</CipayProvider>
);
}
PriceSelector
PriceSelector receives product.offers from useProduct, useProductsList, or a list component callback. It is a labeled native <select>, so keyboard navigation and browser accessibility work without extra JavaScript.
import {
CipayProvider,
PriceSelector,
useProduct,
} from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export function PriceSelectorExample() {
const product = useProduct("coffee");
if (!product.data) return <p>Loading prices…</p>;
return (
<div data-preview="price-selector">
{/* Offers come from the product response and stay contract-typed. */}
<PriceSelector offers={product.data.offers} label="Choose billing" />
</div>
);
}
export default function PriceSelectorPreview() {
return (
<CipayProvider endpoint="/api/cipay" queryClient={componentPreviewClient}>
<PriceSelectorExample />
</CipayProvider>
);
}
PriceSelector props
offersreadonly BuyerOffer[]
Offers returned with a buyer product.
readonly BuyerOffer[]value?string
Controlled selected offer ID.
stringonValueChange?(offerId: string) => void
Runs with the selected offer ID.
(offerId: string) => voidlabel?ReactNode
Accessible select label.
ReactNoderenderOption?(offer: BuyerOffer) => ReactNode
Replaces the default option label.
(offer: BuyerOffer) => ReactNodeCheckoutButton
CheckoutButton creates one hosted checkout attempt, disables itself while pending, and redirects to the returned gateway URL. buyer is checkout contact information for this attempt; it is separate from the merchant-side Customer resource.
import { CipayProvider, CheckoutButton } from "@cipay/client-sdk/react";
export default function CheckoutButtonPreview() {
return (
<CipayProvider endpoint="/api/cipay">
<div data-preview="checkout-button">
<p>SAR 45.00 billed monthly</p>
{/* Checkout stays gateway-hosted; this button never collects card data. */}
<CheckoutButton
input={{ offerId: "monthly", buyer: { email: "buyer@example.test" } }}
label="Continue to secure checkout"
onError={() => undefined}
/>
</div>
</CipayProvider>
);
}
Set recurringConsentAccepted only after the buyer explicitly accepts recurring billing terms.
CheckoutButton props
inputCheckoutInput
Selected offer, optional buyer details, discount, and consent.
CheckoutInputlabel?ReactNode
Button text.
ReactNodeonCheckout?(checkout: Checkout) => void | Promise<void>
Runs before redirect after checkout creation.
(checkout: Checkout) => void | Promise<void>onError?(error: Error) => void
Handles a failed checkout request.
(error: Error) => voidnavigate?(url: string) => void
Overrides browser navigation for routers or tests.
(url: string) => voidSubscriptionsList
SubscriptionsList reads subscriptions for the server-verified mapped customer. It stays idle without a provider sessionKey.
import { CipayProvider, SubscriptionsList } from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export default function SubscriptionsListPreview() {
return (
<CipayProvider
endpoint="/api/cipay"
sessionKey="preview-user"
queryClient={componentPreviewClient}
>
<div data-preview="account-list">
{/* sessionKey partitions cache data; server authentication still owns access. */}
<SubscriptionsList heading="Your subscriptions" />
</div>
</CipayProvider>
);
}
SubscriptionsList props
heading?ReactNode
Optional section heading.
ReactNodesignedOut?() => ReactNode
Shown when sessionKey is absent.
() => ReactNoderenderSubscription?(subscription: Subscription) => ReactNode
Replaces each subscription row.
(subscription: Subscription) => ReactNodeloading?() => ReactNode
Replaces the loading state.
() => ReactNodeempty?() => ReactNode
Replaces the empty state.
() => ReactNodeerror?(error: Error) => ReactNode
Replaces the error state.
(error: Error) => ReactNodeOrdersList
OrdersList reads only orders owned by the authenticated mapped customer and lets your app replace each receipt row.
import { CipayProvider, OrdersList } from "@cipay/client-sdk/react";
import { componentPreviewClient } from "../preview/component-data";
export default function OrdersListPreview() {
return (
<CipayProvider
endpoint="/api/cipay"
sessionKey="preview-user"
queryClient={componentPreviewClient}
>
<div data-preview="account-list">
{/* Orders are scoped to the customer resolved from the server session. */}
<OrdersList heading="Order history" />
</div>
</CipayProvider>
);
}
OrdersList props
heading?ReactNode
Optional section heading.
ReactNodesignedOut?() => ReactNode
Shown when sessionKey is absent.
() => ReactNoderenderOrder?(order: Order) => ReactNode
Replaces each order row.
(order: Order) => ReactNodeloading?() => ReactNode
Replaces the loading state.
() => ReactNodeempty?() => ReactNode
Replaces the empty state.
() => ReactNodeerror?(error: Error) => ReactNode
Replaces the error state.
(error: Error) => ReactNodeInvoiceDownloadButton
InvoiceDownloadButton requests an owned invoice through your backend and downloads the returned PDF. Cross-customer orders receive the same non-enumerating error as a missing order.
import { CipayProvider, InvoiceDownloadButton } from "@cipay/client-sdk/react";
export default function InvoiceButtonPreview() {
return (
<CipayProvider endpoint="/api/cipay" sessionKey="preview-user">
<div data-preview="invoice-button">
<div>
<strong>Invoice INV-1042</strong>
<span>SAR 45.00 · Paid</span>
</div>
{/* The backend verifies that this order belongs to the signed-in customer. */}
<InvoiceDownloadButton
orderId="order_123"
label="Download PDF"
onError={() => undefined}
/>
</div>
</CipayProvider>
);
}
InvoiceDownloadButton props
orderIdstring
Owned Cipay order ID.
stringlabel?ReactNode
Button text.
ReactNodeonDownload?(invoice: Invoice) => void | Promise<void>
Overrides the default browser download.
(invoice: Invoice) => void | Promise<void>onError?(error: Error) => void
Handles invoice request failures.
(error: Error) => void