🚧
Work in progress
We're still writing this guide — details may change as it's finalized.
Reference Web Shopify WordPress WooCommerce

Cart Sync

How to feed your store's cart to the drop-in so benefits and points are calculated against the real basket — without infinite loops.


Cart Sync

Everything the drop-in calculates — which benefits apply, what discount to show, how many points the order would earn — runs against the cart you give it. If the cart in the drop-in doesn't match the cart in your store, the customer sees a benefit they can't redeem (or worse, redeems a benefit your checkout doesn't honor).

This guide explains the shape SimplyClub expects, when to push updates, and how to avoid the most common pitfall: an infinite cart-update loop.

How it works

  1. On page load, push the current cart into the drop-in once.
  2. Every time your storefront mutates the cart (add, remove, change quantity, apply a coupon, etc.), push the updated cart again.
  3. When the user picks a benefit in the drop-in, persist the selection back onto your cart so the discount carries through to checkout. See API Reference → On.benefitSelectionsChanged.
import { dropInLoader } from "https://dropins.simplyclub.co.il/drop-ins/drop-in-loader/index.mjs";

const instance = await dropInLoader.setConfig({ poiId: "YOUR_POI_ID" });

// 1. Initial sync
await instance.commands.setCart(await readMyCart());

// 2. Re-sync on every cart change
onMyCartChange(async () => {
  await instance.commands.setCart(await readMyCart());
});

Cart shape

setCart takes one argument: the cart object. Pass null to clear.

type Cart = {
  token: string;            // any stable identifier — your cart hash, session id, etc.
  items: CartItem[];
  discount_codes?: string[]; // coupons the customer has already applied
};

Minimal item shape

type CartItem = {
  id: string | number;       // your platform's line-item or product id
  quantity: number;
  price: number;             // unit price in MAJOR currency units (e.g. 49.90)
  final_price: number;       // price after line discounts (also major units)
  title?: string;
  sku?: string;
};

That's all you need to make benefits calculate correctly. The richer the item, the more SimplyClub can do — see the full shape below.

Full item shape

Everything optional unless marked. Anything missing is treated as "not provided".

Field Type What it's for
id (required) string | number Line-item id — uniquely identifies this row in the cart.
quantity (required) number How many of this item.
price (required) number Unit price, major currency units.
final_price (required) number Per-unit price after any line discounts.
title string Display name shown in the drop-in.
name string Same idea — set both to the product name.
description string Short description.
sku string Product SKU — used by benefit rules that target specific SKUs.
vendor string Brand / vendor — used by rules that target specific vendors.
image string Image URL shown in the drop-in.
url string Product page URL.
variant_id string | number Variant identifier, if your platform has variants.
variant_title string Variant display name (e.g. "Large / Red").
variant_inventory_quantity number Available stock — used by rules that consider stock.
compare_at_price number Original price before sale (major units). Lets the drop-in show "save X" labels.
discounts { title: string; amount: number }[] Per-line discounts already applied. Amounts in major units.
total_discount number Sum of discounts[].amount. Major units.
line_price number Total for this line before discounts. Major units.
original_line_price number Same as line_price for most platforms.
discounted_price number Per-unit price after line discounts (same as final_price for simple cases).
final_line_price number Line total after discounts.
extended_attributes object Optional extras — { tags: string[], collections: string[], metafields: any[] }. Targeted benefits can match on these.

⚠️ Price units

This is the most common mistake. SimplyClub expects prices in major currency units49.90, not 4990.

  • Shopify's /cart.js returns prices in minor units (cents/agorot). The SimplyClub Theme App Block divides them by 100 before calling setCart. If you build your own Shopify integration, do the same.
  • WooCommerce's WC_Product::get_price() already returns major units — pass it through as-is.
  • Custom backends: send whatever your storefront displays to the customer.

If your numbers look 100× off, this is why.

When to sync

Call setCart at every point your cart can change:

Trigger Re-sync?
Page load (drop-in init) Yes — initial state
User adds an item Yes
User removes an item Yes
User changes quantity Yes
User applies a coupon Yes
User picks a benefit in the drop-in No — the drop-in already knows
User logs in or out Yes (re-sync since cart attribution may change)
Returning to the page after navigation Yes (cart may have changed elsewhere)

The rule of thumb: if the customer would see a different cart subtotal, re-sync.

Avoiding the infinite loop

The drop-in writes back to your cart when the user picks a benefit (typically by setting cart attributes or coupon codes). If your "cart changed" detector treats those writes as a real cart change, you'll end up in this loop:

benefit selected → drop-in writes to cart → your detector fires → you call setCart →
drop-in recalculates → fires benefit again → write to cart → … (forever)

The fix is to tag writes that originate from SimplyClub so your detector can ignore them.

Shopify

The Theme App Block already does this — every cart write the drop-in performs goes through ?simplyclub=true. The block's PerformanceObserver watches /cart/* calls and only resets the discount when another script mutates the cart:

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.initiatorType !== 'fetch') return;
    if (!entry.name.includes('/cart/')) return;
    if (entry.name.includes('simplyclub=true')) return; // ignore our own writes
    if (entry.name.includes('/cart/add.js')) return;    // additive — keep the benefit
    handleCartChange(); // real external mutation — re-sync
  });
});
observer.observe({ type: 'resource' });

