Skip to content

Cart support guide

Diagnostic and troubleshooting reference for the cart drawer + cart page + checkout bridge — elevated discount UX, three-tier upsell cascade (metafield → API → collection), free-shipping progress bar, inline cart attributes (delivery date, marketing/SMS consent, gift wrapping, free-form cart-attribute blocks), optimistic quantity update with rollback, pre-emptive inventory cap, OOS-during-checkout banner, single-code discount semantics, and sticky mobile checkout footer.

For when something does not work the way it should. The merchant-facing setup guide is at Cart customization guide.

FAQ

Q1: Why isn't my discount applying?

Check the discount code is enabled in Shopify admin → Discounts. The theme calls Shopify's /discount/{code} endpoint; it sets a discount cookie if the code is valid + active. If you see "That code isn't valid", the code itself isn't matching Shopify's eligibility rules (date range, customer eligibility, minimum order, etc.) — verify in Shopify admin.

To diagnose:

  1. Visit /discount/{your_code} directly in the browser. It 302-redirects to the storefront home page by default (append ?redirect=/cart to land on the cart) and sets the discount cookie — the redirect fires whether or not the code is valid, so use step 2 to confirm validity.
  2. Open DevTools → Network → click Apply. Does the request hit /discount/{code}? Does the subsequent /cart.js GET show cart_level_discount_applications populated? If no — discount eligibility issue (Shopify side).
  3. Run the console diagnostic snippet below. Inspect cart.cart_level_discount_applications. If empty after the redirect — Shopify rejected eligibility silently.

Q2: Why is my upsell empty?

The cart upsell uses a 3-tier cascade:

  1. Per-product product.metafields.uisce_cart.cross_sells (list of handles).
  2. Shopify Recommendations API (intent=complementary).
  3. Merchant-set upsell_collection setting on cart section.

If all 3 sources yield nothing, the upsell-grid stays empty. Check:

  • Is the upsell_collection setting populated? (Theme editor → Cart section → Upsell → upsell_collection)
  • Do your products have related/complementary items in Shopify admin → Products → Recommendations?
  • Have you created the uisce_cart.cross_sells metafield definition? (Admin → Settings → Custom data → Products)

You can override the cascade via upsell_source_tier setting (cascade / metafield_only / api_only / collection_only).

Q3: Why is the checkout button disabled?

Two possible gates:

  1. Policy-acknowledgement checkbox is unchecked (existing behavior — must check Terms & Conditions before checkout).
  2. OOS-during-checkout banner is active (an item in cart became unavailable mid-session). Click the OOS banner's "Try again" or remove the offending line.

Use the diagnostic snippet to read OOS banner mounted + OOS banner visible. If both true, the OOS banner is the gate.

Q4: What does "Only N available" mean?

The shopper tried to bump a quantity past inventory. Shopify's inventory tracking has determined N units are in stock. The theme caps the input at N + shows the inline message.

