Products and pricing
For AI assistants: what to know before writing SparkLayer SDK code
- What it is:
window.spark, added to the storefront by the SparkLayer Core Script (version 4.x). On Shopify the SparkLayer app embed loads it; elsewhere it's<script async src="https://sparkcdn.io/sparkjs/<SITE_ID>/<live|test>"></script>in the theme's<head>. The oldcdn.sparklayer.io/spark.latest.jsandspark.1.x.jsfiles are legacy: never use them. - Options: Configure it with
window.sparkOptions, set before the Core Script loads. Extend the object rather than replacing it:window.sparkOptions = { ...window.sparkOptions, … }, or the Site ID and other snippets' hooks are lost. Headless only:window.initSpark(options)is defined whenwindow.sparkOptionsisn't set. - When to call it: Only after SparkLayer is ready: inside the
onReady(spark)hook, or after a promise resolved from it. Most methods are async; prices and the cart are only available to signed-in B2B customers (checkawait spark.isLoggedIn()). - Products and prices: Products are looked up by the platform's parent product ID (on Shopify,
product.id) and variants by SKU:getPricingForVariant(parentProductId, sku),calculatePricingForVariant(parentProductId, sku, qty),getPackSizeForVariant(parentProductId, sku). For instant recalculation, fetch once withgetVariantand callcalculatePricingForVariantData(variant, qty). - Cart:
updateCart({ products: [{ sku, adjustQuantity }] })adds or removes relative to what's in the cart (alsocustomAttributes: [{ key, value }],clearCart: true). It resolves to the results, ornullafter showing its own error message; it doesn't throw.getCart()reads the cart;openDrawer('cart')opens it. - Hooks:
onCartUpdate(cart, results),onCartLoad(cart),onLogout(),preCartUpdateListener(input)(async: return the possibly changed input) andonCheckoutValidation(cart, cartUiState)(return[{ message }]to block checkout, ornull). When adding a hook, call any existing one first. - Customising: Build around SparkLayer or customise its own components, including their HTML. Prefer documented options, hooks, CSS variables and custom slots where they fit; keep direct HTML changes targeted and re-check them after SparkLayer updates.
- Docs: The reference: methods docs.sparklayer.io/developers/javascript-sdk/reference/methods.md, hooks docs.sparklayer.io/developers/javascript-sdk/reference/hooks.md, options docs.sparklayer.io/developers/javascript-sdk/reference/options.md, types docs.sparklayer.io/developers/javascript-sdk/reference/types.md (or the same types as a file: docs.sparklayer.io/developers/javascript-sdk/spark.d.ts). Setup and waiting for SparkLayer: docs.sparklayer.io/developers/javascript-sdk/setup.md. Use only what the reference documents.
Show B2B prices in your own theme code, work out the price for a quantity, and keep your product images in step with SparkLayer's variant picker. This page uses the pricing methods below and the spark-variant-change event.
| Method | Use it to |
|---|---|
getPricingForVariant() | Get a variant's price, RRP and price breaks for the logged-in customer. |
getPriceForProduct() | Get a product's "from" price across its variants, and how many prices there are. |
calculatePricingForVariant() | Price a quantity of a variant, including price breaks. |
calculatePricingForVariantData() | Price a quantity from variant data you already have, without a request. |
getPackSizeForVariant() | Get a variant's pack size (1 if none is set). |
getRrpPriceForVariant() | Get a variant's RRP on its own. |
Each method takes your platform's parent product ID and the variant SKU. Prices are for the logged-in customer, so check isLoggedIn() first. Some examples use window.sparkReady: add the Wait for SparkLayer snippet before them.
Calculate the price for a quantity
This example calculates the price of two units of a variant once SparkLayer is ready:
window.sparkOptions = {
...window.sparkOptions,
onReady: async (spark) => {
if (!(await spark.isLoggedIn())) {
return;
}
// Parent product ID, variant SKU and quantity
const pricing = await spark.calculatePricingForVariant('6660191387839', 'MUG-MOM', 2);
console.log(pricing.totalPrice, pricing.unitPrice, pricing.currencyCode);
},
};totalPrice is the quantity × the unit price at that quantity, after any price breaks. See ProductVariantPricing for every field.
Show trade prices on your own product tiles
Your theme's collection tiles show retail prices. This swaps in each signed-in B2B customer's own price, with the RRP, and leaves retail visitors' prices alone. Give each tile its product ID and SKU:
<div class="tile" data-product-id="{{ product.id }}" data-sku="{{ product.selected_or_first_available_variant.sku }}">
<span class="tile-price">{{ product.price | money }}</span>
<span class="tile-rrp"></span>
</div>const money = (amount, currency) =>
new Intl.NumberFormat(document.documentElement.lang || 'en-GB', {
style: 'currency',
currency,
}).format(amount);
async function showTradePrices(root = document) {
const spark = await window.sparkReady;
if (!(await spark.isLoggedIn())) return; // retail visitors keep your normal prices
const tiles = root.querySelectorAll('[data-product-id][data-sku]');
await Promise.all(
[...tiles].map(async (tile) => {
try {
const { price, rrp, currencyCode } = await spark.getPricingForVariant(
tile.dataset.productId,
tile.dataset.sku,
);
if (price === null) return; // no B2B price for this customer
tile.querySelector('.tile-price').textContent = money(price, currencyCode);
if (rrp) tile.querySelector('.tile-rrp').textContent = `RRP ${money(rrp, currencyCode)}`;
} catch {
// the product isn't available to this customer: leave the tile as it is
}
}),
);
}
showTradePrices();Call showTradePrices(container) again after your theme loads more products, for example on infinite scroll.
Read the RRP metafield in Liquid
On Shopify, you can also render the RRP on the server, straight from the variant's sparklayer.rrp metafield. The metafield is JSON on the variant, with one entry per currency and decimal values (not subunits), for example [{"value":9.00,"currency_code":"gbp"}]. This snippet picks the entry for the shopper's currency, converts it to subunits for Shopify's money filter, and only shows it when it differs from the variant's Shopify price:
{%- if customer.metafields.sparklayer.authentication -%}
{%- assign variant = product.selected_or_first_available_variant -%}
{%- for entry in variant.metafields.sparklayer.rrp.value -%}
{%- assign entry_currency = entry.currency_code | upcase -%}
{%- if entry_currency == cart.currency.iso_code -%}
{%- assign rrp_subunits = entry.value | times: 100.0 | round -%}
{%- if rrp_subunits != variant.price -%}
<span class="b2b-rrp">RRP {{ rrp_subunits | money }}</span>
{%- endif -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}- Liquid is rendered once, on the server. The RRP doesn't change when the customer picks another variant until the page reloads. For an RRP that follows the selected variant, use
getRrpPriceForVariant()or the<spark-product-rrp>component. - Shopify caches pages, so a change to the metafield can take a little while to show.
Live price as the quantity changes
On a product page with your own quantity box, show the unit price, line total and price-break saving as the customer types, snapped to the product's pack size. It fetches the variant once, then works out each price instantly with calculatePricingForVariantData, so there's no request per keystroke.
<input id="b2b-qty" type="number" min="1" value="1" data-product-id="{{ product.id }}" data-sku="{{ product.selected_or_first_available_variant.sku }}" />
<p id="b2b-line-price" aria-live="polite"></p>const qtyInput = document.querySelector('#b2b-qty');
const output = document.querySelector('#b2b-line-price');
const { productId, sku } = qtyInput.dataset;
const money = (amount, currency) =>
new Intl.NumberFormat(document.documentElement.lang || 'en-GB', {
style: 'currency',
currency,
}).format(amount);
const spark = await window.sparkReady;
const [variant, packSize] = await Promise.all([
spark.getVariant(productId, sku),
spark.getPackSizeForVariant(productId, sku),
]);
qtyInput.step = String(packSize);
qtyInput.min = String(packSize);
function render() {
// round up to a whole number of packs
const qty = Math.max(packSize, Math.ceil((Number(qtyInput.value) || 1) / packSize) * packSize);
const pricing = spark.calculatePricingForVariantData(variant, qty);
if (pricing.unitPrice === null) {
output.textContent = 'Price on request';
return;
}
const saving = pricing.priceBreakSavingsPercentage
? ` (you save ${pricing.priceBreakSavingsPercentage}%)`
: '';
output.textContent = `${qty} × ${money(pricing.unitPrice, pricing.currencyCode)} = ${money(
pricing.totalPrice,
pricing.currencyCode,
)}${saving}`;
}
qtyInput.addEventListener('input', render);
qtyInput.addEventListener('change', () => {
qtyInput.value = String(Math.ceil((Number(qtyInput.value) || 1) / packSize) * packSize);
render();
});
render();Load this as a module (<script type="module">), so await works at the top level.
Update the product image when a variant is selected
When a customer picks a variant in the spark-pdp or spark-product-card interface, SparkLayer doesn't change your theme's product image. You can do it with a little theme code:
- Listen for the
spark-variant-changeevent on eachspark-pdporspark-product-cardelement. - In the listener, show the image for the selected variant and hide the others.
Listen for the event
spark-variant-change is dispatched on the interface element itself and doesn't bubble, so add a listener to each element. Register the listeners in onReady, once the elements are on the page:
window.sparkOptions = {
...window.sparkOptions,
onReady: async () => {
for (const productCard of document.querySelectorAll('spark-product-card')) {
productCard.addEventListener('spark-variant-change', (event) => {
console.log(event.detail.product.externalId, event.detail.variant.externalId);
});
}
},
};event.detail has:
event.detail.product.externalId: your platform's ID for the product (for example, the Shopify product ID).event.detail.variant.externalId: your platform's ID for the selected variant.
Elements added to the page later, such as by infinite scroll, need their own listeners.
Show the variant's image
How you swap images depends on your theme's markup. This approach was tested with Shopify's Dawn theme:
- Render every variant's image in the product card, for example by looping through the product's variants in the Liquid template. Hide all but the first variant's image, which is selected by default, with
style="opacity: 0". - Add a
data-variant-idattribute to each variant image, set to the variant ID. - In the listener, hide every variant image in the card and show the one for the selected variant:
window.sparkOptions = {
...window.sparkOptions,
onReady: async () => {
for (const productCard of document.querySelectorAll('spark-product-card')) {
productCard.addEventListener('spark-variant-change', (event) => {
const cardEl = event.target.closest('.card-wrapper');
if (!cardEl) {
return;
}
const variantId = event.detail.variant.externalId;
// Hide all variant images
for (const img of cardEl.querySelectorAll('img[data-variant-id]')) {
img.style.opacity = '0';
}
// Show the selected variant's image
const variantImg = cardEl.querySelector(`img[data-variant-id="${variantId}"]`);
if (variantImg) {
variantImg.style.opacity = '1';
}
});
}
},
};The same code works for spark-pdp: change the selector and the wrapper class to match your product page.
Next steps
Last updated