glood-upsell.js) exposes a pub/sub bus and a small imperative API on window.Glood.upsell (PDP surface) and window.Glood.upsell.miniCart (cart-drawer surface). Themes use these to:
- React to selection / quantity / variant changes the customer makes inside the panel.
- Mirror picks into a theme-side totals widget.
- Bundle Glood upsell picks atomically with the theme’s own Add-to-cart POST.
- Drive the panel programmatically (set qty, set variant, commit picks) from custom UI.
PDP surface — window.Glood.upsell.register*
The PDP panel publishes through a single in-memory bus (the embed’s subscribe / publish). For the common selection-change use case, register through the typed helper instead of subscribing to the raw bus.
registerPickCallback(cb)
Fires on every mutation the customer makes inside a pick panel — checkbox toggle, card click, qty stepper +/−, qty input edit, variant picker change — AND on every equivalent call through the programmatic API (selectPickItem / setPickItemQty / setPickItemVariant).
- Commit-to-cart (use the existing
offer-appliedevent instead). - Cart-driven auto-unpicks — the renderer collapses those silently to avoid flooding subscribers on every paint.
console.error, won’t break the chain for other subscribers.
registerFreeGiftCallback(cb)
Fires when a free-gift offer’s qualification state changes — tier crossed, variant changed on a multi-variant tier, gift line added/removed.
registerPickCallback (offerExternalId, offer, triggerVariant, triggerProduct) plus free-gift-specific fields like the current tier id, the qualifying subtotal, and the chosen variant.
Raw pub/sub — Glood.upsell.subscribe(event, fn)
For lower-level events the typed helpers don’t cover, subscribe to the raw bus:
Glood.upsell.publish(event, payload) lets you fire events manually for instrumentation, but does NOT trigger a panel re-render — use Glood.upsell.refresh(...) for that.
Programmatic drivers
Combine these with the register hooks above for fully theme-driven panels:Mini-cart adapter — Glood.upsell.miniCart.register*
The mini-cart adapter is a separate surface — one marker <div> per cart line, hydrated explicitly by the theme. Its callbacks are namespaced under Glood.upsell.miniCart.* so they don’t compete with the PDP panel’s selection state.
All five return an
off() unsubscribe handle and have a matching deregister* for by-reference cleanup, exactly like the PDP helpers.
registerSelectionChangeCallback payload
Same shape as Glood.upsell.registerPickCallback — see above. The change events bus is shared (pick-selection-changed) but auto-scoped to mini-cart anchors so PDP listeners aren’t double-fired.
registerConfirmCallback payload
registerSwapCallback payload
registerSwapFailedCallback payload
registerReselectCallback payload
Hydrate options
Glood.upsell.miniCart.hydrate(opts) accepts:
modules, navigation) are stripped — the prev/next buttons are wired by the storefront and can’t be overridden.
Text slots that live in the merchant dashboard (panelTitle, ctaAddToCart, etc.) are intentionally NOT accepted via hydrate({ text }) — those flow from the offer’s per-locale Display config so the merchant always controls the wording.
Worked example — live PDP total
Combine the trigger variant × qty (from the theme) with the upsell selections total (from the callback):whenReady guard is essential — app-embed.liquid defers loading the upsell asset, so binding before glood:upsell-ready silently no-ops.
Wiring picks into the theme’s Add-to-cart
If your theme owns the Add-to-cart flow (Dawn-style<product-form>), call Glood.upsell.commitPickSelections({ triggerVariantId }) after your /cart/add.js POST resolves:
- Walks
state.selections(the customer’s pending picks from the PDP panel). - Finds the host cart line matching
triggerVariantId(the one your/cart/add.jsjust created/incremented). - Splits the host into
quantity=1cart lines per bundle, each carrying its own__glood_uo_pick_giftsJSON — so the Cart Transform Function emits one bundle per main unit. - Awaits the auto-attach pipeline so the cart is fully split before
refresh()resolves. Your next-line navigation (e.g.,this.cart.renderContents(response)) sees the final cart state.
commitFreeGiftSelections() does the equivalent for any free-gift selections the customer made on multi-variant tiers (where the variant requires explicit picking).
Theme-side cart-drawer refresh on mini-cart actions
The mini-cart confirm/swap fires/cart/change.js + /cart/add.js directly — the cart line count changes but the theme’s drawer markup doesn’t know about it unless you re-render. Wire it up:
cart-drawer.refresh() is theme-specific — the example above is Dawn. For custom drawers, replace with whatever method re-fetches the cart sections and re-paints.
Reference: every register helper
register* returns an off() handle; every deregister* accepts a callback reference. Pick whichever pattern fits your code style — both are first-class.