Skip to main content
The Glood Upsell for PDP and Glood Upsell for Cart theme app blocks ship with a small JS surface designed for easy integration into any Shopify theme — from Dawn out-of-the-box to deeply-customised builds. This guide covers:
  • Block placement requirements
  • Quick-start integration (zero-code for most themes)
  • Auto-listener behaviour
  • The window.Glood.upsell API — refresh(), setQty(), setVariant(), setContext(), setMode(), getPendingItems(), commitSelections(), subscribe(), publish(), getState()
  • Auto vs deeply-integrated mode (when to call the explicit setters)
  • Linked vs Standalone modes (atomic Add-to-cart bundling)
  • Wiring custom variant pickers, quantity inputs, cart-drawer refresh paths
  • Verifying the integration end-to-end + an error reference

Quick start

Most themes need zero code. Drag the block into a section, save the theme, you’re done.
  1. In the Shopify admin → Online Store → Themes → Customize, open a product template.
  2. In the section that should host the upsell (typically Product information below the price/description), click Add block → pick Glood Upsell for PDP.
  3. Repeat in the Cart drawer and the /cart page sections, adding Glood Upsell for Cart.
  4. Save the theme.
The blocks self-load their JS/CSS, auto-listen to standard [name="id"] / [name="quantity"] change events, and refresh on Rk:Added_To_Cart + Shopify-native cart:updated. Open the PDP, add the trigger product → the panel re-renders with applicable offers. You only need theme code if the theme’s PDP uses a custom variant/qty picker that doesn’t dispatch standard form change events, OR you want the block linked to the theme’s native Add-to-cart button (single atomic add for host + upsells). Both paths are below.

Block placement

Both blocks are dragged into theme sections via Shopify’s theme editor. They depend on the section accepting theme app blocks — most modern themes do this via a { "type": "@app" } entry in the section’s schema blocks array. Dawn, Sense, Studio, and other Online Store 2.0 themes work out of the box. If your theme’s section doesn’t accept app blocks, the Glood entry won’t appear in the Add block picker. Edit the section’s {% schema %} and add { "type": "@app" } to its blocks array.

Auto-listener behaviour

On boot, glood-upsell.js subscribes to a curated set of theme-agnostic events. No theme integration is required for any of these: Each call to refresh() re-evaluates which offers apply, recomputes the qty-aware slot count, and re-renders all mounted Glood Upsell blocks on the page.

The window.Glood.upsell API

Auto mode vs deeply-integrated mode

The block runs in one of two modes per state field (qty, variantId): In deeply-integrated mode the block trusts the theme’s number and computes:
So if the theme says setQty(5) and the cart already has 3 pick lines for the offer, the panel will show 2 remaining slots.

Explicit setters

All three explicit setters flag the value as theme-owned for the rest of the session (or until resetSource() / page navigation).

When to use which mode

  • Use auto mode when you have a standard [name="quantity"] input and [name="id"] variant selector. The block picks them up for free, and cart-side qty changes (drawer steppers, line removals) automatically grow / shrink the slot count.
  • Use deeply-integrated mode when:
    • Your custom variant/qty picker doesn’t emit standard DOM events.
    • You want the block to treat your number as authoritative even when the cart has a different qty (e.g. a “configure 5, add all at once” flow).
    • You’re embedding the block inside a custom configurator that owns its own state machine.
If you mix the two — e.g. call setVariant() once and then let the auto-listener handle [name="quantity"] — only the field you called setVariant() on becomes explicit; the qty stays in auto mode.

Published events

Linked vs Standalone modes

The PDP block ships with two integration modes — pick the one that matches your theme.

Choosing the mode

Per-block setting (theme editor — preferred). Drag the Glood Upsell for PDP block into a product section, open its settings panel, and pick Integration mode → Linked to the product’s Add to cart. Save. The block sets data-glood-upsell-mode="linked" on the mount element and the JS picks it up on init. JS override (runtime). Themes that need to flip dynamically:

Linked mode — the atomic-bundle pattern (preferred)

Bundle the upsell items into the SAME /cart/add.js POST that adds the host product. Single network round-trip; no flicker between the host landing and the upsell items landing. Minimal recipe — vanilla form intercept (works on Dawn / Sense / most OS 2.0 themes):
Notes:
  • getPendingItems() returns { id, quantity, properties: { __glood_uo_* } } per picked pool slot, clamped to the offer’s remaining slot budget. Empty array when nothing’s picked.
  • The fallback form.submit() on error ensures the customer can still complete the purchase even if Glood’s add fails — never block the host product from landing.
  • If your theme uses a custom AJAX add-to-cart (e.g. Dawn’s product-form.js’s onSubmitHandler), intercept at that layer instead of the form’s submit event. The body of the recipe is identical.

