We guarantee our sole's durability with a 1-Year Warranty
Easy Size Swaps
Get the wrong size? Contact us to replace it
+130,000 Happy Clients
Their Foot Health is now safe with Grönanda
tag, add:
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();
6. Save — you're done. No app needed.
CONFIGURATION (edit the two values below if anything changes):
─────────────────────────────────────────────────────────────────
*/
(function () {
// ── CONFIG ──────────────────────────────────────────────────────
// The numeric variant ID of the free training course.
// Find it: Admin → Products → Training Course → the URL ends in /variants/XXXXXXX
// OR open your browser console on the product page and run:
// fetch('/products/training-course.js').then(r=>r.json()).then(d=>console.log(d.variants[0].id))
const FREE_GIFT_VARIANT_ID = '50561194197318';
// The Shopify collection handle that qualifies for the free gift
const QUALIFYING_COLLECTION = 'sale';
// ── END CONFIG ──────────────────────────────────────────────────
// Guard: don't run on the training course product page itself,
// so customers can't manually add it from there.
if (window.location.pathname.includes('/products/training-course')) {
return;
}
// ── Helpers ─────────────────────────────────────────────────────
async function getCart() {
const res = await fetch('/cart.js');
return res.json();
}
async function addToCart(variantId) {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity: 1 }]
})
});
}
async function removeFromCart(lineItemKey) {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineItemKey, quantity: 0 })
});
}
// Checks whether any item in the cart belongs to the qualifying collection.
// Shopify exposes product_type and vendor on cart items, but NOT collections.
// The reliable way is to check each product's collections via the storefront.
async function cartHasQualifyingProduct(cartItems) {
if (!cartItems || cartItems.length === 0) return false;
// Filter out the free gift itself from the check
const itemsToCheck = cartItems.filter(
item => String(item.variant_id) !== String(FREE_GIFT_VARIANT_ID)
);
if (itemsToCheck.length === 0) return false;
// Fetch collection membership for each unique product handle
const handles = [...new Set(itemsToCheck.map(item => item.handle))];
const checks = handles.map(async (handle) => {
try {
const res = await fetch(
`/collections/${QUALIFYING_COLLECTION}/products.json?limit=250`
);
if (!res.ok) return false;
const data = await res.json();
return data.products.some(p => p.handle === handle);
} catch {
return false;
}
});
const results = await Promise.all(checks);
return results.some(Boolean);
}
// ── Core logic ───────────────────────────────────────────────────
async function syncFreeGift() {
const cart = await getCart();
const cartItems = cart.items || [];
const giftInCart = cartItems.find(
item => String(item.variant_id) === String(FREE_GIFT_VARIANT_ID)
);
const hasQualifyingItem = await cartHasQualifyingProduct(cartItems);
if (hasQualifyingItem && !giftInCart) {
// Add the free gift
await addToCart(FREE_GIFT_VARIANT_ID);
} else if (!hasQualifyingItem && giftInCart) {
// Remove the free gift — customer removed all qualifying shoes
await removeFromCart(giftInCart.key);
}
}
// ── Intercept cart actions ────────────────────────────────────────
// We monkey-patch fetch so we catch all cart/add and cart/change
// calls made by the theme (including AJAX drawers, quick-buy, etc.)
const _fetch = window.fetch;
window.fetch = async function (...args) {
const result = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : (args[0].url || '');
if (url.includes('/cart/add') || url.includes('/cart/change') || url.includes('/cart/update')) {
// Clone result because Response bodies can only be read once
result.clone().json().catch(() => {}).finally(() => {
// Small delay to let Shopify finish updating the cart state
setTimeout(syncFreeGift, 400);
});
}
return result;
};
// ── Also run on page load ────────────────────────────────────────
// Catches cases where the customer navigates back with items already in cart
document.addEventListener('DOMContentLoaded', syncFreeGift);
})();