Package Exports
- @commercengine/checkout
- @commercengine/checkout/package.json
- @commercengine/checkout/react
- @commercengine/checkout/solid
- @commercengine/checkout/svelte
- @commercengine/checkout/vue
Readme
@commercengine/checkout
Universal checkout SDK for Commerce Engine. Works with React, Vue, Svelte, Solid, and vanilla JavaScript.
Features
- Universal - One package for all frameworks
- No Provider Required - Initialize once, use anywhere
- Reactive State - Automatic UI updates when cart changes
- Type Safe - Full TypeScript support
- Tree Shakeable - Only bundle what you use
Architecture
Designed for large applications with multiple routes (Next.js, Remix, etc.):
- Singleton Store - One checkout instance shared across all components and routes
- Non-blocking Init -
initCheckout()returns immediately, iframe loads in background - SSR Safe - Initialization only runs on client, server renders with default state
- Zero Core Web Vitals Impact:
- LCP: Iframe is hidden, doesn't affect largest contentful paint
- INP/FID: No main thread blocking during initialization
- CLS: Fixed positioning, no layout shifts
┌─────────────────────────────────────────────────────────────┐
│ App Root (layout.tsx / _app.tsx) │
│ └─ initCheckout() ──► Creates hidden iframe (async) │
├─────────────────────────────────────────────────────────────┤
│ Any Component (header, product page, cart page...) │
│ └─ useCheckout() ──► Same singleton store │
├─────────────────────────────────────────────────────────────┤
│ Route Changes (SPA navigation) │
│ └─ Checkout instance persists, no re-initialization │
└─────────────────────────────────────────────────────────────┘Installation
npm install @commercengine/checkout
# or
pnpm add @commercengine/checkout
# or
yarn add @commercengine/checkoutQuick Start
React / Next.js / Remix
import { useCheckout, initCheckout, destroyCheckout } from "@commercengine/checkout/react";
import { useEffect } from "react";
// Initialize once at app root
function App({ children }) {
useEffect(() => {
initCheckout({
storeId: "store_xxx",
apiKey: "ak_xxx",
onComplete: (order) => {
console.log("Order placed:", order.orderNumber);
},
});
return () => destroyCheckout();
}, []);
return <>{children}</>;
}
// Use anywhere - no provider needed
function Header() {
const { cartCount, openCart, isReady } = useCheckout();
return (
<button onClick={openCart} disabled={!isReady}>
Cart ({cartCount})
</button>
);
}
function ProductCard({ product }) {
const { addToCart } = useCheckout();
return (
<button onClick={() => addToCart(product.id, product.variantId, 1)}>
Add to Cart
</button>
);
}
// User state is also reactive
function UserGreeting() {
const { isLoggedIn, user } = useCheckout();
if (!isLoggedIn) return <span>Guest</span>;
return <span>Welcome, {user?.firstName || user?.email}</span>;
}Vue / Nuxt
<script setup>
import { useCheckout, initCheckout, destroyCheckout } from "@commercengine/checkout/vue";
import { onMounted, onUnmounted } from "vue";
// Initialize once at app root (App.vue or plugin)
onMounted(() => {
initCheckout({
storeId: "store_xxx",
apiKey: "ak_xxx",
});
});
onUnmounted(() => {
destroyCheckout();
});
// Use anywhere
const { cartCount, openCart, isReady } = useCheckout();
</script>
<template>
<button @click="openCart" :disabled="!isReady">
Cart ({{ cartCount }})
</button>
</template>Svelte / SvelteKit
<script>
import { checkout, initCheckout, destroyCheckout } from "@commercengine/checkout/svelte";
import { onMount, onDestroy } from "svelte";
// Initialize once at app root (+layout.svelte)
onMount(() => {
initCheckout({
storeId: "store_xxx",
apiKey: "ak_xxx",
});
});
onDestroy(() => {
destroyCheckout();
});
</script>
<button on:click={() => $checkout.openCart()} disabled={!$checkout.isReady}>
Cart ({$checkout.cartCount})
</button>Or use individual stores for fine-grained reactivity:
<script>
import { cartCount, isReady } from "@commercengine/checkout/svelte";
</script>
<span class="badge">{$cartCount}</span>Solid / SolidStart
import { useCheckout, initCheckout, destroyCheckout } from "@commercengine/checkout/solid";
import { onMount, onCleanup } from "solid-js";
// Initialize once at app root
function App(props) {
onMount(() => {
initCheckout({
storeId: "store_xxx",
apiKey: "ak_xxx",
});
});
onCleanup(() => {
destroyCheckout();
});
return <>{props.children}</>;
}
// Use anywhere
function Header() {
const checkout = useCheckout();
return (
<button onClick={() => checkout.openCart()} disabled={!checkout.isReady}>
Cart ({checkout.cartCount})
</button>
);
}Vanilla JavaScript
import { initCheckout, getCheckout, subscribeToCheckout } from "@commercengine/checkout";
// Initialize
initCheckout({
storeId: "store_xxx",
apiKey: "ak_xxx",
onComplete: (order) => {
window.location.href = `/thank-you?order=${order.orderNumber}`;
},
});
// Use anywhere
document.getElementById("cart-btn").onclick = () => {
getCheckout().openCart();
};
// Subscribe to cart updates
subscribeToCheckout(
(state) => state.cartCount,
(count) => {
document.getElementById("cart-badge").textContent = count.toString();
}
);Configuration
Pass configuration to initCheckout():
initCheckout({
// Required
storeId: "store_xxx",
apiKey: "ak_xxx",
// Environment (default: "production")
environment: "production", // or "staging"
// Or use custom URL for local development
url: "http://localhost:8080",
// Theme (default: "system")
theme: "light", // or "dark" or "system"
// Drawer direction per viewport
drawerDirection: {
mobile: "bottom", // "bottom" | "top" | "left" | "right" | "modal"
desktop: "right", // "bottom" | "top" | "left" | "right" | "modal"
},
// Appearance
appearance: {
zIndex: 99999,
},
// Authentication
authMode: "managed", // or "provided"
accessToken: "...", // if user already logged in
refreshToken: "...",
// Quick Buy (add product on init)
quickBuy: {
productId: "prod_xxx",
variantId: "var_xxx", // or null
quantity: 1,
},
sessionMode: "continue-existing", // or "force-new"
autoDetectQuickBuy: false, // parse URL params
// Features
features: {
loyalty: true, // Loyalty points redemption (default: true)
coupons: true, // Coupon/promo codes (default: true)
collectInStore: false, // Collect-in-store option (default: false)
freeShippingProgress: true, // Free shipping progress tracker (default: true)
productRecommendations: true, // Product recommendations in cart (default: true)
},
// Callbacks
onReady: () => {},
onOpen: () => {},
onClose: () => {},
onComplete: (order) => {},
onCartUpdate: (cart) => {},
onTokensUpdated: (data) => {},
onLogin: (data) => {},
onLogout: (data) => {},
onSessionError: () => {},
onError: (error) => {},
});API Reference
Initialization
| Function | Description |
|---|---|
initCheckout(config) |
Initialize checkout with configuration |
destroyCheckout() |
Destroy checkout instance and cleanup |
Hooks / Composables
| Framework | Import |
|---|---|
| React | useCheckout() from @commercengine/checkout/react |
| Vue | useCheckout() from @commercengine/checkout/vue |
| Svelte | checkout store from @commercengine/checkout/svelte |
| Solid | useCheckout() from @commercengine/checkout/solid |
Return Values
| Property | Type | Description |
|---|---|---|
isReady |
boolean |
Whether checkout is ready to use |
isOpen |
boolean |
Whether checkout overlay is open |
cartCount |
number |
Number of items in cart |
cartTotal |
number |
Cart subtotal amount |
cartCurrency |
string |
Currency code (e.g., "USD") |
isLoggedIn |
boolean |
Whether user is logged in |
user |
UserInfo | null |
Current user info (null if not logged in) |
| Method | Description |
|---|---|
openCart() |
Open the cart drawer |
openCheckout() |
Open checkout directly (Buy Now flow) |
close() |
Close the checkout overlay |
addToCart(productId, variantId, quantity?) |
Add item to cart |
updateTokens(accessToken, refreshToken?) |
Update auth tokens (pre-isReady safe — buffered until the iframe can receive) |
Vanilla JS Helpers
| Function | Description |
|---|---|
getCheckout() |
Get current state and methods (non-reactive) |
subscribeToCheckout(listener) |
Subscribe to all state changes |
subscribeToCheckout(selector, listener) |
Subscribe to specific state |
checkoutStore |
Direct access to Zustand store |
TypeScript
All types are exported for convenience:
import type {
// Configuration
CheckoutConfig,
CheckoutFeatures,
DrawerDirection,
DrawerDirectionConfig,
Environment,
AuthMode,
SessionMode,
QuickBuyConfig,
// State
CartData,
OrderData,
UserInfo,
ErrorData,
// Auth events
AuthTokensUpdatedData,
AuthLoginData,
AuthLogoutData,
// Store types
CheckoutState,
CheckoutActions,
CheckoutStore,
} from "@commercengine/checkout/react"; // or /vue, /svelte, /solidPerformance
Two tuning levers for time-to-interactive on the embedded checkout. Both are optional and additive.
Preconnect to the checkout origin
The first time a user visits a page that embeds checkout, the browser pays a
cold DNS + TLS handshake to a brand-new origin
({storeId}.checkout.commercengine.com). Warming that connection in <head>
before the iframe is created shaves the handshake cost off the first render.
Use getCheckoutOrigin() to build the URL — it's re-exported from every
framework binding so you don't have to hardcode the subdomain convention:
// Next.js (app/layout.tsx)
import { getCheckoutOrigin } from "@commercengine/checkout/react";
const origin = getCheckoutOrigin({
storeId: process.env.NEXT_PUBLIC_CE_STORE_ID!,
environment: "production",
});
export default function RootLayout({ children }) {
return (
<html>
<head>
<link rel="preconnect" href={origin} crossOrigin="anonymous" />
<link rel="dns-prefetch" href={origin} />
</head>
<body>{children}</body>
</html>
);
}Same import path for Vue / Svelte / Solid bindings (@commercengine/checkout/vue,
/svelte, /solid). Also available as getCheckoutIframeUrl() if you want
the full URL with query params (e.g. for <link rel="prefetch" as="document">).
Parallel init (cold visitor speedup)
When authMode: "provided" and the parent doesn't have a cached token, you
can call initCheckout() first and updateTokens() after — instead of
serializing the iframe boot behind the token fetch.
// Serial (default): token fetch blocks iframe creation
useEffect(() => {
async function init() {
const { accessToken, refreshToken } = await sessionStorefront.auth.getAnonymousToken();
initCheckout({ storeId, apiKey, authMode: "provided", accessToken, refreshToken });
}
init();
}, []);// Parallel: iframe starts loading immediately, tokens delivered when ready
useEffect(() => {
async function init() {
initCheckout({
storeId, apiKey, authMode: "provided",
// No accessToken — iframe waits for tokens via postMessage
});
const { accessToken, refreshToken } = await sessionStorefront.auth.getAnonymousToken();
// Safe to call before isReady — buffered until the iframe can receive.
getCheckout().updateTokens(accessToken, refreshToken);
}
init();
}, []);The parent buffers updateTokens() calls made before the iframe has bound its
postMessage handler, then flushes them as soon as it can. The iframe then
hydrates with the delivered tokens and emits ready normally.
When this helps: first-time visitors with no token in storage. The anonymous-token round-trip (~100-300ms typical) overlaps with iframe boot instead of blocking it.
When this doesn't help: returning visitors with a cached token — pass it
upfront via accessToken as usual. There's nothing to parallelize.
Migration from @commercengine/react
If you're migrating from the old @commercengine/react package:
Before:
import { useCheckout } from "@commercengine/react";
function App() {
const { openCart, cartCount } = useCheckout({
storeId: "store_xxx",
apiKey: "ak_xxx",
onComplete: (order) => { ... },
});
return <button onClick={openCart}>Cart ({cartCount})</button>;
}After:
import { useCheckout, initCheckout, destroyCheckout } from "@commercengine/checkout/react";
import { useEffect } from "react";
// Initialize once at app root
function App({ children }) {
useEffect(() => {
initCheckout({
storeId: "store_xxx",
apiKey: "ak_xxx",
onComplete: (order) => { ... },
});
return () => destroyCheckout();
}, []);
return <>{children}</>;
}
// Use anywhere without config
function Header() {
const { openCart, cartCount } = useCheckout();
return <button onClick={openCart}>Cart ({cartCount})</button>;
}Key differences:
- Initialize once at app root with
initCheckout()instead of passing config to every hook useCheckout()takes no parameters - just returns state and methods- Cleanup with
destroyCheckout()in app unmount - Works across the entire app without prop drilling or providers
License
MIT