<script>
const STRATEGY_ID = "YOUR_STRATEGY_ID";
const STRATEGY_API_KEY = "YOUR_STRATEGY_API_KEY";
const STRATEGY_BACKEND_URL = "https://start.aftersell.app";
let cartToken = null;
const fetchCartToken = async () => {
const res = await fetch("/cart.js");
const c = await res.json();
cartToken = c.token;
};
const mapCartItemToContext = (cartItem) => ({
productId: "gid://shopify/Product/" + cartItem.productId.toString(),
variantId: "gid://shopify/ProductVariant/" + cartItem.variantId.toString(),
tags: [],
title: cartItem.title,
vendor: cartItem.vendor,
productType: cartItem.productType,
handle: cartItem.handle,
quantity: cartItem.quantity,
price: cartItem.originalPrice / 100,
});
// --- StrategyProduct -> Upcart Product conversion -----------------------
const gidToNumericId = (gid) => Number(String(gid).split("/").pop());
const priceStringToCents = (price) =>
price == null ? null : Math.round(parseFloat(price) * 100);
const strategyMetafieldsToProductMetafields = (metafields = []) => {
const grouped = {};
for (const { namespace, key, value } of metafields) {
grouped[namespace] = grouped[namespace] || {};
grouped[namespace][key] = value;
}
return { product: grouped };
};
const deriveProductOptions = (variants = []) => {
const byName = new Map();
for (const variant of variants) {
(variant.selectedOptions ?? []).forEach((opt, idx) => {
if (!byName.has(opt.name)) {
byName.set(opt.name, { name: opt.name, position: idx + 1, values: [] });
}
const entry = byName.get(opt.name);
if (!entry.values.includes(opt.value)) entry.values.push(opt.value);
});
}
return [...byName.values()];
};
const mapStrategyVariantToProductVariant = (variant) => {
const selected = variant.selectedOptions ?? [];
const optionValues = selected.map((o) => o.value);
return {
id: gidToNumericId(variant.variantId),
title: variant.title,
option1: optionValues[0] ?? null,
option2: optionValues[1] ?? null,
option3: optionValues[2] ?? null,
sku: variant.sku ?? "",
requires_shipping: true,
taxable: true,
featured_image: null,
available: variant.availableForSale,
name: variant.title,
public_title: variant.title,
options: optionValues,
price: priceStringToCents(variant.price) ?? 0,
weight: 0,
compare_at_price: priceStringToCents(variant.compareAtPrice),
inventory_management: "",
barcode: null,
requires_selling_plan: false,
selling_plan_allocations: [],
};
};
const mapStrategyProductToProduct = (product) => {
const variants = (product.variants ?? []).map(mapStrategyVariantToProductVariant);
const variantPrices = variants.map((v) => v.price);
const priceMin = variantPrices.length ? Math.min(...variantPrices) : (priceStringToCents(product.price) ?? 0);
const priceMax = variantPrices.length ? Math.max(...variantPrices) : (priceStringToCents(product.price) ?? 0);
const variantCompareAtPrices = variants
.map((v) => v.compare_at_price)
.filter((p) => p != null);
const compareAtMin = variantCompareAtPrices.length ? Math.min(...variantCompareAtPrices) : 0;
const compareAtMax = variantCompareAtPrices.length ? Math.max(...variantCompareAtPrices) : 0;
const images = (product.images ?? [])
.slice()
.sort((a, b) => a.position - b.position)
.map((img) => img.src);
return {
id: gidToNumericId(product.productId),
title: product.title,
handle: product.handle,
description: product.description ?? "",
published_at: "",
created_at: "",
vendor: product.vendor ?? "",
type: product.productType ?? "",
tags: [...(product.tags ?? [])],
price: priceStringToCents(product.price) ?? 0,
price_min: priceMin,
price_max: priceMax,
available: product.availableForSale,
price_varies: priceMin !== priceMax,
compare_at_price: priceStringToCents(product.compareAtPrice),
compare_at_price_min: compareAtMin,
compare_at_price_max: compareAtMax,
compare_at_price_varies: compareAtMin !== compareAtMax,
variants,
images,
featured_image: images[0] ?? "",
options: deriveProductOptions(product.variants),
url: product.url ?? "",
media: [],
requires_selling_plan: false,
selling_plan_groups: [],
metafields: strategyMetafieldsToProductMetafields(product.metafields),
};
};
// -----------------------------------------------------------------------
let replacedUpsells = null;
let lastFetchedCartSignature = null;
const cartSignature = (cart) =>
JSON.stringify(cart.items.map((i) => [i.variantId, i.quantity]));
const runStrategyEvaluation = async () => {
if (!STRATEGY_ID || !STRATEGY_API_KEY) return;
await fetchCartToken();
if (!cartToken) return;
const cart = window.upcartGetCart();
if (!cart) return;
const signature = cartSignature(cart);
if (signature === lastFetchedCartSignature) return;
lastFetchedCartSignature = signature;
const cartContext = {
subtotal: cart.total_price / 100,
itemCount: cart.items.reduce((acc, item) => acc + item.quantity, 0),
lineCount: cart.items.length,
};
const products = cart.items.map(mapCartItemToContext);
const res = await fetch(STRATEGY_BACKEND_URL + "/api/public/strategy/evaluate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Strategy-Api-Key": STRATEGY_API_KEY,
},
body: JSON.stringify({
shopDomain: window.Shopify.shop,
strategyId: STRATEGY_ID,
context: {
products,
cartToken,
cart: cartContext.itemCount > 0 ? cartContext : undefined,
session: { currencyCode: window.Shopify.currency.active },
},
}),
});
replacedUpsells = await res.json();
if (typeof window.upcartRefreshCart === "function") {
window.upcartRefreshCart();
}
};
window.upcartSubscribeCartUpdated(runStrategyEvaluation);
const waitForUpcartCart = (timeoutMs = 10000) =>
new Promise((resolve) => {
const start = Date.now();
const check = () => {
if (window.upcartGetCart()) return resolve(true);
if (Date.now() - start > timeoutMs) return resolve(false);
setTimeout(check, 100);
};
check();
});
waitForUpcartCart().then((ready) => {
if (ready) runStrategyEvaluation();
});
window.upcartModifyListOfUpsells = () => {
if (!replacedUpsells || !Array.isArray(replacedUpsells.products)) return;
try {
return replacedUpsells.products.map(mapStrategyProductToProduct);
} catch (err) {
console.error("upcartModifyListOfUpsells mapping failed", err);
return;
}
};
</script>