Hooks
Build custom catalog, checkout, subscription, order, and invoice interfaces with TanStack Query.
Start with Cipay components when their markup fits. Use hooks when your product needs custom state, layout, or orchestration. Both paths call the same buyer handler and use the same contract-derived types; using a hook does not require replacing useful Cipay components with hand-written markup.
Each read hook returns a normal TanStack Query v5 result. The SDK supplies the query key, fetcher, same-origin credentials, no-store policy, and ownership boundary.
Catalog query controls
This example uses useProductsList for explicit cache and refresh behavior, then passes each fetched product to ProductCard. Products are created in Cipay and returned by the hook; they are not defined in UI code.
"use client";
import {
ProductCard,
useProductsList,
} from "@cipay/client-sdk/react";
export function Catalog() {
const products = useProductsList(
{ query: "coffee", pageSize: 12 },
{
staleTime: 30_000,
retry: 2,
retryDelay: (attempt) => Math.min(1_000 * 2 ** attempt, 8_000),
refetchOnWindowFocus: false,
},
);
if (products.isPending) return <p>Loading products…</p>;
if (products.isError) {
return <p role="alert">{products.error.message}</p>;
}
return (
<section>
{/* isFetching also covers a background refresh while data remains visible. */}
<button
type="button"
disabled={products.isFetching}
onClick={() => void products.refetch()}
>
{products.isFetching ? "Refreshing…" : "Refresh catalog"}
</button>
{products.data.items.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</section>
);
}
Changing query, pageSize, or cursor creates a different cache key. Send nextCursor with the same filters to request the next page, and reset the cursor when search filters change.
TanStack Query v5 does not support onSuccess, onError, or onSettled on read-query options. Derive UI directly from the result, as above. Use an effect only when fetched data must synchronize with a genuine external system.
Retrieve one product
useProduct(id) loads one published product and stays disabled while id is null. ProductCard renders the returned contract object directly.
"use client";
import {
ProductCard,
useProduct,
} from "@cipay/client-sdk/react";
export function ProductDetail({ productId }: { productId: string | null }) {
const product = useProduct(productId, {
staleTime: 60_000,
retry: 1,
});
if (!productId) return <p>Select a product.</p>;
if (product.isPending) return <p>Loading product…</p>;
if (product.isError) return <p role="alert">{product.error.message}</p>;
return <ProductCard product={product.data} />;
}
The product response includes published assets and offers. See the generated product response for every field.
Preview a quote
useQuote returns Cipay-calculated amounts for the chosen offer and discount. It does not create checkout or return a payment URL.
"use client";
import {
CheckoutButton,
useQuote,
} from "@cipay/client-sdk/react";
export function Quote({ offerId }: { offerId: string }) {
const quote = useQuote(
{ offerId, discountCode: "SANDBOX10" },
{ staleTime: 15_000, retry: 1 },
);
if (quote.isPending) return <p>Calculating total…</p>;
if (quote.isError) return <p role="alert">{quote.error.message}</p>;
if (!quote.data) return <p>This offer is unavailable.</p>;
return (
<section>
{/* Money is returned in integer halalas; format it only for display. */}
<p>Total: {(quote.data.totalHalalas / 100).toFixed(2)} {quote.data.currency}</p>
<CheckoutButton
input={{
offerId,
buyer: { email: "buyer@example.test", locale: "en" },
recurringConsentAccepted: true,
}}
/>
</section>
);
}
Set recurringConsentAccepted only after the buyer has explicitly accepted the recurring terms shown by your application.
Create checkout with a hook
Prefer CheckoutButton for the standard create-and-redirect flow. Use useCheckoutCreate when you need custom validation, analytics, or navigation before redirecting.
"use client";
import { useCheckoutCreate } from "@cipay/client-sdk/react";
export function CustomCheckout({ offerId }: { offerId: string }) {
const checkout = useCheckoutCreate({
// This callback applies to every checkout started by this component.
onError: (error) => reportSafeError(error.status, error.message),
});
function startCheckout() {
checkout.mutate(
{
offerId,
buyer: { email: "buyer@example.test", locale: "en" },
recurringConsentAccepted: true,
},
{
// This callback belongs only to this button click.
onSuccess: (created) => {
saveCapabilityInServerSession(created.capability);
if (created.checkoutUrl) window.location.assign(created.checkoutUrl);
},
},
);
}
return (
<button type="button" disabled={checkout.isPending} onClick={startCheckout}>
{checkout.isPending ? "Opening checkout…" : "Continue"}
</button>
);
}
The capability is a short-lived secret. Send it to your backend session; never put it in a URL, browser storage, analytics, or logs.
Read checkout status
useCheckoutStatus polls only when a capability exists. Use the generated CheckoutState values instead of repeating contract strings.
"use client";
import {
CheckoutState,
useCheckoutStatus,
} from "@cipay/client-sdk/react";
export function CheckoutStatus({ capability }: { capability: string | null }) {
const status = useCheckoutStatus(capability, {
retry: 2,
refetchInterval: (query) =>
query.state.data?.state === CheckoutState.Completed ? false : 2_000,
});
if (!capability) return null;
if (status.isPending) return <p>Checking payment…</p>;
if (status.isError) return <p role="alert">{status.error.message}</p>;
return <p>Checkout status: {status.data.state}</p>;
}
A webhook remains the durable payment signal. Status is for the buyer’s return screen, not for irreversible fulfillment.
Subscription account hooks
useSubscriptionsList and subscription mutations run only after CipayProvider receives a non-secret sessionKey. The backend still verifies the application session and resolves the stored customer mapping.
SubscriptionsList handles query states and supplies each fetched subscription to renderSubscription; the mutation hook adds the account-specific cancellation action.
"use client";
import {
SubscriptionsList,
useScheduleSubscriptionCancellation,
} from "@cipay/client-sdk/react";
export function Subscriptions() {
const cancellation = useScheduleSubscriptionCancellation({
// Shared callback: refresh surrounding UI after any successful cancellation.
onSuccess: () => showNotice("Cancellation scheduled"),
onError: (error) => showError(error.message),
});
return (
<SubscriptionsList
heading="Your subscriptions"
renderSubscription={(subscription) => (
<article>
<p>Status: {subscription.status}</p>
<button
type="button"
disabled={cancellation.isPending}
onClick={() => cancellation.mutate({
id: subscription.id,
revision: subscription.revision,
reason: "Requested in account settings",
})}
>
Cancel at period end
</button>
</article>
)}
/>
);
}
revision stops an old screen from overwriting a newer subscription change. useRemoveSubscriptionCancellation uses the same { id, revision } pattern. See the subscription concurrency flow.
Orders and invoice download
Use OrdersList for the owned order query and InvoiceDownloadButton for the binary request and browser download. Both remain scoped to the server-verified customer.
"use client";
import {
InvoiceDownloadButton,
OrdersList,
} from "@cipay/client-sdk/react";
export function Orders() {
return (
<OrdersList
heading="Order history"
renderOrder={(order) => (
<article>
<p>{order.invoiceNumber ?? order.id}</p>
<p>{order.totalHalalas} {order.currency}</p>
<InvoiceDownloadButton
orderId={order.id}
label="Download invoice"
onError={(error) => showError(error.message)}
/>
</article>
)}
/>
);
}
Use useOrdersList or useOrder(id) when you need a fully custom order UI. Use useInvoiceDownload when your application must handle the returned { bytes, filename } itself instead of using the component’s default browser download.
Mutation callbacks
Mutation callbacks have two useful scopes:
| API | Hook-level callback | Per-call callback |
|---|---|---|
useCheckoutCreate |
Shared error reporting or analytics for every attempt in the component. | Store one returned capability and navigate for that specific click. |
useScheduleSubscriptionCancellation |
Show a shared notice; the SDK also refreshes subscription queries. | Close the confirmation dialog opened for this one subscription. |
useRemoveSubscriptionCancellation |
Refresh shared account UI or report errors consistently. | Confirm the particular “keep subscription” action. |
useInvoiceDownload |
Apply one download/error policy to every invoice request. | Track or rename the file for the selected order only. |
Pass shared callbacks to the hook. Pass one-off onSuccess or onError callbacks as the second argument to mutate or mutateAsync. CipayBuyerError provides an HTTP status and safe display message; do not log capabilities, PII, or payment secrets.