Controlled Elements (React)
React wrappers around the Cimplify Element iframes. Drop a component into your tree, pass a few props, and you have a working checkout. The iframe still does all the rendering; you just orchestrate it with React.
Two ways in
@cimplify/sdk/react ships two layers:
<CimplifyCheckout>: a single component that renders the unified checkout iframe (auth + address + payment + submit) and exposes lifecycle callbacks. This is whatpay.cimplify.ioitself uses.<ElementsProvider>+ piecewise<AuthElement>/<AddressElement>/<PaymentElement>: for layouts where you want auth above the fold and address/payment below, or want to interleave Cimplify-rendered fields with your own.
Single-component checkout
import { CimplifyClient } from "@cimplify/sdk";
import { CimplifyCheckout } from "@cimplify/sdk/react";
const client = new CimplifyClient({
publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY!,
credentials: "include",
});
export function CheckoutScreen({ cartId }: { cartId: string }) {
return (
<CimplifyCheckout
client={client}
cartId={cartId}
orderTypes={["delivery", "pickup"]}
defaultOrderType="delivery"
submitLabel="Pay now"
appearance={{
theme: "light",
variables: { primaryColor: "#059669", borderRadius: "0.85rem" },
}}
onStatusChange={(status, ctx) => {
console.log(status, ctx.display_text);
}}
onComplete={(result) => {
if (result.success) {
window.location.assign(`/orders/${result.order!.id}`);
}
}}
onError={(err) => console.error(err.code, err.message)}
/>
);
}CimplifyCheckout props
| Prop | Type | Required | Notes |
|---|---|---|---|
client | CimplifyClient | yes | The same client you use elsewhere; the iframe inherits its public key. |
businessId | string | no | Auto-resolved from the public key when omitted. |
cartId | string | no | Auto-resolved via client.cart.get() when omitted. |
locationId | string | no | Pin the order to a specific store location. |
orderTypes | ("delivery" | "pickup" | "dine_in")[] | no | Defaults to ["pickup", "delivery"]. |
defaultOrderType | same as above | no | Pre-selected order type. |
submitLabel | string | no | Override the Pay button copy. |
enrollInLink | boolean | no | Default true. Saves the customer's details for 1-click checkout next time. |
appearance | ElementAppearance | no | Memoize this; reference changes after mount are warned about and ignored. |
linkUrl | string | no | Override the Link host (development). |
demoMode | boolean | no | Skip the API; for screenshots and storybook. |
onComplete | (result: ProcessCheckoutResult) => void | yes | Fires once on terminal success/failure. |
onStatusChange | (status, ctx) => void | no | See checkout lifecycle. |
onError | (err: { code, message }) => void | no | Non-terminal initialization or runtime errors. |
Piecewise Elements
Use <ElementsProvider> when you want Auth, Address, and Payment to live in different parts of your layout. The provider creates a single CimplifyElements controller; child components share token and customer state through it.
import {
ElementsProvider,
AuthElement,
AddressElement,
PaymentElement,
useCheckout,
} from "@cimplify/sdk/react";
function CheckoutForm({ cartId }: { cartId: string }) {
const { process, isLoading } = useCheckout();
return (
<>
<AuthElement
prefillEmail="jane@example.com"
onAuthenticated={(d) => console.log("signed in", d.customerId)}
onRequiresOtp={(d) => console.log("OTP sent to", d.contactMasked)}
onError={(e) => console.error(e)}
/>
<AddressElement mode="shipping" onChange={({ address, saveToLink }) => {/* … */}} />
<PaymentElement amount={2999} currency="GHS" onChange={({ paymentMethod }) => {/* … */}} />
<button
disabled={isLoading}
onClick={async () => {
const result = await process({
cart_id: cartId,
order_type: "delivery",
});
if (result.success) router.push(`/orders/${result.order!.id}`);
}}
>
{isLoading ? "Processing…" : "Pay"}
</button>
</>
);
}
export default function CheckoutScreen({ cartId }: { cartId: string }) {
return (
<ElementsProvider client={client} options={{ appearance: { theme: "light" } }}>
<CheckoutForm cartId={cartId} />
</ElementsProvider>
);
}Component reference
<AuthElement>
| Prop | Type | Notes |
|---|---|---|
prefillEmail | string | Pre-fill the contact field. |
onAuthenticated | (data: AuthenticatedData) => void | Carries token, accountId, customerId, customer. |
onRequiresOtp | (data: { contactMasked }) => void | OTP dispatched. |
onReady / onError | () => void | Lifecycle hooks. |
<AddressElement>
| Prop | Type | Notes |
|---|---|---|
mode | "shipping" | "billing" | Default shipping. |
onChange | ({ address: AddressInfo, saveToLink }) => void | Fires on edit and on saved-address selection. |
<PaymentElement>
| Prop | Type | Notes |
|---|---|---|
amount | number | Cents; drives display only. |
currency | CurrencyCode | Defaults to cart currency. |
onChange | ({ paymentMethod: PaymentMethodInfo, saveToLink }) => void |
No CheckoutElement / AccountElement wrappers
For the unified checkout iframe, use <CimplifyCheckout> (it already wraps that route). For the Link account portal, mount the iframe directly; see AccountElement.
Hooks
| Hook | Returns |
|---|---|
useElements() | CimplifyElements | null. The underlying controller. |
useElementsReady() | boolean. True once the businessId resolves. |
useCheckout() | { submit, process, isLoading }. Submit fires elements.submitCheckout; process fires elements.processCheckout. |
Next
- Appearance API: Theme the iframe
- Vanilla Elements: Same controller without React
Embedded checkout iframe
The same checkout UI as the hosted Pay page, but you mount it as an iframe on your own domain. No SDK needed; just an `<iframe>` and a few postMessage listeners. Use this when you want to stay on your domain but don't need React (or want to mount inside Vue, Svelte, plain HTML, a webview, etc).
Vanilla Elements
The framework-agnostic checkout surface. Use this from Vue, Svelte, plain HTML, or anywhere else React isn't the right tool. The same controller backs the React wrappers; they're just convenience around `CimplifyElements`.