Recipe: Custom cart bridge
The widget listens for the neuroon:cart-update event on window. When it receives it, it refreshes its responses: cross-sell, "already in cart" validations, contextual prompts.
If your storefront does not use WooCommerce or an official plugin, emit the event yourself from the code that mutates the cart.
Payload schema
type CartItem = {
productId: string; // your externalId / SKU (must match the one synced in Neuroon)
name: string;
price: number;
quantity: number;
imageUrl?: string;
variantId?: string;
attributes?: Record<string, string>;
};
type CartPayload = {
items: CartItem[];
total: number;
currency: string; // ISO 4217
itemsCount?: number; // optional, can be inferred from items
};
If your cart has sensitive calculations (offers, tax) that you prefer not to expose to the frontend, dispatch the event without
detail. The widget will ask the server for the updated state.
"Ping" mode (no payload)
window.dispatchEvent(new CustomEvent('neuroon:cart-update'));
This is the simplest and most private pattern: the widget will query the backend.
"Full" mode (with payload)
window.dispatchEvent(new CustomEvent('neuroon:cart-update', {
detail: {
items: [
{ productId: 'SKU-001', name: 'Smart Speaker', price: 79.99, quantity: 2 }
],
total: 159.98,
currency: 'EUR'
}
}));
Advantage: the widget does not need a second server round-trip.
Vanilla JS
<script>
(function () {
const debounce = (fn, ms) => {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
};
const sendCartUpdate = debounce(() => {
const cart = window.__cart; // your global store
window.dispatchEvent(new CustomEvent('neuroon:cart-update', {
detail: {
items: cart.lines.map(l => ({
productId: l.sku,
name: l.name,
price: l.price,
quantity: l.quantity,
imageUrl: l.imageUrl,
})),
total: cart.total,
currency: cart.currency,
}
}));
}, 200);
document.addEventListener('cart:added', sendCartUpdate);
document.addEventListener('cart:removed', sendCartUpdate);
document.addEventListener('cart:cleared', sendCartUpdate);
})();
</script>
The 200 ms debounce mirrors what the official plugin does (see ).
jQuery (legacy stores)
(function ($) {
if (!$) return;
var debounceTimer = null;
$(document.body).on('myStore:cart-changed', function () {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function () {
window.dispatchEvent(new CustomEvent('neuroon:cart-update'));
}, 200);
});
})(window.jQuery);
React / Next.js
With a client store (Zustand, Redux, Context):
// app/components/CartBridge.tsx
'use client';
import { useEffect } from 'react';
import { useCartStore } from '@/store/cart';
export function CartBridge() {
const cart = useCartStore((s) => s.snapshot()); // stable selector
useEffect(() => {
const t = setTimeout(() => {
window.dispatchEvent(new CustomEvent('neuroon:cart-update', {
detail: {
items: cart.lines.map((l) => ({
productId: l.sku, name: l.name, price: l.price, quantity: l.quantity,
})),
total: cart.total,
currency: cart.currency,
},
}));
}, 200);
return () => clearTimeout(t);
}, [cart]);
return null;
}
Render <CartBridge /> in app/layout.tsx or _app.tsx.
Vue 3 / Nuxt 3
With Pinia:
// composables/useNeuroonCartBridge.ts
import { storeToRefs } from 'pinia';
import { useCartStore } from '~/stores/cart';
export function useNeuroonCartBridge() {
if (process.server) return;
const cart = storeToRefs(useCartStore());
let timer: ReturnType<typeof setTimeout> | null = null;
watch(
() => cart.snapshot.value,
(s) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
window.dispatchEvent(new CustomEvent('neuroon:cart-update', { detail: s }));
}, 200);
},
{ deep: true },
);
}
And from app.vue:
<script setup lang="ts">
useNeuroonCartBridge();
</script>
Validate that the bridge works
// In the browser console
window.addEventListener('neuroon:cart-update', (e) => console.log('cart-update', e.detail));
// Add a product to the cart
Best practices
- Debounce 200 ms. Carts with discounts fire several events in cascade; without debounce the widget gets overloaded.
productIdmust match theexternalIdyou sent to/products/sync. If they differ, the widget cannot deduplicate against the cart.- Do not emit personal data in
detail(email, name, address). The payload should only contain product identifiers and aggregates. - Single source of truth: if you already use the official WordPress plugin, do not add a manual dispatch; the plugin already covers 95 % of cases.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
| The widget ignores the event | Listener registered before the widget loaded | Make sure the widget is loaded (see neuroon:ready event). |
| Duplicate calls | No debounce | Apply setTimeout 200 ms and clearTimeout on new events. |
productId not found | SKU different from the one synced | Reconcile: both endpoints must use the same identifier. |
Next steps
plugins/wordpress/cart-bridge— reference implementation for WooCommerce.plugins/dnn/cart-bridge— Razor pattern.- Widget · Cart integration — what the widget does when it receives the event.