React checkout and hooks
Render checkout natively in your app, observe its lifecycle from sibling components, and recover a failed checkout with one shared flow.
Install
@canopypay/react-checkout@0.3.0 includes the native component, useCheckout and useCheckoutStatus. It requires React and React DOM ^18.3.0 or ^19.0.0, a browser bundler with dynamic imports, and your exact frontend origin registered.
npm install @canopypay/react-checkout@0.3.0Import the stylesheet once in your app. Create the intent on your backend and pass its ID and your publishable merchant key to the client. Keep secret keys on the server. canopyOrigin defaults to https://www.canopypay.io on the React client.
Connect the checkout
Keep the client stable with useMemo. Pass the hook's checkoutProps to one widget, and its opaque checkout handle to any sibling status components. An intentId of null displays the shell while your backend creates the intent; mount it immediately instead of hiding it until ready.
"use client";
import { useMemo } from "react";
import {
CanopyCheckout,
createCheckoutClient,
useCheckout,
useCheckoutStatus,
type CheckoutHandle,
} from "@canopypay/react-checkout";
import "@canopypay/react-checkout/styles.css";
function DepositStatus({ checkout }: { checkout: CheckoutHandle }) {
const status = useCheckoutStatus(checkout);
return (
<p aria-live="polite">
{status.fatalError
? "Checkout unavailable"
: status.paid
? "Payment update received. Checking your account…"
: status.depositDetected
? "Deposit detected"
: status.ready
? "Ready to deposit"
: "Preparing checkout…"}
</p>
);
}
export function Checkout({
merchant,
intentId,
refreshOrder,
}: {
merchant: string;
intentId: string | null;
refreshOrder: () => void;
}) {
const client = useMemo(() => createCheckoutClient({ merchant }), [merchant]);
const flow = useCheckout({ client, intentId, onPaid: refreshOrder });
return (
<section>
<CanopyCheckout {...flow.checkoutProps} />
<DepositStatus checkout={flow.checkout} />
{flow.fatalError && (
<button type="button" onClick={() => void flow.retry().catch(() => {})}>
Retry checkout
</button>
)}
</section>
);
}useCheckoutStatus(checkout) subscribes to the same in-memory state. It makes no requests and starts no timers. Live deposit and payment notifications require the mounted widget; these hooks expose its lifecycle, not a headless payment controller. Render one CanopyCheckout per handle. The handle and status contain no bearer token or prepared response.
Lifecycle state
Both hooks expose these fields. useCheckout additionally returns checkout, checkoutProps and the three controls below. Preparation, shell readiness and usable checkout are separate milestones.
| Field | Meaning |
|---|---|
preparing | A preparation request is pending. |
prepared | Verified preparation has completed. The widget may still be loading. |
shellReady | The mounted shell has reached its first animation frame. |
ready | The widget has reported that checkout is usable. |
depositDetected | The widget has emitted a deposit_detected notification in this flow. |
paid | The widget has emitted a paid notification in this flow. |
error | The latest error, including a nonfatal rejected theme variable, or null. |
fatalError | checkout_unavailable or invalid_features, or null. Theme errors are nonfatal. |
Prepare, retry and reset
prepare(): Promise<void>warms preparation before mounting, or resumes a reset flow. It shares the client's pending work and 45-second cache. A null intent does nothing. Mounting the widget automatically prepares a non-null intent.retry(): Promise<void>clears this intent's cached preparation and lifecycle errors, then restarts it. It can recover a failed runtime load or widget error boundary. Other intents' cached preparations stay available.reset(): voidclears flow state and unmounts its payment widget untilprepare(),retry(), or a new client/intent resumes it. It does not cancel a payment, revoke a session or clear the client cache.
The async controls finish preparation, not widget readiness, and reject with a generic checkout error on preparation failure. Handle the rejection and read fatalError for recovery UI. Reset, retry, intent/client changes and unmount discard stale results and callbacks. Requests already sent may finish in the background.
To warm the UI code separately, call preloadCheckout() and handle its rejection. It loads the manual checkout runtime without wallet discovery. Neither hook makes a network request during render or server rendering. Keep clients scoped to a browser checkout flow; never share one global client between server-rendered users.
Events and callbacks
onEvent receives shell_ready, ready, deposit_detected, paid and error. Typed onReady, onDepositDetected, onPaid and onError callbacks receive the corresponding event object. They are available on both the hook and component. Register a side effect in one place to avoid handling it twice.
Callbacks run after state updates. Replacing them does not restart checkout, and a throwing callback cannot fail checkout or prevent other callbacks. ready and shell_ready fire once until reset, retry or a client/intent change starts a new flow. Later deposit/payment notifications still forward. Theme-variable errors populate error, not fatalError.
The browser loader's ready is a shell handshake. React separates that into shell_ready and ready. Do not copy readiness assumptions between the two integrations.
paid and deposit_detected are UI notifications. Confirm deposits and update balances using your backend order state or verified settlement webhooks.
Upgrade an integration
Update your dependency to @canopypay/react-checkout@0.3.0, rebuild and deploy your app. Existing client / intentId component usage remains supported. To adopt hooks, pass those options to useCheckout and replace the widget's two props with {...flow.checkoutProps}. Do not combine a checkout handle with client or intentId props.
React 0.2.0 introduced the versioned checkout status contract; 0.3.0 includes it and adds hooks. Canopy's hosted server already supports that contract. Self-hosted deployments must serve it before upgrading. Keep your stylesheet import, feature switches and theme options; they work with either component form.
The separate browser package is @canopypay/checkout-sdk@0.7.2, with loader version 10. It uses mount() and onEvent; React hooks belong to react-checkout. npm consumers must update and rebuild to receive package changes. Script-tag integrations can use the rolling /v1/canopy.jsor pin /v1/10/canopy.js.