@storefront-ui-plugin/cart-checkout-plugin
v3.1.0
Published
Reusable cart and checkout SDK for multi-tenant React storefront applications
Readme
@ecommerce-store/cart-checkout-plugin
A small React SDK for cart state and persistence in multi-tenant storefronts. Supports local-only mode (localStorage per tenant) or API mode (sync with your backend: guest session, add/view/update/remove/clear, checkout, merge guest cart on login).
Requirements: React 18+
For flow, requirements checklist, and where to debug, see ARCHITECTURE.md.
Install
npm install @ecommerce-store/cart-checkout-pluginQuick start
- Wrap your app with
CartProviderand pass atenantId(e.g. store or site id):
import { CartProvider } from "@ecommerce-store/cart-checkout-plugin";
function App() {
return (
<CartProvider tenantId="my-store-id">
<YourApp />
</CartProvider>
);
}- Use
useCartin any child to read the cart and call actions:
import { useCart } from "@ecommerce-store/cart-checkout-plugin";
function ProductCard({ product }) {
const { addItem } = useCart();
return (
<button
onClick={() =>
addItem({
id: product.id,
name: product.name,
price: product.price,
quantity: 1,
})
}
>
Add to cart
</button>
);
}
function CartPage() {
const { items, summary, removeItem, updateQuantity, clearCart } = useCart();
return (
<div>
{items.map((item) => (
<div key={item.id}>
{item.name} × {item.quantity} — ${(item.price ?? 0) * item.quantity}
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
<p>Subtotal: ${summary.subtotal}</p>
<p>Items: {summary.itemCount}</p>
<button onClick={clearCart}>Clear cart</button>
</div>
);
}API
CartProvider
| Prop | Type | Default | Description |
|---------------------|----------|-----------|--------------------------------------------------|
| tenantId | string | required | Tenant/store id. Cart is stored per tenant. |
| storageKeyPrefix | string | "cart" | Prefix for localStorage key (local-only mode). |
| children | ReactNode | required | Your app or subtree. |
| apiBaseUrl | string | — | When set, enables API mode (see below). |
| storeId | string | — | Required when apiBaseUrl is set. |
| getHeaders | () => Record<string, string> | — | Optional. Return headers for each request (e.g. { "X-User-Id": userId } for logged-in users). |
useCart()
Returns:
| Property | Type | Description |
|-----------------|-------------------------|-------------|
| items | CartItem[] | Current line items. |
| summary | CartSummary | { subtotal, total, itemCount }. |
| tenantId | string | Current tenant id. |
| isEmpty | boolean | items.length === 0. |
| addItem | (item, quantity?) => void | Add or merge by id. |
| removeItem | (id) => void | Remove line by id. |
| updateQuantity| (id, quantity) => void| Set quantity; remove if ≤ 0. |
| clearCart | () => void | Remove all items. |
| isSyncing | boolean | API mode only. True while loading or mutating. |
| lastError | Error \| null | API mode only. Last API error, clear on next success. |
| mergeGuestCart| () => Promise<void> | API mode only. Call after login to merge guest cart into user cart. |
useCheckout()
API mode only. Returns:
| Property | Type | Description |
|-----------------|-------------------------|-------------|
| startCheckout | (body?) => Promise<CheckoutResponse \| null> | POST /checkout/?tenant_id&store_id (query when set); empty body unless you pass a JSON body. |
| isPending | boolean | True while checkout request is in flight. |
| error | Error \| null | Checkout request error. |
| result | CheckoutResponse \| null | Last successful response (checkout_url, order_id, etc.). |
Types
CartItem—id(string or number), optionalname,price,quantity(defaults to 1 when adding), and any extra fields.CartSummary—{ subtotal, total, itemCount }.CheckoutResponse— Typed response from checkout (e.g.checkout_url,order_id). Import when usinguseCheckout.CartContextValue— Full context type if you need it.
import type { CartItem, CartSummary, CheckoutResponse } from "@ecommerce-store/cart-checkout-plugin";API mode (backend sync)
When you pass apiBaseUrl and storeId, the plugin switches to API mode:
- Guest session: The plugin calls
POST /cart/guest/sessionto get a guest cart id, stores it in localStorage (keycart_guest_id:{tenantId}:{storeId}), and sends it as theX-Guest-Cart-Idheader on all cart requests. - Logged-in users: Provide
getHeaders={() => ({ "X-User-Id": currentUserId })}. The plugin sendsX-User-Idinstead of (or with) the guest id. After login, callmergeGuestCart()so the backend merges the guest cart into the user cart; the plugin then stops using the stored guest id and refetches the cart. - Endpoints used:
GET /cart/view,POST /cart/add,PUT /cart/item/{item_id},DELETE /cart/item/{item_id},DELETE /cart/clear?tenant_id&store_id,POST /cart/merge-guest-cart,POST /checkout/?tenant_id&store_id(checkout body empty by default). Request bodies and query params usetenant_idandstore_idas required by your API.
Example:
<CartProvider
tenantId={tenantId}
storeId={storeId}
apiBaseUrl="https://api.example.com"
getHeaders={() => (user ? { "X-User-Id": user.id } : {})}
>
<App />
</CartProvider>
// After user logs in:
const { mergeGuestCart } = useCart();
await mergeGuestCart();
// Checkout:
const { startCheckout, isPending, result } = useCheckout();
const res = await startCheckout();
if (res?.checkout_url) window.location.href = res.checkout_url;Multi-tenant behavior
- Local-only mode: Each
tenantIdgets its own cart in localStorage under the key{storageKeyPrefix}:{tenantId}. Persistence runs only after hydration so the cart is not overwritten on first paint. - API mode: Cart is stored on the server. Guest cart id is stored per tenant+store and reused across refreshes. Use
mergeGuestCart()after login to merge guest cart into the user cart.
Building from source
npm install
npm run buildOutput is in dist/: ESM and CJS bundles plus TypeScript declarations.
