Controlled Elements Checkout
Tier 2 -- use React components with custom layout. The unified checkout element handles auth, address, payment, and order creation in a single iframe.
Recommended: For most integrations, use CimplifyCheckout (Tier 1). Use Tier 2 only when you need custom layout around auth or need to control element placement in your page.
Primary Pattern: CimplifyCheckout
The unified checkout element handles the entire flow internally -- auth, address collection (for delivery), payment, and order creation.
"use client";
import { createCimplifyClient } from "@cimplify/sdk";
import { CimplifyCheckout } from "@cimplify/sdk/react";
const client = createCimplifyClient({
publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY,
});
export default function CheckoutPage({ cartId }: { cartId: string }) {
return (
<CimplifyCheckout
client={client}
cartId={cartId}
locationId="loc_1"
orderTypes={["pickup", "delivery"]}
defaultOrderType="pickup"
enrollInLink={true}
appearance={{
theme: "light",
variables: { primaryColor: "#111827", borderRadius: "12px" },
}}
onStatusChange={(status, context) => {
console.log(status, context.display_text);
}}
onComplete={(result) => {
if (result.success) {
window.location.href = "/order/" + result.order?.id;
}
}}
onError={(error) => console.error(error)}
/>
);
}Advanced Pattern: AuthElement + CimplifyCheckout
Use ElementsProvider + AuthElement for a standalone sign-in step, then render CimplifyCheckout for the rest. This is useful when you want to show sign-in on a separate page or need to capture the auth token before proceeding.
"use client";
import { createCimplifyClient } from "@cimplify/sdk";
import {
ElementsProvider,
AuthElement,
CimplifyCheckout,
} from "@cimplify/sdk/react";
import { useState } from "react";
const client = createCimplifyClient({
publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY,
});
export default function CheckoutPage({ cartId }: { cartId: string }) {
const [authed, setAuthed] = useState(false);
return (
<ElementsProvider
client={client}
businessId="bus_123"
options={{
appearance: {
theme: "light",
variables: { primaryColor: "#111827", borderRadius: "12px" },
},
}}
>
{!authed ? (
<div>
<h2>Sign in</h2>
<AuthElement
onAuthenticated={(data) => {
console.log("Customer:", data.customerId);
setAuthed(true);
}}
onRequiresOtp={(data) => {
console.log("OTP sent to:", data.contactMasked);
}}
onError={(err) => console.error(err.message)}
/>
</div>
) : (
<CimplifyCheckout
client={client}
cartId={cartId}
orderTypes={["pickup", "delivery"]}
onComplete={(result) => {
if (result.success) {
window.location.href = "/order/" + result.order?.id;
}
}}
onError={(error) => console.error(error)}
/>
)}
</ElementsProvider>
);
}ElementsProvider Props
| Prop | Type | Description |
|---|---|---|
client | CimplifyClient | Required. Created via createCimplifyClient(). |
businessId? | string | If omitted, resolved from the public key. |
options? | { appearance?, linkUrl? } | Theme and element settings. |
AuthElement
Standalone sign-in element. This is the only element that renders a true standalone form -- AddressElement and PaymentElement now render the full unified checkout iframe.
| Prop | Type |
|---|---|
prefillEmail? | string |
className? | string |
style? | React.CSSProperties |
onAuthenticated? | (data: { token, accountId, customerId, customer }) => void |
onRequiresOtp? | (data: { contactMasked }) => void |
onReady? | () => void |
onError? | (err: { code, message }) => void |
<AuthElement
prefillEmail="customer@example.com"
onAuthenticated={(data) => {
console.log("Token:", data.token);
console.log("Customer:", data.customerId);
}}
onRequiresOtp={(data) => {
console.log("OTP sent to:", data.contactMasked);
}}
/>Cart Summary & Built-in Submit
The CimplifyCheckout component automatically fetches cart data and sends it to the checkout iframe for an inline order summary (line items, tax, totals). The iframe also renders its own submit button — when pressed, it emits a request_submit event that the SDK handles automatically by calling processCheckout().
You don't need to render a submit button yourself — the checkout element includes one. If you omit cartId, the SDK resolves the active cart automatically.
useCheckout()
Returns { submit, process, isLoading }. process gives full control with status callbacks and timeout; submit is a simpler wrapper.
const { process, isLoading } = useCheckout();
const result = await process({
cart_id: cartId,
order_type: "delivery",
location_id: locationId,
notes: "Ring the bell",
tip_amount: 200,
scheduled_time: "2025-06-15T12:00:00Z",
enroll_in_link: true,
pay_currency: "GHS",
timeout_ms: 180_000,
on_status_change: (status, context) => {
setStatus(status);
setStatusText(context.display_text || "");
},
});
if (result.success) {
router.push("/order/" + result.order?.id);
} else if (result.error?.recoverable) {
// Show retry button
}Legacy: Composed Elements
Backward compatibility: AddressElement and PaymentElement still exist and are exported, but they now render the full unified checkout iframe internally (not standalone forms). The composed pattern below still works but is no longer the recommended approach.
// Legacy composed pattern -- still works via backward compatibility.
// AddressElement and PaymentElement mount the full unified checkout iframe.
// Prefer CimplifyCheckout or AuthElement + CimplifyCheckout instead.
function LegacyCheckoutForm({ cartId }: { cartId: string }) {
const { process, isLoading } = useCheckout();
return (
<div>
<AuthElement
onAuthenticated={(data) => console.log("Authed:", data.customerId)}
/>
<AddressElement mode="shipping" />
<PaymentElement />
<button disabled={isLoading} onClick={() => process({ cart_id: cartId, order_type: "delivery" })}>
{isLoading ? "Processing..." : "Pay now"}
</button>
</div>
);
}