Integración de carrito
El widget no toca el carrito del host directamente. En su lugar, define un contrato de callbacks que tu integración implementa contra el carrito real (WooCommerce, custom, headless). Esto mantiene el widget agnóstico de plataforma y deja todo el control sobre el estado del carrito en tu lado.
Cuando cart.enabled = true, el widget:
- Lee el carrito vía
onGetCart()al montarse y tras cualquierneuroon:cart-update. - Muta el carrito vía
onAddToCart,onRemoveFromCart,onUpdateQuantitycuando el usuario interactúa con CTAs del widget. - Envía un
cartSnapshotcon cadaPOST /api/widget/search, lo que activa la rama cart-aware del agente conversacional (cross-sell, recordar destinos, etc.).
Contrato CartConfig
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>
}
Tipos completos en
widget/src/types/cart.ts.
CartState
interface CartState {
items: CartItem[]
totalItems: number
subtotal: string // formateado por el host: "232,48 €"
total: string // formateado por el host
currency: string // ISO 4217: "EUR", "USD"
shipping?: CartShippingInfo
}
interface CartItem {
key: string // identificador único del item en el carrito
id: string // productId Neuroon
externalId?: string // host-platform id (e.g. WC post_id)
name: string
price: string // formateado por el host
quantity: number
image?: string
url?: string
variant?: string // "Talla: 42, Color: Negro"
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 // p.ej. "España"
formattedThreshold?: string // p.ej. "50,00 €"
formattedDelta?: string
availableCountries?: { code: string; label: string; threshold?: number }[]
}
subtotal, total, price y los formatted* de shipping son strings ya formateados por el host. Esto evita disputas de redondeo y currency formatting entre regiones.
CartOperationResult
type CartOperationResult =
| { success: true; cart: CartState; notice?: string }
| { success: false; cart?: CartState; error?: string }
Si tu callback devuelve success: true, el widget actualiza su estado interno con la cart retornada. Si devuelve success: false, el widget muestra el error en una toast y no muta su estado.
Puente neuroon:cart-update
El host debe emitir neuroon:cart-update después de cualquier mutación del carrito fuera del widget (mini-cart, página de carrito, otro plugin):
window.dispatchEvent(new CustomEvent('neuroon:cart-update'))
Al recibirlo, el widget:
- Verifica que no haya operaciones del propio widget en vuelo (evita pisar estado optimista).
- Aplica un debounce de 300 ms (algunos hosts emiten múltiples eventos seguidos:
added_to_cart,wc_fragments_refreshed). - Llama a
cart.onGetCart()y actualiza su estado interno.
Lógica en
widget/src/context/CartContext.tsx. El nombre del evento es configurable víacart.externalUpdateEvent.
Ejemplo: WooCommerce con 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>
El plugin oficial de Neuroon para WordPress ya implementa este puente; sólo lo necesitas si tienes una integración custom.
Ejemplo: implementación de CartConfig
window.NeuroonWidget.init({
container: '#neuroon-search',
token: 'WIDGET_TOKEN',
cart: {
enabled: true,
initialCount: window.MyShop.cartCount, // del 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: 'No se pudo añadir' }
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'
},
},
})
Lo que ocurre por debajo
Cuando cart.enabled = true, el widget envía automáticamente un snapshot del carrito al backend en cada búsqueda. Tú no construyes ese snapshot: sale del CartState que devuelves en cart.onGetCart(). El backend lo usa para personalizar resultados (cross-sell, recordar destinos de envío, etc.).
Si el asistente decide que el usuario podría querer añadir/quitar/actualizar un producto, el widget llama a tus callbacks (onAddToCart, onRemoveFromCart, onUpdateQuantity, onCheckout, onSetDestinationCountry?) — siempre tras confirmación visible al usuario para mutaciones destructivas. Tu único trabajo es implementar esos callbacks contra tu carrito real.
Próximas lecturas
- Configuración —
CartConfigen contexto. - Eventos del widget —
neuroon:cart-updatey otros. - Reference → Modelos de datos — schemas completos.