The Upsell Offers storefront module (Documentation Index
Fetch the complete documentation index at: https://docs.glood.ai/llms.txt
Use this file to discover all available pages before exploring further.
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:
| Event | Payload | Fires when |
|---|---|---|
variant-changed | { variantId, productId, source } | The driving variant changed (form [name="id"] change, programmatic setVariant, or scoped variant change). |
qty-changed | { qty, source } | Main-product qty changed (form [name="quantity"], programmatic setQty). |
cart-updated | { cart } | Internal cart state refreshed (NOT the same as Shopify’s DOM cart:updated event — this is the storefront module’s own publish after refresh({ refetchCart: true })). |
pick-selection-changed | same as registerPickCallback | Underlying event the typed register helper subscribes to. Use the helper unless you need raw access. |
offer-applied | { offerExternalId, source } | Picks committed to cart via commitPickSelections. |
free-gift-added | { offerExternalId, tierId, variantId } | Auto-add or manual-add of a free-gift line succeeded. |
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.
| Callback | Fires when |
|---|---|
registerSelectionChangeCallback(cb) | Every pick/unpick/qty stepper/variant edit inside any mini-cart carousel. Use this for live totals. |
registerConfirmCallback(cb) | Confirm clicked on a TRIGGER-line carousel (first-add path) AND commit succeeded. |
registerSwapCallback(cb) | Confirm clicked in swap-mode (Reselect → pick replacement) AND the atomic swap committed. |
registerSwapFailedCallback(cb) | Swap commit failed. Theme owns recovery UI (e.g., toast). |
registerReselectCallback(cb) | Customer clicked Reselect on a gift line. Fires BEFORE any cart mutation. |
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.