Skip to main content
The Upsell Offers storefront module (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.
This page covers every register/callback hook on both surfaces, with payload shapes and minimal wiring examples. If you’re integrating for the first time, start with the block placement guide — it covers the auto-listener behaviour every theme inherits for free.

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).
Payload shape:
Does NOT fire on:
  • Commit-to-cart (use the existing offer-applied event instead).
  • Cart-driven auto-unpicks — the renderer collapses those silently to avoid flooding subscribers on every paint.
Callbacks that throw are isolated — logged via 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.
Payload shape mirrors 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:
Useful internal events: 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:
Reserved Swiper keys (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):
The 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:
What this does:
  1. Walks state.selections (the customer’s pending picks from the PDP panel).
  2. Finds the host cart line matching triggerVariantId (the one your /cart/add.js just created/incremented).
  3. Splits the host into quantity=1 cart lines per bundle, each carrying its own __glood_uo_pick_gifts JSON — so the Cart Transform Function emits one bundle per main unit.
  4. 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

Every typed register* returns an off() handle; every deregister* accepts a callback reference. Pick whichever pattern fits your code style — both are first-class.