Shopping Cart Sync
Synchronize the Xinfer AI shopping cart with your store's native cart using DOM events. The widget runs on your page (not an iframe), so both sides share window.
Quick Start
1. Enable Cart Sync
Set cartSync before the widget script loads:
<script>
window.XinferChat = { cartSync: true };
</script>
<script src="https://xinfer.ai/t/your-workspace/widget.js" async></script>
2. Build an Adapter
Your adapter bridges the widget's events to your store's cart API. It must:
- Fire
xinfer:cart:readyon mount - Respond to
xinfer:cart:requestwith your current cart - Handle
xinfer:cart:actionevents from the widget (add, remove, update, empty) - Push
xinfer:cart:syncwhenever your cart changes
Events
All events are CustomEvents on window. Each includes a source field ("host" or "widget") to prevent infinite loops — only handle events from the other side.
| Event | Direction | Payload |
|---|---|---|
xinfer:cart:ready | Host -> Widget | {} |
xinfer:cart:request | Either -> Either | { source } |
xinfer:cart:sync | Either -> Either | { source, items: XinferCartItem[] } |
xinfer:cart:action | Either -> Either | { source, action, item? } |
Actions
| Action | Description |
|---|---|
add | Add item to cart. Needs variant_id or a url with ?variant= param. |
remove | Remove item by variant_id |
update | Update item quantity |
empty | Clear all items (checkout or manual clear) |
XinferCartItem
type XinferCartItem = {
id?: string; // cart line ID (opaque to widget)
product_id?: string;
variant_id?: string; // primary match key
sku?: string; // fallback match key
title: string;
variant_title?: string;
quantity: number;
unit_price: number;
currency?: string;
image_url?: string;
url?: string;
};
The variant_id is the primary key used to match items between carts. When the AI adds items, it may pass a product url with ?variant=12345 instead of variant_id directly — your adapter should extract the variant from the URL as a fallback (see reference adapter below). If your platform doesn't use variant IDs, fall back to sku.
Reference Adapter (React + Shopify)
Below is a complete adapter for a Next.js Shopify store. Adapt the cart API calls to your platform.
"use client";
import { useEffect, useRef } from "react";
import { useCart } from "./cart-context"; // your cart provider
import { addItem, removeItem, updateItemQuantity, emptyCartAction } from "./cart-actions";
type XinferCartItem = {
id?: string;
product_id?: string;
variant_id?: string;
title: string;
variant_title?: string;
quantity: number;
unit_price: number;
currency?: string;
image_url?: string;
url?: string;
};
function dispatch(event: string, detail: unknown) {
window.dispatchEvent(new CustomEvent(event, { detail }));
}
function toXinferItem(item: CartItem): XinferCartItem {
return {
id: item.id,
product_id: item.merchandise.product.id,
variant_id: item.merchandise.id,
title: item.merchandise.product.title,
variant_title: item.merchandise.title !== "Default Title"
? item.merchandise.title : undefined,
quantity: item.quantity,
unit_price: Number(item.cost.totalAmount.amount) / Math.max(item.quantity, 1),
currency: item.cost.totalAmount.currencyCode,
image_url: item.merchandise.product.featuredImage?.url,
url: `/product/${item.merchandise.product.handle}`,
};
}
export function XinferCartAdapter() {
const { cart } = useCart();
const lines = cart?.lines ?? [];
const prevRef = useRef(lines);
// Push cart state on every change
useEffect(() => {
const prev = prevRef.current;
prevRef.current = lines;
dispatch("xinfer:cart:sync", {
source: "host",
items: lines.map(toXinferItem),
});
// Host cart went from items → empty (e.g. after checkout)
if (prev.length > 0 && lines.length === 0) {
dispatch("xinfer:cart:action", { source: "host", action: "empty" });
}
}, [lines]);
// Listen for widget events
useEffect(() => {
function onRequest(e: Event) {
const { source } = (e as CustomEvent).detail ?? {};
if (source === "widget") {
dispatch("xinfer:cart:sync", {
source: "host",
items: lines.map(toXinferItem),
});
}
}
// Resolve variant_id from the item, falling back to ?variant= in the URL
function resolveVariantId(item: XinferCartItem): string | undefined {
if (item.variant_id) {
return item.variant_id.startsWith("gid://")
? item.variant_id
: `gid://shopify/ProductVariant/${item.variant_id}`;
}
if (item.url) {
try {
const url = new URL(item.url, window.location.origin);
const v = url.searchParams.get("variant");
if (v) return `gid://shopify/ProductVariant/${v}`;
} catch {
const m = item.url.match(/[?&]variant=(\d+)/);
if (m) return `gid://shopify/ProductVariant/${m[1]}`;
}
}
return undefined;
}
async function onAction(e: Event) {
const { action, item, source } = (e as CustomEvent).detail ?? {};
if (source !== "widget") return; // ignore own dispatches
if (action === "empty") {
await emptyCartAction();
return;
}
if (!item) return;
const variantId = resolveVariantId(item);
if (!variantId) return;
switch (action) {
case "add":
await addItem(variantId);
break;
case "remove":
await removeItem(variantId);
break;
case "update":
await updateItemQuantity({
merchandiseId: variantId,
quantity: item.quantity ?? 0,
});
break;
}
}
window.addEventListener("xinfer:cart:request", onRequest);
window.addEventListener("xinfer:cart:action", onAction);
dispatch("xinfer:cart:ready", {});
return () => {
window.removeEventListener("xinfer:cart:request", onRequest);
window.removeEventListener("xinfer:cart:action", onAction);
};
}, [lines]);
return null; // renderless
}
Mount inside your cart provider so it has access to cart state:
// app/layout.tsx
<CartProvider>
<XinferCartAdapter />
{children}
</CartProvider>
Vanilla JS Adapter
If you're not using React, the same pattern works with plain JavaScript:
<script>
window.XinferChat = { cartSync: true };
function dispatch(event, detail) {
window.dispatchEvent(new CustomEvent(event, { detail }));
}
function pushCart() {
// Replace with your cart API
const items = getYourCartItems().map(item => ({
variant_id: item.variantId,
title: item.name,
quantity: item.qty,
unit_price: item.price,
image_url: item.image,
}));
dispatch("xinfer:cart:sync", { source: "host", items });
}
window.addEventListener("xinfer:cart:request", (e) => {
if (e.detail?.source === "widget") pushCart();
});
window.addEventListener("xinfer:cart:action", async (e) => {
const { action, item, source } = e.detail ?? {};
if (source !== "widget") return;
switch (action) {
case "add":
await yourCartApi.add(item.variant_id, 1);
break;
case "remove":
await yourCartApi.remove(item.variant_id);
break;
case "update":
await yourCartApi.update(item.variant_id, item.quantity);
break;
case "empty":
await yourCartApi.clear();
break;
}
pushCart(); // re-sync after mutation
});
// Signal ready, then push initial cart
dispatch("xinfer:cart:ready", {});
pushCart();
</script>
<script src="https://xinfer.ai/t/your-workspace/widget.js" async></script>
How It Works
Sync Flow
Host adapter mounts
→ fires xinfer:cart:ready
→ widget hears ready, fires xinfer:cart:request { source: "widget" }
→ host responds with xinfer:cart:sync { source: "host", items: [...] }
→ widget stores host cart in memory
AI Adds Item
User asks AI to add item → shopping_cart tool runs server-side
→ widget fires xinfer:cart:sync { source: "widget", items: [...] }
→ widget fires xinfer:cart:action { source: "widget", action: "add", item }
→ host adapter hears action → calls addItem(variantId)
→ host cart updates → pushes xinfer:cart:sync { source: "host", items: [...] }
AI Checkout
AI calls shopping_cart "submit" → order created, cart emptied
→ widget fires xinfer:cart:action { source: "widget", action: "empty" }
→ host adapter empties its cart
Host Checkout
User checks out on host site → cart empties
→ adapter detects lines went from >0 to 0
→ fires xinfer:cart:action { source: "host", action: "empty" }
→ widget empties its DB cart via DELETE /api/embed/cart
Full Chat Tab (Socket.IO)
When a user opens full chat in a new tab, cart mutations are pushed to the widget tab via Socket.IO so the host store stays in sync — no extra code needed in your adapter.
Live Demo
Visit the Widget Embed Demo page on any tenant to test cart sync in real-time. The page includes:
- Host Cart Simulator — a mock store cart that acts as the adapter
- Event Monitor — shows all
xinfer:cart:*events firing with timestamps and payloads - Full Chat link — open full chat in a new tab to test Socket.IO cross-tab sync
Related Pages
- Shopping Carts - Admin guide for managing carts
- Orders - View submitted orders
- Chat Clients - Widget installation and configuration
- Integration - Get embed codes and setup