JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 17
  • Score
    100M100P100Q83882F
  • License MIT

Universal checkout SDK for Commerce Engine - works with React, Vue, Svelte, Solid, and vanilla JS

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/checkout

Quick 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, /solid

Performance

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:

  1. Initialize once at app root with initCheckout() instead of passing config to every hook
  2. useCheckout() takes no parameters - just returns state and methods
  3. Cleanup with destroyCheckout() in app unmount
  4. Works across the entire app without prop drilling or providers

License

MIT