Skip to main content

Cart integration

The widget never touches the host cart directly. Instead, it defines a callback contract that your integration implements against the real cart (WooCommerce, custom, headless). This keeps the widget platform-agnostic and leaves all cart-state control on your side.

When cart.enabled = true, the widget:

  1. Reads the cart via onGetCart() on mount and after every neuroon:cart-update.
  2. Mutates the cart via onAddToCart, onRemoveFromCart, onUpdateQuantity when the user interacts with widget CTAs.
  3. Sends a cartSnapshot with every POST /api/widget/search, which activates the cart-aware branch of the conversational agent (cross-sell, remembering destinations, etc.).

CartConfig contract

interface CartConfig {
enabled: boolean
initialCount?: number
onGetCart: () => Promise<CartState>
onAddToCart: (productId: string, quantity: number, externalProductId?: string) => Promise<CartOperationResult>
onRemoveFromCart: (itemKey: string) => Promise<CartOperationResult>
onUpdateQuantity: (itemKey: string, quantity: number) => Promise<CartOperationResult>
onCheckout: () => void
canAddToCart?: (product: Product) => boolean
externalUpdateEvent?: string // default 'neuroon:cart-update'
onSetDestinationCountry?: (countryCode: string) => Promise<CartOperationResult | void>
}

Full types in widget/src/types/cart.ts.

CartState

interface CartState {
items: CartItem[]
totalItems: number
subtotal: string // host-formatted: "$232.48"
total: string // host-formatted
currency: string // ISO 4217: "EUR", "USD"
shipping?: CartShippingInfo
}

interface CartItem {
key: string // unique item id in the cart
id: string // Neuroon productId
externalId?: string // host-platform id (e.g. WC post_id)
name: string
price: string // host-formatted
quantity: number
image?: string
url?: string
variant?: string // "Size: 42, Color: Black"
maxQuantity?: number
}

interface CartShippingInfo {
currentAmount: number
thresholdMet: boolean
needsCountryResolution?: boolean
freeShippingThreshold?: number
delta?: number // max(0, threshold - currentAmount)
destinationCountry?: string // ISO-3166-1 alpha-2 (uppercase)
destinationLabel?: string // e.g. "Spain"
formattedThreshold?: string // e.g. "$50.00"
formattedDelta?: string
availableCountries?: { code: string; label: string; threshold?: number }[]
}

subtotal, total, price and shipping formatted* fields are already-formatted strings by the host. This avoids rounding and currency-formatting disputes across regions.

CartOperationResult

type CartOperationResult =
| { success: true; cart: CartState; notice?: string }
| { success: false; cart?: CartState; error?: string }

If your callback returns success: true, the widget updates its internal state with the returned cart. If it returns success: false, the widget shows the error in a toast and does not mutate its state.

neuroon:cart-update bridge

The host must emit neuroon:cart-update after any cart mutation outside the widget (mini-cart, cart page, another plugin):

window.dispatchEvent(new CustomEvent('neuroon:cart-update'))

When it receives the event, the widget:

  1. Verifies that no widget-originated operations are in flight (avoids overwriting optimistic state).
  2. Applies a 300 ms debounce (some hosts emit several events in a row: added_to_cart, wc_fragments_refreshed).
  3. Calls cart.onGetCart() and updates its internal state.

Logic in widget/src/context/CartContext.tsx. The event name is configurable via cart.externalUpdateEvent.

Example: WooCommerce with jQuery

<script>
(function() {
function notifyNeuroon() {
window.dispatchEvent(new CustomEvent('neuroon:cart-update'))
}
if (window.jQuery) {
jQuery(document.body).on(
'added_to_cart removed_from_cart wc_fragments_refreshed updated_cart_totals',
notifyNeuroon
)
}
})()
</script>

The official Neuroon plugin for WordPress already ships this bridge; you only need it for a custom integration.

Example: CartConfig implementation

window.NeuroonWidget.init({
container: '#neuroon-search',
token: 'WIDGET_TOKEN',
cart: {
enabled: true,
initialCount: window.MyShop.cartCount, // from cookie/SSR
async onGetCart() {
const r = await fetch('/api/cart', { credentials: 'include' })
const data = await r.json()
return {
items: data.items.map(i => ({
key: i.line_item_key,
id: i.product_id,
externalId: String(i.post_id),
name: i.title,
price: i.formatted_price,
quantity: i.qty,
image: i.thumbnail,
url: i.permalink,
variant: i.variant_label,
maxQuantity: i.stock_qty,
})),
totalItems: data.total_items,
subtotal: data.formatted_subtotal,
total: data.formatted_total,
currency: data.currency,
}
},
async onAddToCart(productId, quantity, externalProductId) {
const r = await fetch('/api/cart/add', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: externalProductId || productId,
quantity,
}),
})
if (!r.ok) return { success: false, error: 'Could not add item' }
return { success: true, cart: await this.onGetCart() }
},
async onRemoveFromCart(itemKey) {
await fetch(`/api/cart/${itemKey}`, { method: 'DELETE', credentials: 'include' })
return { success: true, cart: await this.onGetCart() }
},
async onUpdateQuantity(itemKey, quantity) {
await fetch(`/api/cart/${itemKey}`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity }),
})
return { success: true, cart: await this.onGetCart() }
},
onCheckout() {
window.location.href = '/checkout'
},
},
})

What happens under the hood

When cart.enabled = true, the widget automatically sends a cart snapshot to the backend on every search. You don't build that snapshot: it comes from the CartState you return from cart.onGetCart(). The backend uses it to personalize results (cross-sell, remember shipping destinations, etc.).

If the assistant decides the user might want to add/remove/update a product, the widget calls your callbacks (onAddToCart, onRemoveFromCart, onUpdateQuantity, onCheckout, onSetDestinationCountry?) — always after a visible user confirmation for destructive mutations. Your only job is to implement those callbacks against your real cart.

Further reading