Appearance
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:
- Visit
/discount/{your_code}directly in the browser. It 302-redirects to the storefront home page by default (append?redirect=/cartto 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. - Open DevTools → Network → click Apply. Does the request hit
/discount/{code}? Does the subsequent/cart.jsGET showcart_level_discount_applicationspopulated? If no — discount eligibility issue (Shopify side). - 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:
- Per-product
product.metafields.uisce_cart.cross_sells(list of handles). - Shopify Recommendations API (
intent=complementary). - Merchant-set
upsell_collectionsetting on cart section.
If all 3 sources yield nothing, the upsell-grid stays empty. Check:
- Is the
upsell_collectionsetting 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_sellsmetafield 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:
- Policy-acknowledgement checkbox is unchecked (existing behavior — must check Terms & Conditions before checkout).
- 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_quantityon the variant in Shopify admin. - Confirm
inventory_policy = deny(otherwise Shopify allows backorder; the cap doesn't apply). - Run the diagnostic snippet → inspect
inventoryAttrsto see whatdata-inventory-quantity+data-inventory-policyare rendered on each line.
Q5: Why does the optimistic quantity update sometimes "flash back"?
Two cases:
- The mutation failed on the server side (rare; usually inventory drifted between cart-render and click — see Q4).
- 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:
- Disable
enable_gift_wrappingon the cart section. - Add a
cart-attributeblock. - Set
key=gift_wrapping,input_type=checkbox, and your customlabel.
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.
Q8: How does SMS consent work?
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:
- Check
<html lang>and<html dir>are correctly set on the page (use the diagnostic snippet'scart.tokenline — it confirmswindow.CartAPIis reachable; fordir, inspectdocument.documentElement.dir). - Confirm the storefront URL has the locale path
/ar/cart(or your store's RTL locale path) — the locale routing must be active. - 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
- Visit
/discount/{your_code}directly in the browser. It 302-redirects to the storefront home page by default (append?redirect=/cartto 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. - Open DevTools → Network → click Apply. Does the request hit
/discount/{code}? Does the subsequent/cart.jsGET showcart_level_discount_applicationspopulated? If no — discount eligibility issue (Shopify side). - Run console snippet (below). Inspect
cart.cart_level_discount_applications.size. If 0 after the redirect — Shopify rejected eligibility silently.
Upsell cards not showing
- Open
/recommendations/products.json?product_id={any_product_id}&intent=complementary&limit=4directly. Does it return products? If no — Shopify hasn't computed complementary recommendations yet (requires sales history). - Run console snippet. Inspect
upsell tier1 handles(per-product metafield) +upsell tier3 handle(fallback collection). Tier 1 yielded? Tier 3 set? - Verify
upsell_collectionsetting is populated on the cart section. - If
upsell_source_tier=metafield_onlyis set and no products haveuisce_cart.cross_sellspopulated, the JS cascade returns nothing and the server-rendered collection cards stay visible (the server rendersupsell_collectioncards whenever that setting is populated). The grid is empty only whenupsell_collectionis also unset — populate the metafield or switch back tocascade.
OOS-during-checkout banner not appearing
- Verify a product variant has
inventory_quantity=0 + inventory_management=shopify + inventory_policy=deny. Withoutpolicy=deny, Shopify allows backorder. - Run console snippet. After clicking Check out, inspect
OOS banner mountedandOOS banner visible. Ifmounted=false, the banner element didn't render — confirm the cart-page has[data-checkout-bridge-banner]in the DOM. - Inspect
checkout-retry-counterin sessionStorage.nullis 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
- Run the console snippet and read
in-flight lines— non-zero means a mutation is still mid-cycle. (mutationTokens count/debounceTimers countpersist after settling and do not indicate in-flight state.) - Check Network tab for failed
/cart/change.jsPOST. Inspect the 422description— 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. - 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
- Confirm the
cart-attributeblock 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. - Inspect the rendered
<input name="attributes[your_key]">— confirm thenameattribute isattributes[*](cart-level) orproperties[*](per-line). - Run the diagnostic snippet → inspect
cart.attributes. Should reflect the field's value after a/cart/update.jsPOST. - If
cart.attributesis{}, the field's value didn't post — confirm the input sits inside the cart form and itsnameis exactlyattributes[your_key]; the cart form posts attributes by input name alone.
Sticky mobile checkout footer not appearing
- Confirm viewport is
< 1024 px(DevTools device emulation) — the element always renders, but CSS shows it only below 1024 px (display: noneon desktop, where the sticky sidebar handles the checkout CTA). - Run diagnostic snippet → inspect
sticky-footer present. It should betrueat any viewport; iffalse, the[data-sticky-mobile-checkout]element isn't in the DOM — confirm cart.liquid renders the sticky-footer markup. - 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 message | Likely cause | Fix |
|---|---|---|
| "Some items are no longer available" | OOS variant in cart at /checkout commit | Theme 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-ineligible | Verify in Shopify admin → Discounts. Check date range, customer eligibility, minimum order requirements. |
| "Cart is empty" on /checkout | Cart 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 checkout | Optimistic 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 filled | Find 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 === trueANDpage 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 === trueifenable_upsell=true; iffalse, upsell setting is OFF.upsell tier1 handlespopulated whenuisce_cart.cross_sellsmetafield is set on a line-item product.upsell tier3 handleset whenupsell_collectionsetting is populated.OOS banner mounted === truealways (the element exists hidden);visibleflips totruewhen an item is no longer available.sticky-footer present === trueat any viewport — the element always renders; CSS hides it at>= 1024 px(desktop uses the sticky sidebar instead).inventoryAttrs count > 0for any line-item withinventory_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. Usein-flight linesfor 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, sonullis the normal state. At 3 consecutive failures the bridge clears the counter and lets checkout proceed (fails open).cart.cart_level_discount_applications.size > 0when 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 belowFor 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.
Apply discount via direct redirect (sets the discount cookie)
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.
Related docs
- Cart customization guide — settings catalog + per-preset block composition + metafield walkthrough
- PLP support — Phase 06.5 collection page support
- PDP support — Phase 06.4 product page support
- Header support — Phase 06.3 header support
- Translations (50 locales)
- Support form — submit a ticket if the above does not resolve your issue