Linked mode — the two-trip fallback

If the theme’s Add-to-cart handler doesn’t expose a pre-POST hook, call commitSelections() AFTER the theme’s own ATC completes:
commitSelections() fires its own /cart/add.js for the pending picks, clears state.selections, refetches the cart, fires offer-applied. Cost: the customer sees the host land first, then the upsell items land a moment later (two network trips).

Standalone mode — no theme integration needed

Just drop the block in. The block reads the cart, shows applicable offers when the trigger product is present, and uses its own Add-to-cart button to commit. The host PDP form is left alone. Empty-state copy: when the cart has zero of the offer’s trigger product, the panel collapses to a single “Add this product to your cart to unlock the upsell.” prompt instead of showing 0 selectable slots.

When to use which mode

  • Standalone when the upsell is a reactive “after-purchase add-on” experience — the customer must first add the host product to the cart, and the upsell section lights up afterwards. Works with literally any theme.
  • Linked when the upsell is part of a bundle-style purchase — the customer configures host + accessories in one go and clicks one Add-to-cart button. Requires either picking the mode from the theme editor OR a small JS hook in the theme’s ATC handler.

Wiring native variant selectors

The block auto-listens to [name="id"] changes on any product form, which covers ~95% of themes. For custom variant pickers that don’t update a hidden name="id" input:
For themes that integrate with the Glood Bundles widget, the existing glood-ai:on_bundle_variant_change bus is already auto-handled.

Variant-aware pool items

If a pool item’s reward product has multiple variants, the block lazy-loads the variant list from Shopify’s public /products/{handle}.js endpoint when the customer picks that card. The customer can then pick a different variant from the dropdown that appears under the product name. This works automatically when the canonical payload carries the product’s handle (admin captures it via the App Bridge resource picker). Single-variant products show no dropdown.

Wiring native quantity inputs

The block auto-listens to [name="quantity"] changes anywhere on the page. The PDP block’s slot meter is qty-aware: slotsPerMainQty × qty, capped at maxSlots. For custom quantity pickers (e.g. a data-action="increase" button on a custom +/ widget), either dispatch a synthetic change event on the native input or call the API directly:
Both paths trigger the same internal state update. The slot meter and Add-to-cart button reflect the new qty within one paint.

Reacting to cart updates

The block has three complementary cart-refresh paths:
  1. Auto — Shopify-native cart:updated / cart:refresh events.
  2. Existing Glood busRk:Added_To_Cart (dispatched by other Glood widgets).
  3. Manualwindow.Glood.upsell.refresh({ refetchCart: true }).
If your theme’s cart drawer uses the Section Render API to update its own markup, call refresh({ refetchCart: true }) after the section swap so the Glood block re-renders against the new cart state:
The block doesn’t store cart-line markup itself — re-rendering after a Section Render is cheap.

Custom add-to-cart flows

If your theme intercepts /cart/add.js POSTs (e.g. for analytics or to inject extra properties), the Glood Upsell add-to-cart uses the same endpoint with these properties:
These properties drive the Discount Function’s eligibility check at checkout. Don’t strip them in your theme middleware. After a successful add, the block dispatches Rk:Added_To_Cart and refreshes its own cart state. Listen on Rk:Added_To_Cart from your theme to chain additional behaviour.

Custom CSS

Each block has a Custom CSS textarea in its theme-editor settings — content is scoped to that block instance. For repo-wide overrides, target the block’s BEM-scoped classes from your theme’s stylesheet:

Verifying the integration

After placing the block and (optionally) wiring any theme hooks, paste these into the browser console on the PDP to validate end-to-end:
Health checks:

Errors and what they mean

Every error surfaces as a pill on the gift card (free-gift) or an inline banner under the pick panel. The exact text → cause → fix mapping:

Stuck Adding…

If the pill stays on Adding… indefinitely, check the Network tab for POST /cart/add.js:
  • No request fired → the storefront short-circuited before POST. Usually market.variantId is null/NaN. The error pill should already say Reward variant missing; if it doesn’t, hard-refresh the page (Cmd+Shift+R) — you may be on a stale extension JS bundle.
  • Request returned 5xx → Shopify-side hiccup; the block retries on the next cart event automatically.
  • Request returned 4xx but pill still says “Adding…” → bug; please send the request body + response to the Glood support channel.

Debugging

glood-upsell.js logs every render with the full payload + state, so production debugging is one console line away:
Check payload.pickOffers and payload.freeGiftOffers to confirm the block has data; state.cart to confirm the cart snapshot is current. If you see [Glood.upsell] mount has no payload, the inline <script type="application/json" data-glood-upsell-payload> was missing — usually means the Liquid block failed to render (check for theme-level JSON syntax errors in browser source view).