Custom integration

Apply the same idea to your stack:

async function updateCart(payload, opts = {}) {
  payload._sc = opts.fromSimplyClub === true; // tag origin
  const res = await fetch("/api/cart", { method: "POST", body: JSON.stringify(payload) });
  if (!opts.fromSimplyClub) {
    await instance.commands.setCart(await readMyCart()); // only re-sync on external writes
  }
  return res.json();
}

Two simple rules:

  1. Mark every write you make on behalf of SimplyClub so your detector can skip it.
  2. Re-sync only when the write is genuinely external — coming from the user's actions, another tab, or your own checkout code.

Recipes

Shopify

If you use the Theme App Block, cart sync is already wired — you don't need to do anything. If you're building your own Shopify integration, use the pattern the block uses:

async function syncShopifyCart() {
  const res = await fetch('/cart.js?_sc=1', { cache: 'no-store' });
  const cart = await res.json();

  const items = cart.items.map((i) => ({
    id: i.id,
    quantity: i.quantity,
    price: i.price / 100,                 // cents → major units
    final_price: i.final_price / 100,
    line_price: i.line_price / 100,
    final_line_price: i.final_line_price / 100,
    total_discount: i.total_discount / 100,
    discounts: (i.discounts || []).map((d) => ({ title: d.title, amount: d.amount / 100 })),
    title: i.product_title,
    sku: i.sku,
    vendor: i.vendor,
    image: i.image,
    url: i.url,
    variant_id: i.variant_id,
    variant_title: i.variant_title,
    variant_inventory_quantity: i.variant_inventory_quantity,
  }));

  await instance.commands.setCart({
    token: cart.token,
    items,
    discount_codes: cart.discount_codes || [],
  });
}

See Install on Shopify for the full block source.

WooCommerce

Render the cart from PHP on page load, then re-sync on AJAX cart events (WooCommerce fires wc_fragment_refresh and updated_cart_totals):

<script>
  window.simplyClub?.commands.setCart({
    token: "wc-<?php echo esc_js(WC()->session?->get_customer_id()); ?>",
    items: [
      <?php foreach (WC()->cart->get_cart() as $item):
        $product = $item['data']; ?>
      {
        id: "<?php echo esc_js($item['key']); ?>",
        quantity: <?php echo (int) $item['quantity']; ?>,
        price: <?php echo (float) $product->get_price(); ?>,
        final_price: <?php echo (float) $product->get_price(); ?>,
        title: "<?php echo esc_js($product->get_name()); ?>",
        sku: "<?php echo esc_js($product->get_sku()); ?>",
        image: "<?php echo esc_js(wp_get_attachment_url($product->get_image_id())); ?>",
        url: "<?php echo esc_js(get_permalink($product->get_id())); ?>"
      },
      <?php endforeach; ?>
    ]
  });

  jQuery(document.body).on('updated_cart_totals wc_fragments_refreshed', () => {
    fetch('/?wc-ajax=get_refreshed_fragments')
      .then((r) => r.json())
      .then(() => location.reload()); // simplest: reload to re-render PHP
  });
</script>

For SPAs or AJAX-heavy themes, expose a small REST endpoint that returns the cart as JSON and call it from JS instead of reloading.

Custom integration / SPA

Wire setCart to your state-management layer. Any time the cart slice changes, push it.

import { dropInLoader } from "https://dropins.simplyclub.co.il/drop-ins/drop-in-loader/index.mjs";
import { useEffect } from "react";
import { useCart } from "./store/cart";

const instance = await dropInLoader.setConfig({ poiId: "YOUR_POI_ID" });

function CartSync() {
  const cart = useCart();
  useEffect(() => {
    instance.commands.setCart({
      token: cart.token,
      items: cart.items.map((i) => ({
        id: i.id,
        quantity: i.qty,
        price: i.unitPrice,         // major units
        final_price: i.unitPrice - i.lineDiscount / i.qty,
        title: i.name,
        sku: i.sku,
        image: i.image,
      })),
      discount_codes: cart.couponCodes,
    });
  }, [cart]);
  return null;
}

Verifying the sync

  1. Open your storefront and start the drop-in.
  2. In DevTools: await window.simplyClub.commands.getCart() — confirm the returned cart matches what's in your storefront.
  3. Add or remove an item. Re-run getCart() — the change should be reflected.
  4. Open the drop-in modal. The displayed totals and applicable benefits should match the current cart.

If the drop-in shows stale data, your re-sync trigger isn't firing on that mutation type.

Common issues

Symptom Cause Fix
Benefits don't apply, but the cart looks right in the storefront Prices off by 100× (sent cents instead of major units). Divide by 100 before calling setCart — or use your platform's display price.
The drop-in shows the cart from a previous page Initial setCart was skipped or setConfig ran before the cart was ready. Await both the cart fetch and setConfig before the first setCart.
Benefit gets reset every few seconds Loop — your detector is treating SimplyClub's own writes as external. Tag SimplyClub-originated writes and skip them in your detector.
Items appear in the drop-in with no name or price Missing required fields (title/name, price, final_price). Provide at least id, quantity, price, final_price, plus a display name.
Cart sync stops after an AJAX update Your re-sync trigger only fired the first time. Subscribe to the event your platform emits on every cart change, not just the initial render.

Related