Documentation

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:

  1. Fire xinfer:cart:ready on mount
  2. Respond to xinfer:cart:request with your current cart
  3. Handle xinfer:cart:action events from the widget (add, remove, update, empty)
  4. Push xinfer:cart:sync whenever 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.

EventDirectionPayload
xinfer:cart:readyHost -> Widget{}
xinfer:cart:requestEither -> Either{ source }
xinfer:cart:syncEither -> Either{ source, items: XinferCartItem[] }
xinfer:cart:actionEither -> Either{ source, action, item? }

Actions

ActionDescription
addAdd item to cart. Needs variant_id or a url with ?variant= param.
removeRemove item by variant_id
updateUpdate item quantity
emptyClear 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