If you see this for an item that should have more inventory:

  • Double-check inventory_tracked + inventory_quantity on the variant in Shopify admin.
  • Confirm inventory_policy = deny (otherwise Shopify allows backorder; the cap doesn't apply).
  • Run the diagnostic snippet → inspect inventoryAttrs to see what data-inventory-quantity + data-inventory-policy are rendered on each line.

Q5: Why does the optimistic quantity update sometimes "flash back"?

Two cases:

  1. The mutation failed on the server side (rare; usually inventory drifted between cart-render and click — see Q4).
  2. The latest-wins guard rejected the response because a newer mutation superseded it (rapid +/+/+/+ clicks coalesce to a single mutation; intermediate UI states are discarded).

Both are expected behavior. To check whether a mutation is still in flight, read the diagnostic snippet's in-flight lines count (lines mid-mutation carry the cart__line-item--in-flight class) — it returns to 0 once the network settles. The mutationTokens count / debounceTimers count rows only show which lines have mutated this session; entries persist after settling, so non-zero there does not mean in-flight.

Q6: How do I customize the gift-wrap label?

The simple enable_gift_wrapping toggle uses a fixed label from your locale file ({{ 'cart.gift_wrapping' | t }} — "Add gift wrapping" in English). To customize:

  1. Disable enable_gift_wrapping on the cart section.
  2. Add a cart-attribute block.
  3. Set key=gift_wrapping, input_type=checkbox, and your custom label.

Multiple cart-attribute blocks can be stacked for richer gift-wrap variants (e.g., gift_wrap_message + gift_recipient_name).

Q7: How do I add a per-line attribute (vs cart-level)?

  • Cart-level attributes post as attributes[*] (order-level — visible on order confirmation under "Notes").
  • Per-line attributes post as properties[*] and bind to a specific line item (e.g., per-recipient name on a multi-gift order).

The free-form cart-attribute block ships a Scope select: Cart (default) posts attributes[*]; Line item posts properties[*]. Set the block's Scope to "Line item" and place it in the per-line dispatch slot so the input binds to that line item.

Setting enable_sms_consent=true renders a checkbox that posts as attributes[sms_consent]=true if the shopper checks it. You connect this to your SMS marketing app (e.g., Klaviyo, Postscript) via the Shopify admin app integration — the theme just captures consent.

Same applies to enable_marketing_consent for email — posts as attributes[email_consent].

Q9: Why is delivery date required on Uisce only?

Uisce ships the Services niche; merchants on Uisce sell appointments + bookings, where a delivery date IS the service date. So listings/uisce/templates/cart.json ships with delivery_date_required=true by default. Other presets default to OFF (you can toggle ON if your products require a delivery date — e.g., live-plant Talamh shops or perishable Neart products).

Q10: Why is the cart-drawer slide direction wrong in Arabic?

Verify the storefront has Arabic published (Shopify admin → Settings → Languages → Add language → Arabic). The theme uses inset-inline-end + a [dir='rtl'] keyframe override so the drawer enters from the inline-end edge in both directions — in Arabic that is the physical left edge, which is the intended mirrored behavior, not a bug. If the slide is still wrong:

  1. Check <html lang> and <html dir> are correctly set on the page (use the diagnostic snippet's cart.token line — it confirms window.CartAPI is reachable; for dir, inspect document.documentElement.dir).
  2. Confirm the storefront URL has the locale path /ar/cart (or your store's RTL locale path) — the locale routing must be active.
  3. Report the dev-store URL + Shopify-admin language settings + locale-routing path to support.

Decision tree: "Why is X not working?"

Discount-code not applying

  1. Visit /discount/{your_code} directly in the browser. It 302-redirects to the storefront home page by default (append ?redirect=/cart to land on the cart) and sets the discount cookie — the redirect fires whether or not the code is valid, so use step 2 to confirm validity.
  2. Open DevTools → Network → click Apply. Does the request hit /discount/{code}? Does the subsequent /cart.js GET show cart_level_discount_applications populated? If no — discount eligibility issue (Shopify side).
  3. Run console snippet (below). Inspect cart.cart_level_discount_applications.size. If 0 after the redirect — Shopify rejected eligibility silently.

Upsell cards not showing

  1. Open /recommendations/products.json?product_id={any_product_id}&intent=complementary&limit=4 directly. Does it return products? If no — Shopify hasn't computed complementary recommendations yet (requires sales history).
  2. Run console snippet. Inspect upsell tier1 handles (per-product metafield) + upsell tier3 handle (fallback collection). Tier 1 yielded? Tier 3 set?
  3. Verify upsell_collection setting is populated on the cart section.
  4. If upsell_source_tier=metafield_only is set and no products have uisce_cart.cross_sells populated, the JS cascade returns nothing and the server-rendered collection cards stay visible (the server renders upsell_collection cards whenever that setting is populated). The grid is empty only when upsell_collection is also unset — populate the metafield or switch back to cascade.

OOS-during-checkout banner not appearing

  1. Verify a product variant has inventory_quantity=0 + inventory_management=shopify + inventory_policy=deny. Without policy=deny, Shopify allows backorder.
  2. Run console snippet. After clicking Check out, inspect OOS banner mounted and OOS banner visible. If mounted=false, the banner element didn't render — confirm the cart-page has [data-checkout-bridge-banner] in the DOM.
  3. Inspect checkout-retry-counter in sessionStorage. null is the normal state — the counter is written only when the bridge's availability-validation fetch fails, and a successful validation removes it. A numeric value means recent validation-fetch failures; at 3 the bridge clears the counter and lets checkout proceed (fails open).

Optimistic UI rolls back unexpectedly

  1. Run the console snippet and read in-flight lines — non-zero means a mutation is still mid-cycle. (mutationTokens count / debounceTimers count persist after settling and do not indicate in-flight state.)
  2. Check Network tab for failed /cart/change.js POST. Inspect the 422 description — the theme matches it against three unanchored patterns: /Only (\d+) available/i, /You can only add (\d+) of this item/i, /All (\d+) of .+ are in your cart/i.
  3. If the rollback only happens on rapid +/+/+/+ clicks, this is the latest-wins guard working as designed (intermediate UI states are discarded).

Cart-attribute not posting to order

  1. Confirm the cart-attribute block is added to the cart section (theme editor → Cart → Add block) — it renders as a direct block of the cart section, inside the cart form.
  2. Inspect the rendered <input name="attributes[your_key]"> — confirm the name attribute is attributes[*] (cart-level) or properties[*] (per-line).
  3. Run the diagnostic snippet → inspect cart.attributes. Should reflect the field's value after a /cart/update.js POST.
  4. If cart.attributes is {}, the field's value didn't post — confirm the input sits inside the cart form and its name is exactly attributes[your_key]; the cart form posts attributes by input name alone.
  1. Confirm viewport is < 1024 px (DevTools device emulation) — the element always renders, but CSS shows it only below 1024 px (display: none on desktop, where the sticky sidebar handles the checkout CTA).
  2. Run diagnostic snippet → inspect sticky-footer present. It should be true at any viewport; if false, the [data-sticky-mobile-checkout] element isn't in the DOM — confirm cart.liquid renders the sticky-footer markup.
  3. iOS Safari sometimes mis-computes env(safe-area-inset-bottom) — try toggling between portrait + landscape orientation.

Shopify checkout rejection mappings

When Shopify checkout itself rejects a cart at /checkout commit time (after the theme's cart layer), Shopify shows error messages on the checkout page. Common rejections + their cart-side root causes:

Shopify messageLikely causeFix
"Some items are no longer available"OOS variant in cart at /checkout commitTheme banner should have caught this PRE-redirect; if it didn't, check the OOS pre-validate flow — /cart.js does not expose per-item availability, so the bridge probes GET /products/{handle}.js and reads variant.available. Run the console snippet to inspect inventory state.
"Discount code [X] is no longer valid"Discount expired / max-uses-reached / customer-ineligibleVerify in Shopify admin → Discounts. Check date range, customer eligibility, minimum order requirements.
"Cart is empty" on /checkoutCart cleared post-add (race)Network race during ATC; rare; refresh page. Check the diagnostic snippet's cart.item_count — should match what the shopper sees.
"Inventory is no longer sufficient"Inventory drifted after cart-render but before checkoutOptimistic UI should rollback to cap; if checkout still rejects, the cap drifted further. Refresh cart and retry.
"Required attribute X is missing"cart-attribute required=true field not filledFind the missing field on cart page; fill before checkout. The theme blocks checkout client-side via required validator on the input.

Console diagnostic snippet

Paste this into your browser DevTools Console on /cart or any page with the cart-drawer mounted. Produces a single console.table of cart state — safe, read-only, and adds zero theme code.

javascript
(async function () {
  const cart = await window.CartAPI.getCart();
  const drawer = document.querySelector('cart-drawer');
  const page = document.querySelector('cart-page');
  const upsellSlot = document.querySelector('[data-cart-upsell-slot]');
  const oosBanner = document.querySelector('[data-checkout-bridge-banner]');
  const stickyFooter = document.querySelector('[data-sticky-mobile-checkout]');
  const cartHost = page || drawer;
  const mutationTokens = cartHost && cartHost._mutationTokens ? Array.from(cartHost._mutationTokens.entries()) : [];
  const debounceTimers = cartHost && cartHost._debounceTimers ? Array.from(cartHost._debounceTimers.entries()) : [];
  const inFlightLines = document.querySelectorAll('.cart__line-item--in-flight').length;
  const inventoryAttrs = Array.from(document.querySelectorAll('[data-inventory-quantity]')).map((el) => ({
    key: el.dataset.lineKey,
    inventoryQuantity: el.dataset.inventoryQuantity,
    inventoryPolicy: el.dataset.inventoryPolicy,
  }));
  const upsellCards = document.querySelector('[data-cart-upsell-cards]');
  const upsellTier1 = upsellCards ? upsellCards.dataset.tier1Handles : null;
  const upsellTier3 = upsellCards ? upsellCards.dataset.upsellTier3 : null;
  const retryCounter = sessionStorage.getItem('uisce.state.v1.checkout-bridge-retry-counter');
  const freeShippingDebounce = sessionStorage.getItem('uisce.state.v1.free-shipping-announce');
  console.table({
    'cart.token': cart.token,
    'cart.item_count': cart.item_count,
    'cart.total_price (cents)': cart.total_price,
    'cart.note (length)': (cart.note || '').length,
    'cart.attributes': JSON.stringify(cart.attributes || {}),
    'cart.cart_level_discount_applications.size': (cart.cart_level_discount_applications || []).length,
    'drawer mounted': drawer !== null,
    'page mounted': page !== null,
    'upsell-slot present': upsellSlot !== null,
    'upsell tier1 handles': upsellTier1,
    'upsell tier3 handle': upsellTier3,
    'OOS banner mounted': oosBanner !== null,
    'OOS banner visible': oosBanner && !oosBanner.hidden,
    'sticky-footer present': stickyFooter !== null,
    'inventoryAttrs count': inventoryAttrs.length,
    'mutationTokens count': mutationTokens.length,
    'debounceTimers count': debounceTimers.length,
    'in-flight lines': inFlightLines,
    'checkout-retry-counter': retryCounter,
    'free-shipping-debounce': freeShippingDebounce,
    'window.uisceStrings size': Object.keys(window.uisceStrings || {}).length,
  });
  console.log('Full cart:', cart);
  console.log('Inventory attrs:', inventoryAttrs);
  console.log('Mutation tokens:', mutationTokens);
})();

This dumps cart state + drawer/page mount status + upsell tier resolution + OOS banner state + inventory cap state + attribute state + sessionStorage retry counters + uisceStrings registry size. Send the table to support.

What to look for in the output:

  • drawer mounted === true AND page mounted === true (on /cart) — both Custom Elements connected.
  • cart.item_count > 0 — cart has line items (otherwise upsell cascades to fallback collection, OOS banner is moot).
  • upsell-slot present === true if enable_upsell=true; if false, upsell setting is OFF.
  • upsell tier1 handles populated when uisce_cart.cross_sells metafield is set on a line-item product.
  • upsell tier3 handle set when upsell_collection setting is populated.
  • OOS banner mounted === true always (the element exists hidden); visible flips to true when an item is no longer available.
  • sticky-footer present === true at any viewport — the element always renders; CSS hides it at >= 1024 px (desktop uses the sticky sidebar instead).
  • inventoryAttrs count > 0 for any line-item with inventory_tracked + policy=deny.
  • mutationTokens count + debounceTimers count — which lines have mutated this session; entries persist after the network settles (cleared only when the element disconnects), so non-zero does NOT mean in-flight. Use in-flight lines for that — it drops to 0 once the network settles.
  • checkout-retry-counter — written only when the checkout-bridge's availability-validation fetch fails; a successful validation removes it, so null is the normal state. At 3 consecutive failures the bridge clears the counter and lets checkout proceed (fails open).
  • cart.cart_level_discount_applications.size > 0 when a discount is applied.

Embedded diagnostic tool: A full embedded diagnostic UI is planned for a future update. The console snippet above provides the core diagnostic value.

Curl cheat-sheet

Use these one-liners to verify your live store's cart flow. Replace {store} with your storefront domain (e.g., uisce.sionnach.solutions) and capture the storefront-password cookie first if your store is a dev store.

For dev stores, capture the auth cookie via:

bash
curl -s -o /dev/null -D - "https://{store}/password" \
  -d "form_type=storefront_password&password={password}" \
  -L -c cookies.txt | head -5
# Then add `-b cookies.txt` to all subsequent curl commands below

For live production stores (no password protection), no cookie is needed.

Get cart state

bash
curl -s 'https://{store}/cart.js' | jq .

Expected: JSON with token, note, attributes, total_price, item_count, items[], cart_level_discount_applications[].

Bundled section refresh

The theme refreshes sections two ways: the optimistic flow bundles a sections key into the mutation POST body, and the discount flow GETs the page URL via the Section Rendering API.

bash
# Sections bundled into a mutation (optimistic-UI flow)
curl -s -X POST 'https://{store}/cart/change.js' \
  -H 'Content-Type: application/json' \
  -d '{"line": 1, "quantity": 1, "sections": "cart-drawer"}' | jq '.sections | keys'

# Section Rendering API GET on the page URL (discount-refresh flow)
curl -s 'https://{store}/cart?sections=cart,cart-drawer' | jq 'keys'

Expected: the POST response carries a sections object with rendered HTML for the requested sections; the GET returns JSON keyed by section name.

bash
curl -sIw '%{http_code} %{redirect_url}\n' -c cookies.txt 'https://{store}/discount/WELCOME20'

Expected: HTTP 302 with a Location redirect to the storefront root (/) by default — append ?redirect=/cart to land on the cart instead. The redirect fires for unknown codes too, so it does not prove validity; verify with the next probe.

Change item quantity

bash
curl -s -X POST 'https://{store}/cart/change.js' \
  -H 'Content-Type: application/json' \
  -d '{"line": 1, "quantity": 3}' | jq .

Expected: JSON with the updated cart state. If line 1 has inventory < 3, the response will include description with the inventory-cap message.

Update cart attributes + note

bash
curl -s -X POST 'https://{store}/cart/update.js' \
  -H 'Content-Type: application/json' \
  -d '{"note": "Please ring doorbell", "attributes": {"delivery_date": "2026-05-01", "gift_wrapping": "true"}}' | jq .

Expected: JSON with note + attributes reflecting the posted values.

Verify the applied discount (the theme's own Apply check)

bash
curl -s -b cookies.txt 'https://{store}/cart.js' | jq '.cart_level_discount_applications'

Expected: a populated array when the code applied to the current cart; [] after the redirect means Shopify rejected eligibility (the theme surfaces "That code isn't valid." the same way).

Remove the currently-applied discount

bash
curl -s -X POST 'https://{store}/cart/update.js' \
  -H 'Content-Type: application/json' \
  -d '{"discount_code": ""}' | jq .

Expected: JSON with cart_level_discount_applications[] empty.

Get product recommendations (tier 2 of cascade)

bash
curl -s 'https://{store}/recommendations/products.json?product_id={id}&intent=complementary&limit=4' | jq .

Expected: JSON with products[] array of recommended products. Empty when Shopify has not yet computed complementary recommendations (requires sales history).

Verify metafield definition exists (Admin GraphQL)

Metafield definitions are exposed via the Admin GraphQL API (there is no REST metafield_definitions.json resource). You can also check Shopify admin → Settings → Custom data → Products.

bash
curl -s -X POST 'https://{store-myshopify-domain}/admin/api/2024-10/graphql.json' \
  -H "X-Shopify-Access-Token: $UISCE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"query": "{ metafieldDefinitions(first: 20, ownerType: PRODUCT, namespace: \"uisce_cart\") { nodes { namespace key name type { name } } } }"}' | jq .

Expected: nodes[] containing the uisce_cart.cross_sells definition (and any other uisce_cart.* definitions you have created).

Empirical availability check

bash
curl -s 'https://{store}/products/{handle}.js' | jq '.variants[] | {id, sku, available}'

Expected: per-variant available boolean — the same GET /products/{handle}.js probe the checkout bridge uses. Public product JSON does not expose inventory_quantity / inventory_policy; read exact quantities and policy in Shopify admin (or via the Admin API), and cross-check the cap values the theme renders via the snippet's inventoryAttrs.

Verify cart attributes post correctly (round-trip)

bash
# Set
curl -s -X POST 'https://{store}/cart/update.js' \
  -H 'Content-Type: application/json' \
  -d '{"attributes": {"test_key": "test_value"}}' | jq '.attributes'

# Read back
curl -s 'https://{store}/cart.js' | jq '.attributes'

Expected: both responses show {"test_key": "test_value"}.

Need more help?

  • For deep diagnostic beyond what the console snippet captures, a full embedded diagnostic UI is planned for a future update.
  • File a GitHub issue with the console-snippet output + browser/OS + preset.
  • See the cart customization guide for configuration walkthrough.

Built for the Shopify Theme Store.