function hitsyFormatMoney(amount) { const moneyFormat = window.hitsyPriceFormat; const formatPattern = /{{\s*(\w+)\s*}}/; const formatType = moneyFormat.match(formatPattern)?.[1] || "amount"; const formatWithDelimiters = ( num, decimalPlaces = 2, thousandsSeparator = ",", decimalSeparator = ".", ) => { if (typeof num === "string") num = Number(num); if (isNaN(num) || num == null) return "0"; num = num.toFixed(decimalPlaces); const [integerPart, decimalPart] = num.split("."); return ( integerPart.replace( /(\d)(?=(\d{3})+(?!\d))/g, `$1${thousandsSeparator}`, ) + (decimalPart ? `${decimalSeparator}${decimalPart}` : "") ); }; const formatMapping = { amount: formatWithDelimiters(amount, 2), amount_no_decimals: formatWithDelimiters(amount, 0), amount_no_decimals_with_space_separator: formatWithDelimiters( amount, 0, " ", ), amount_with_comma_separator: formatWithDelimiters(amount, 2, ".", ","), amount_no_decimals_with_comma_separator: formatWithDelimiters( amount, 0, ".", ), amount_with_apostrophe_separator: formatWithDelimiters(amount, 2, "'", "."), amount_with_space_separator: formatWithDelimiters(amount, 2, " ", ","), amount_with_period_and_space_separator: formatWithDelimiters( amount, 2, " ", ".", ), }; return moneyFormat.replace( formatPattern, formatMapping[formatType] || formatMapping.amount, ); } function getAmount(config) { const rewardBasis = config.rewardBasis; const cart = window.hitsyCart; if (rewardBasis === "cart-total") { const amount = config.discountSetup === "auto" ? cart.original_total_price : cart.items_subtotal_price; return amount / 100; } else { return cart.item_count; } } let lastKnownGifts = new Map(); function getHitsyCart(config, cart) { const lastKnownGiftCount = Array.from(lastKnownGifts.values()).reduce( (sum, quantity) => sum + quantity, 0, ); const buyXGetYEnabled = config.buyXGetY && config.rewardBasis === "number-of-items"; const currentFreeGiftCount = cart.items .filter((item) => item.final_price === 0) .reduce((sum, item) => sum + item.quantity, 0); let notCountKnownGifts = false; if (lastKnownGiftCount > currentFreeGiftCount) { notCountKnownGifts = true; } else { lastKnownGifts = new Map(); } cart.items = cart.items .filter((item) => { if ( notCountKnownGifts && !buyXGetYEnabled && lastKnownGifts.get(item.variant_id) === item.quantity ) { lastKnownGifts.delete(item.variant_id); return false; } if (item.final_price === 0 && !buyXGetYEnabled) { lastKnownGifts.set(item.variant_id, item.quantity); return false; } const productIds = config.excludedProducts.map((id) => { const list = id.split("/"); return list[list.length - 1]; }); if (config.productExcludeType === "exclude-products") { return !productIds.includes(item.product_id.toString()); } if (config.productExcludeType === "include-products") { return productIds.includes(item.product_id.toString()); } return true; }) .map((item) => { if (buyXGetYEnabled) return item; if (notCountKnownGifts && lastKnownGifts.has(item.variant_id)) { const giftCount = lastKnownGifts.get(item.variant_id); lastKnownGifts.delete(item.variant_id); return { ...item, quantity: item.quantity - giftCount, }; } else { return item; } }); cart.item_count = cart.items.reduce((sum, item) => sum + item.quantity, 0); cart.original_total_price = cart.items.reduce( (sum, item) => sum + item.original_line_price, 0, ); cart.items_subtotal_price = cart.items.reduce( (sum, item) => sum + item.final_line_price, 0, ); return cart; } function hitsyLog(log) { if (!window.HitsyLogs) { window.HitsyLogs = []; } window.HitsyLogs.push(log); } function resolveTierText(tier, field) { if (!tier) return ""; const lang = window.hitsyLanguageCode; const translated = lang && tier.translations && tier.translations[lang] ? tier.translations[lang][field] : undefined; return translated || tier[field] || ""; } function resolveRootText(config, field) { if (!config) return ""; const lang = window.hitsyLanguageCode; const bundle = lang && config.translations ? config.translations[lang] : undefined; return (bundle && bundle[field]) || config[field] || ""; } function createTexts(configIndex) { const config = window.hitsyProgress[configIndex]; const rewardBasis = config.rewardBasis; const tiers = rewardBasis === "cart-total" ? config.tiersCartTotal : config.tiersNumberOfItems; const amount = getAmount(config); const currentTierIndex = tiers .map((tier) => tier.amount) .reduceRight((foundIndex, amountValue, index) => { return foundIndex === -1 && amountValue <= amount ? index : foundIndex; }, -1); const isLastTier = currentTierIndex === tiers.length - 1; const nextTier = currentTierIndex === -1 ? tiers[0] : isLastTier ? null : tiers[currentTierIndex + 1]; const previousTier = currentTierIndex > -1 ? tiers[currentTierIndex] : null; const afterText = resolveTierText(previousTier, "textAfterAchieving"); if (!nextTier) { return [resolveRootText(config, "textWhenAllRewardsAreAchieved"), afterText]; } const leftAmount = nextTier.amount - amount; const formattedAmount = rewardBasis === "cart-total" ? hitsyFormatMoney(leftAmount) : leftAmount.toString(); const beforeText = resolveTierText(nextTier, "textBeforeAchieving").replace( "{amount}", formattedAmount, ); return [beforeText, afterText]; } const hitsyText = "hitsy-text"; class HitsyText extends HTMLElement { connectedCallback() { this.render(); } static get observedAttributes() { return ["text"]; } attributeChangedCallback(name, oldValue, newValue) { if (name === "text" && oldValue !== newValue) { this.updateText(newValue); } } render() { if (this.hasChildNodes()) return; const config = window.hitsyProgress[this.getAttribute("configIndex")]; const textType = this.getAttribute("textType"); const textAlign = textType === "before" ? config.inProgressTextAlign : config.achievedTextAlign; const textSize = config.textSize; const textColor = config.textColor; const gapBetweenElements = config.gapBetweenElements ?? 10; const text = this.getAttribute("text") || ""; this.textView = document.createElement("div"); this.textView.setAttribute("dir", "auto"); this.textView.classList.add( textType === "before" ? "hitsy-text-before" : "hitsy-text-after", ); this.textView.style.width = "100%"; this.textView.style.textAlign = textAlign; this.textView.style.lineHeight = 1.5; this.textView.style.fontSize = `${textSize}px`; this.textView.style.color = textColor; if (textType === "before") { this.textView.style.marginBottom = `${gapBetweenElements}px`; } else if (textType === "after") { this.textView.style.marginTop = `${gapBetweenElements}px`; } this.textView.innerHTML = text; this.appendChild(this.textView); } updateText(text) { if (text) { this.style.display = "block"; this.textView.innerHTML = text; } else { this.style.display = "none"; } } } customElements.define(hitsyText, HitsyText); const hitsyIcon = "hitsy-icon"; const defaultRewardIconByRewardType = { "free-shipping": "delivery1", "order-discount": "discount1", "free-gift": "gift1", }; const rewardIconSvgCache = new Map(); function fetchRewardIconSvg(name) { if (rewardIconSvgCache.has(name)) return rewardIconSvgCache.get(name); const url = `https://progress-assets.hitsyapps.com/reward-tier-icons/${name}.svg`; const promise = fetch(url) .then((res) => res.text()) .then((text) => { const doc = new DOMParser().parseFromString(text, "image/svg+xml"); return doc.querySelector("svg"); }) .catch(() => null); rewardIconSvgCache.set(name, promise); return promise; } class HitsyIcon extends HTMLElement { connectedCallback() { this.config = window.hitsyProgress[this.getAttribute("configIndex")]; this.tiers = this.config.rewardBasis === "cart-total" ? this.config.tiersCartTotal : this.config.tiersNumberOfItems; this.tier = this.tiers[this.getAttribute("data-tier-index")]; this.size = this.getAttribute("size"); this.circleOutlineInProgressColor = this.config.circleOutlineInProgressColor || "#00000041"; this.circleOutlineAchievedColor = this.config.circleOutlineAchievedColor || "#00000041"; this.render(); window.addEventListener("Hitsy:cartUpdated", () => this.onCartUpdated()); } disconnectedCallback() { window.removeEventListener("Hitsy:cartUpdated", () => this.onCartUpdated()); } render() { if (this.hasChildNodes()) return; this.style.height = `${this.size}px`; this.style.width = `${this.size}px`; this.style.borderRadius = "50%"; this.style.boxSizing = "border-box"; if (!this.config.hideIcons) { const iconName = this.tier.rewardIcon || defaultRewardIconByRewardType[this.tier.rewardType]; if (iconName) { fetchRewardIconSvg(iconName).then((svg) => { if (!svg || !this.isConnected) return; this.icon = svg.cloneNode(true); this.icon.setAttribute("width", "100%"); this.icon.setAttribute("height", "100%"); this.icon.style.display = "block"; this.appendChild(this.icon); this.setIconColor(this.tier.amount <= getAmount(this.config)); }); } } this.onCartUpdated(); } setBackground(achieved) { this.style.background = achieved ? this.config.circleAchievedColor : this.config.circleInProgressColor; } setIconColor(achieved) { if (this.config.hideIcons || !this.icon) return; const color = achieved ? this.config.iconAchievedColor : this.config.iconInProgressColor; this.icon .querySelectorAll("[fill]") .forEach((el) => el.setAttribute("fill", color)); } getBorderThickness() { const barThickness = this.config.barThickness; if (barThickness < 10) return 1; if (barThickness < 14) return 2; return 3; } setCircleOutlineColor(achieved) { if (this.config.hideIcons) { this.style.border = undefined; return; } const borderThickness = this.getBorderThickness(); this.style.border = achieved ? `${this.circleOutlineAchievedColor} solid ${borderThickness}px` : `${this.circleOutlineInProgressColor} solid ${borderThickness}px`; } onCartUpdated() { const achieved = this.tier.amount <= getAmount(this.config); this.classList.toggle("hitsy-progress-icon-achieved", achieved); this.classList.toggle("hitsy-progress-icon-in-progress", !achieved); this.setBackground(achieved); this.setIconColor(achieved); this.setCircleOutlineColor(achieved); } } customElements.define(hitsyIcon, HitsyIcon); const hitsyProgress = "hitsy-progress"; class HitsyProgress extends HTMLElement { connectedCallback() { this.config = window.hitsyProgress[this.getAttribute("configIndex")]; this.updateValues(); this.render(); window.addEventListener("Hitsy:cartUpdated", () => this.onCartUpdated()); } disconnectedCallback() { window.removeEventListener("Hitsy:cartUpdated", () => this.onCartUpdated()); } updateValues() { this.amount = getAmount(this.config); this.tiers = this.calculateTiers(this.config, this.amount); } onCartUpdated() { this.updateValues(); this.updateProgress(); this.renderIcons(); } render() { if (this.hasChildNodes()) return; const barThickness = this.config.barThickness; this.iconSize = barThickness * 3; this.style.marginInlineEnd = `${this.iconSize / 2}px`; const container = document.createElement("div"); container.style.width = "100%"; container.style.position = "relative"; container.style.display = "flex"; container.style.height = `${this.iconSize}px`; container.style.justifyContent = "center"; container.style.alignItems = "center"; this.foreground = document.createElement("div"); this.foreground.classList.add("hitsy-progress-fill"); this.foreground.style.width = "0%"; this.foreground.style.height = "100%"; this.foreground.style.backgroundColor = this.config.barColor; this.foreground.style.display = "flex"; this.foreground.style.justifyContent = "center"; this.foreground.style.alignItems = "center"; this.foreground.style.transition = "width 1s ease-in-out"; const progressBackground = document.createElement("div"); progressBackground.classList.add("hitsy-progress-background"); progressBackground.style.display = "flex"; progressBackground.style.width = "100%"; progressBackground.style.backgroundColor = this.config.barBackgroundColor; progressBackground.style.borderRadius = "50px"; progressBackground.style.overflow = "hidden"; progressBackground.style.height = `${barThickness}px`; progressBackground.appendChild(this.foreground); container.appendChild(progressBackground); // Icons overlay: a grid layered over the bar. Each tier gets an equal // column, mirroring HitsyRewardLabels, so icons land on tier boundaries // via layout instead of pixel math — and flip together with the bar in RTL. this.iconsOverlay = document.createElement("div"); this.iconsOverlay.style.position = "absolute"; this.iconsOverlay.style.top = "0"; this.iconsOverlay.style.left = "0"; this.iconsOverlay.style.width = "100%"; this.iconsOverlay.style.height = "100%"; this.iconsOverlay.style.display = "grid"; this.iconsOverlay.style.alignItems = "center"; container.appendChild(this.iconsOverlay); this.appendChild(container); this.updateProgress(); this.renderIcons(); } renderIcons() { const currentIcons = this.iconsOverlay.querySelectorAll( ".hitsy-progress-icon", ); if (this.tiers.length === currentIcons.length) return; const isRtl = getComputedStyle(this).direction === "rtl"; const iconOffset = isRtl ? "-50%" : "50%"; this.iconsOverlay.innerHTML = ""; this.iconsOverlay.style.gridTemplateColumns = `repeat(${this.tiers.length}, 1fr)`; this.tiers.forEach((_tier, index) => { const cell = document.createElement("div"); cell.style.display = "flex"; cell.style.justifyContent = "flex-end"; cell.style.alignItems = "center"; cell.style.overflow = "visible"; const icon = document.createElement(hitsyIcon); icon.classList.add("hitsy-progress-icon"); icon.setAttribute("configIndex", this.getAttribute("configIndex")); icon.setAttribute("data-tier-index", index); icon.setAttribute("size", this.iconSize); icon.style.flexShrink = "0"; icon.style.transform = `translateX(${iconOffset})`; cell.appendChild(icon); this.iconsOverlay.appendChild(cell); }); } calculateTiers(config, amount) { const tiers = config.rewardBasis === "cart-total" ? config.tiersCartTotal : config.tiersNumberOfItems; if (config.displayCurrentTierOnly) { const index = tiers.findIndex((tier) => amount < tier.amount); if (index === -1) return tiers; return tiers.slice(0, index + 1); } else { return tiers; } } calculateProgress() { if (this.amount >= this.tiers[this.tiers.length - 1].amount) return 100; const tierWidth = 100 / this.tiers.length; let width = 0; let calculatedAmount = 0; for (let i = 0; i < this.tiers.length; i++) { const tier = this.tiers[i]; if (this.amount >= tier.amount) { width += tierWidth; calculatedAmount = tier.amount; } else { const tierDifference = this.amount - calculatedAmount; const tierProgress = (tierDifference / (tier.amount - calculatedAmount)) * tierWidth; width += tierProgress; return width; } } return 100; } updateProgress() { const progress = this.calculateProgress(this.amount); this.foreground.style.width = `${progress}%`; } } customElements.define(hitsyProgress, HitsyProgress); const hitsyRewardLabels = "hitsy-reward-labels"; class HitsyRewardLabels extends HTMLElement { connectedCallback() { this.configIndex = this.getAttribute("configIndex"); this.config = window.hitsyProgress[this.configIndex]; this.render(); window.addEventListener("Hitsy:cartUpdated", () => this.onCartUpdated()); } disconnectedCallback() { window.removeEventListener("Hitsy:cartUpdated", () => this.onCartUpdated()); } calculateTiers() { const tiers = this.config.rewardBasis === "cart-total" ? this.config.tiersCartTotal : this.config.tiersNumberOfItems; if (this.config.displayCurrentTierOnly) { const amount = getAmount(this.config); const index = tiers.findIndex((tier) => amount < tier.amount); if (index === -1) return tiers; return tiers.slice(0, index + 1); } return tiers; } render() { const config = this.config; const barThickness = config.barThickness; const iconSize = barThickness * 3; const rewardLabel = config.rewardLabel; const labelOuterDistance = config.labelOuterDistance ?? 0; const labelInnerDistance = config.labelInnerDistance ?? 0; const labelTextSize = config.labelTextSize ?? 11; const labelTextColor = config.labelTextColor; const tiers = this.calculateTiers(); const isRtl = getComputedStyle(this).direction === "rtl"; const labelOffset = isRtl ? "-50%" : "50%"; this.innerHTML = ""; this.container = document.createElement("div"); this.container.style.display = "grid"; this.container.style.gridTemplateColumns = `repeat(${tiers.length}, 1fr)`; this.container.style.marginInlineEnd = `${iconSize / 2}px`; this.container.style.marginTop = rewardLabel === "above" ? `${labelOuterDistance}px` : `${labelInnerDistance}px`; this.container.style.marginBottom = rewardLabel === "below" ? `${labelOuterDistance}px` : `${labelInnerDistance}px`; tiers.forEach((tier, index) => { const cell = document.createElement("div"); cell.style.display = "flex"; cell.style.justifyContent = "flex-end"; cell.style.overflow = "visible"; const label = document.createElement("div"); label.classList.add("hitsy-reward-label"); label.setAttribute("data-tier-index", index); label.style.maxWidth = "100%"; label.style.transform = `translateX(${labelOffset})`; label.style.fontSize = `${labelTextSize}px`; label.style.color = labelTextColor; label.style.lineHeight = "1"; label.style.textAlign = "center"; label.setAttribute("dir", "auto"); label.innerHTML = resolveTierText(tier, "labelText"); cell.appendChild(label); this.container.appendChild(cell); }); this.appendChild(this.container); } onCartUpdated() { if (this.config.displayCurrentTierOnly) { this.render(); } } } customElements.define(hitsyRewardLabels, HitsyRewardLabels); const hitsyCard = "hitsy-card"; class HitsyCard extends HTMLElement { connectedCallback() { this.configIndex = this.getAttribute("configIndex"); this.config = window.hitsyProgress[this.configIndex]; this.horizontalPadding = this.config.horizontalPadding ?? 32; this.verticalPadding = this.config.verticalPadding ?? 16; this.render(); window.addEventListener("Hitsy:cartUpdated", () => this.updateTexts()); } disconnectedCallback() { window.removeEventListener("Hitsy:cartUpdated", () => this.updateTexts()); } render() { if (this.hasChildNodes()) return; this.card = document.createElement("div"); this.card.classList.add("hitsy-card"); this.card.style.border = `${this.config.cardBorderSize}px solid ${this.config.cardBorderColor}`; this.card.style.borderRadius = `${this.config.cardCornerRadius}px`; this.card.style.backgroundColor = this.config.cardBackgroundColor; this.card.style.padding = `${this.verticalPadding}px ${this.horizontalPadding}px`; this.card.style.display = "flex"; this.card.style.flexDirection = "column"; this.beforeTextView = document.createElement(hitsyText); this.beforeTextView.setAttribute("configIndex", this.configIndex); this.beforeTextView.setAttribute("textType", "before"); this.afterTextView = document.createElement(hitsyText); this.afterTextView.setAttribute("configIndex", this.configIndex); this.afterTextView.setAttribute("textType", "after"); this.progress = document.createElement(hitsyProgress); this.progress.setAttribute("configIndex", this.configIndex); const rewardLabel = this.config.rewardLabel; this.card.appendChild(this.beforeTextView); if (rewardLabel === "above") { this.labelsView = document.createElement(hitsyRewardLabels); this.labelsView.setAttribute("configIndex", this.configIndex); this.card.appendChild(this.labelsView); } this.card.appendChild(this.progress); if (rewardLabel === "below") { this.labelsView = document.createElement(hitsyRewardLabels); this.labelsView.setAttribute("configIndex", this.configIndex); this.card.appendChild(this.labelsView); } this.card.appendChild(this.afterTextView); this.appendChild(this.card); this.updateTexts(); } updateTexts() { const [beforeText, afterText] = createTexts(this.configIndex); this.beforeTextView.setAttribute("text", beforeText); this.afterTextView.setAttribute("text", afterText); } } customElements.define(hitsyCard, HitsyCard); const hitsyContainer = "hitsy-container"; class HitsyContainer extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { this.config = window.hitsyProgress[this.getAttribute("configIndex")]; this.cart = window.hitsyCart; this.marginPosition = this.getAttribute("marginPosition"); this.maxWidth = this.getAttribute("maxWidth") || "100%"; this.render(); this.updateVisibility(); window.addEventListener("Hitsy:cartUpdated", () => this.onCartUpdate()); } disconnectedCallback() { window.removeEventListener("Hitsy:cartUpdated", () => this.onCartUpdate()); } render() { if (this.shadowRoot.hasChildNodes()) return; if (this.config.customCss) { const customStyle = document.createElement("style"); customStyle.textContent = this.config.customCss; this.shadowRoot.appendChild(customStyle); } this.container = document.createElement("div"); this.container.style.marginBottom = this.marginPosition.includes("bottom") ? "16px" : undefined; this.container.style.marginTop = this.marginPosition.includes("top") ? "16px" : undefined; this.container.style.maxWidth = this.maxWidth; this.card = document.createElement(hitsyCard); this.card.setAttribute("configIndex", this.getAttribute("configIndex")); this.container.appendChild(this.card); this.shadowRoot.appendChild(this.container); } onCartUpdate() { this.cart = window.hitsyCart; this.updateVisibility(); } updateVisibility() { if (this.config.hideWhenCartIsEmpty) { const cartIsEmpty = this.cart.item_count < 1; this.container.style.display = cartIsEmpty ? "none" : "block"; } else { this.container.style.display = "block"; } } } customElements.define(hitsyContainer, HitsyContainer); function startHitsyCartSync(config) { let debounceTimeout = null; new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { const isValidRequestType = ["xmlhttprequest", "fetch"].includes( entry.initiatorType, ); const isResponseOk = entry.responseStatus === undefined || entry.responseStatus === 200; const ShopifyCartURLs = [ "/cart/update", "/cart/change", "/cart/clear", "/cart/update.js", "/cart/change.js", "/cart/clear.js", "?section_id=cart-drawer", "/cart/add", "/cart/add.js", ]; if ( isValidRequestType && isResponseOk && ShopifyCartURLs.some((url) => entry.name.includes(url)) ) { clearTimeout(debounceTimeout); debounceTimeout = setTimeout(() => { updateCart(); }, 700); } }); }).observe({ entryTypes: ["resource"] }); function updateCart() { fetch("/cart.js") .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.json(); }) .then((cart) => { window.hitsyOriginalCart = { ...cart }; window.hitsyCart = getHitsyCart(config, cart); window.dispatchEvent(new CustomEvent("Hitsy:cartUpdated")); }) .catch((error) => { console.error("There was a problem with the fetch operation:", error); }); } } function getCartDrawer(htmlDocument = document) { for (const selector of window.HitsyProgress.cartDrawerContainerSelectors) { const el = htmlDocument.querySelector(selector); if (el !== null) { return el; } } return null; } function getAvailableElement(targets, htmlDocument = document) { for (const target of targets) { const element = htmlDocument.querySelector(target); if (element !== null) { return element; } } return undefined; } function startFreeGiftSync(config) { const rewardTiers = config.rewardBasis === "cart-total" ? config.tiersCartTotal : config.tiersNumberOfItems; const freeGiftTiers = rewardTiers.filter( (tier) => tier.rewardType === "free-gift", ); if (freeGiftTiers.length === 0) return; if (freeGiftTiers.every((tier) => tier.freeGifts.length === 0)) return; let oldAchievedTierIndex = 0; rewardTiers.forEach((tier, index) => { if (tier.amount <= getAmount(config)) { oldAchievedTierIndex = index + 1; } }); if (window.location.href.endsWith("/cart")) { const freeGiftTiersToApply = rewardTiers .slice(0, oldAchievedTierIndex) .filter((tier) => tier.rewardType === "free-gift"); const achievedFreeGifts = new Map(); freeGiftTiersToApply.forEach((tier) => { tier.freeGifts.forEach((gift) => { gift.variantIds.forEach((variantId) => { const id = variantId.split("/").pop(); achievedFreeGifts.set(id, achievedFreeGifts.get(id) + 1 || 1); }); }); }); const cartLines = window.hitsyOriginalCart.items; cartLines.forEach((line) => { const id = line.variant_id.toString(); if (achievedFreeGifts.has(id)) { const newQty = achievedFreeGifts.get(id) - line.quantity; if (newQty <= 0) { achievedFreeGifts.delete(id); } else { achievedFreeGifts.set(id, newQty); } } }); if (achievedFreeGifts.size > 0) { fetch(window.Shopify.routes.root + "cart/add.js", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ items: Array.from(achievedFreeGifts.entries()).map( ([giftVariantId, quantity]) => { return { id: giftVariantId, quantity: quantity, }; }, ), }), }).then((response) => { if (response.ok) { window.location.reload(); } else { hitsyLog("Free gift cart/add.js error: " + response.status + " " + response.statusText); } }); } } window.addEventListener("Hitsy:cartUpdated", async () => { let newAchievedTierIndex = 0; rewardTiers.forEach((tier, index) => { if (tier.amount <= getAmount(config)) { newAchievedTierIndex = index + 1; } }); if (newAchievedTierIndex === oldAchievedTierIndex) return; if (newAchievedTierIndex < oldAchievedTierIndex) { // Remove free gifts const decreasedFreeGiftTiers = rewardTiers .slice(newAchievedTierIndex, oldAchievedTierIndex) .filter((tier) => tier.rewardType === "free-gift"); if (decreasedFreeGiftTiers.length > 0) { const freeGiftsToRemove = new Map(); decreasedFreeGiftTiers.forEach((tier) => { tier.freeGifts.forEach((gift) => { gift.variantIds.forEach((variantId) => { const id = variantId.split("/").pop(); freeGiftsToRemove.set(id, freeGiftsToRemove.get(id) + 1 || 1); }); }); }); const cartLinesUpdate = new Map(); freeGiftsToRemove.forEach((quantity, id) => { const cartLines = window.hitsyOriginalCart.items.filter( (item) => item.variant_id.toString() == id, ); let quantityToRemove = quantity; for (const cartLine of cartLines) { if (quantityToRemove > 0) { if (cartLine.quantity > quantityToRemove) { cartLinesUpdate.set( cartLine.key, cartLine.quantity - quantityToRemove, ); quantityToRemove = 0; } else { cartLinesUpdate.set(cartLine.key, 0); quantityToRemove -= cartLine.quantity; } } } }); const response = await fetch( window.Shopify.routes.root + "cart/update.js", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ updates: Object.fromEntries(cartLinesUpdate), sections: window.HitsyProgress.freeGiftCartDrawerSection, }), }, ); const json = await response.json(); refreshCart(json.sections[window.HitsyProgress.freeGiftCartDrawerSection]); } oldAchievedTierIndex = newAchievedTierIndex; } else { // Add free gifts const increasedFreeGiftTiers = rewardTiers .slice(oldAchievedTierIndex, newAchievedTierIndex) .filter((tier) => tier.rewardType === "free-gift"); if (increasedFreeGiftTiers.length > 0) { const freeGiftsToAdd = new Map(); increasedFreeGiftTiers.forEach((tier) => { tier.freeGifts.forEach((gift) => { gift.variantIds.forEach((variantId) => { const id = variantId.split("/").pop(); freeGiftsToAdd.set(id, freeGiftsToAdd.get(id) + 1 || 1); }); }); }); const response = await fetch( window.Shopify.routes.root + "cart/add.js", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ items: Array.from(freeGiftsToAdd.entries()).map( ([giftVariantId, quantity]) => { return { id: giftVariantId, quantity: quantity, }; }, ), sections: window.HitsyProgress.freeGiftCartDrawerSection, }), }, ); if (!response.ok) { hitsyLog("Free gift cart/add.js error: " + response.status + " " + response.statusText); return; } const json = await response.json(); refreshCart(json.sections[window.HitsyProgress.freeGiftCartDrawerSection]); } oldAchievedTierIndex = newAchievedTierIndex; } }); } function refreshCart(cartDrawer) { if (window.location.href.endsWith("/cart")) { window.location.reload(); return; } hitsyLog("Refreshing cart after free gift: " + cartDrawer); if (!cartDrawer) return; const parser = new DOMParser(); const html = parser.parseFromString(cartDrawer, "text/html"); let newCartDrawer = undefined; if (window.HitsyProgress.freeGiftCartDrawerSelector) { newCartDrawer = html.querySelector(window.HitsyProgress.freeGiftCartDrawerSelector); } else { newCartDrawer = getCartDrawer(html); } let currentDrawer = undefined; if (window.HitsyProgress.freeGiftCartDrawerSelector) { currentDrawer = document.querySelector(window.HitsyProgress.freeGiftCartDrawerSelector); } else { currentDrawer = getCartDrawer(); } if (currentDrawer && newCartDrawer) { currentDrawer.innerHTML = newCartDrawer.innerHTML; const uniqueClasses = new Set([ ...currentDrawer.classList, ...newCartDrawer.classList, ]); currentDrawer.className = [...uniqueClasses].join(" "); } } window.HitsyProgress = { cartDrawerTargets: [ ".cart__empty-text", ".drawer__content.drawer__content--center", ".empty-state__icon-wrapper", ".cart-drawer__line-items", ".cart-drawer__empty-content", 'form[action="/cart"] #CartContainer', 'form[action="/cart"] .da_trustbadge + .Drawer__Container', 'form[action="/en-me/cart"]', 'form[action="/fr-ca/cart"]', "#drawer-cart .drawer__content .drawer__body", 'form[action="/cart"] .drawer__inner', "#cart-notification #cart-notification-product", ".order-value-booster-side-cart .cart-drawer__items", "#Cart-Drawer .side-panel-content", "#cartSlideoutWrapper .ajax-cart--top-wrapper", "#cart-drawer .cart-drawer__items", 'form[action="/cart"] .cart-list', "#dropdnMinicartPopup .cart-form-element", "cart-notification-drawer .quick-buy-drawer__info", "cart-drawer .cart-drawer__line-items", "#CartDrawer .ajaxcart__product", "sidebar-drawer#site-cart-sidebar .sidebar__body", "hdt-cart-drawer .hdt-mini-cart__header-title", ".mini-cart-wrap.drawer #header-mini-cart-content", 'form[action="/cart"]', "#CartDrawer-Form", "#halo-cart-sidebar .previewCart-wrapper", ".cart-container .w-commerce-commercecartform", ".qsc2-drawer .qsc2-drawer-rows", ".snippet-quick-cart .cart-items", ".mini-cart #mini-cart-form", "cart-form.cart-drawer .cart-item-list", ".js-minicart .product-cart", ], cartDrawerContainerSelectors: [ "cart-drawer", ".cart-drawer__inner", "#sidebar-cart", "#CartDrawer", "#offcanvas-cart", ], cartNotificationTargets: ["#cart-notification-product"], cartNotificationContainerSelectors: ["#cart-notification"], productTargets: [".product-form__buttons", ".product-form", ".product-info"], productMaxWidth: undefined, cartPageTargets: [ "cart-items .page-width", "cart-items", "#main section .empty-state", "#main section .page-content", ".cart", "form[action='/cart']", ], freeGiftCartDrawerSection: "cart-drawer", freeGiftCartDrawerSelector: undefined, preloadDelay: undefined, }; const hitsyConfigs = window.hitsyProgress; let hitsyConfig = undefined; let hitsyConfigIndex = undefined; if (hitsyConfigs && hitsyConfigs.length > 0) { hitsyConfigs.some((c, index) => { const isLegacyMode = c.currencyCode !== undefined; if (isLegacyMode) { if (c.currencyCode === window.hitsyCart.currency) { hitsyConfig = c; hitsyConfigIndex = index; return true; } else { return false; } } else { if (c.countryCodes.includes(window.hitsyCountryCode)) { hitsyConfig = c; hitsyConfigIndex = index; return true; } else { return false; } } }); } if (hitsyConfig) { window.hitsyCart = getHitsyCart(hitsyConfig, window.hitsyCart); loadHitsyThemeConfig(() => { if (window.HitsyProgress.preloadDelay) { setTimeout(() => { renderHitsyProgress(); }, window.HitsyProgress.preloadDelay); } else { renderHitsyProgress(); } }); } function loadHitsyThemeConfig(onLoad) { const hitsyTheme = window.Shopify.theme; let themeUrl = undefined; let cacheKey = undefined; if (hitsyTheme.theme_store_id) { themeUrl = `https://progress-assets.hitsyapps.com/themes/store-id/${hitsyTheme.theme_store_id}.json`; cacheKey = `hitsy_theme_store_id_${hitsyTheme.theme_store_id}`; } else if (hitsyTheme.schema_name) { themeUrl = `https://progress-assets.hitsyapps.com/themes/schema-name/${hitsyTheme.schema_name}.json`; cacheKey = `hitsy_theme_schema_${hitsyTheme.schema_name}`; } if (!themeUrl) { onLoad(); return; } const cachedData = getCachedThemeConfig(cacheKey); if (cachedData) { applyCustomThemeConfig(cachedData); onLoad(); return; } fetch(themeUrl, { headers: { "Cache-Control": "max-age=3600", "X-Requested-With": "XMLHttpRequest", }, }) .then((response) => { if (response.status === 404) { setCachedThemeConfig(cacheKey, {}); return null; } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then((data) => { if (data) { setCachedThemeConfig(cacheKey, data); applyCustomThemeConfig(data); } }) .finally(() => { onLoad(); }); } function applyCustomThemeConfig(data) { if (data.cartDrawerTargets) { window.HitsyProgress.cartDrawerTargets = data.cartDrawerTargets; } if (data.cartDrawerContainerSelectors) { window.HitsyProgress.cartDrawerContainerSelectors = data.cartDrawerContainerSelectors; } if (data.cartNotificationTargets) { window.HitsyProgress.cartNotificationTargets = data.cartNotificationTargets; } if (data.cartNotificationContainerSelectors) { window.HitsyProgress.cartNotificationContainerSelectors = data.cartNotificationContainerSelectors; } if (data.productTargets) { window.HitsyProgress.productTargets = data.productTargets; } if (data.productMaxWidth) { window.HitsyProgress.productMaxWidth = data.productMaxWidth; } if (data.cartPageTargets) { window.HitsyProgress.cartPageTargets = data.cartPageTargets; } if (data.freeGiftCartDrawerSelector) { window.HitsyProgress.freeGiftCartDrawerSelector = data.freeGiftCartDrawerSelector; } if (data.freeGiftCartDrawerSection) { window.HitsyProgress.freeGiftCartDrawerSection = data.freeGiftCartDrawerSection; } if (data.preloadDelay) { window.HitsyProgress.preloadDelay = data.preloadDelay; } } function getCachedThemeConfig(key) { try { const cached = localStorage.getItem(key); if (!cached) return null; const { data, timestamp } = JSON.parse(cached); const now = Date.now(); if (now - timestamp < 3600000) { return data; } else { localStorage.removeItem(key); return null; } } catch (error) { console.error("Error reading cached theme config:", error); return null; } } function setCachedThemeConfig(key, data) { try { const cacheData = { data: data, timestamp: Date.now(), }; localStorage.setItem(key, JSON.stringify(cacheData)); } catch (error) { console.error("Error caching theme config:", error); } } function renderHitsyProgress() { let startSync = false; if ( window.hitsyTemplate.startsWith("product") && hitsyConfig.displayPages.includes("product") ) { if (!isProductExcluded(hitsyConfig)) { renderProgress(); startSync = true; } } const showInQuickCart = hitsyConfig.displayPages.includes("cart") || hitsyConfig.displayPages.includes("quick-cart"); const showOnCartPage = hitsyConfig.displayPages.includes("cart") || hitsyConfig.displayPages.includes("cart-page"); if (showInQuickCart) { renderProgressInCartNotificationOrDrawer(); startSync = true; } if (showOnCartPage && window.hitsyTemplate.startsWith("cart")) { window.HitsyProgress.cartPageTargets.some((target) => { const targetElement = document.querySelector(target); if (targetElement) { const container = document.createElement(hitsyContainer); container.setAttribute("configIndex", hitsyConfigIndex); container.setAttribute("marginPosition", "top|bottom"); container.style.display = "block"; targetElement.prepend(container); return true; } return false; }); startSync = true; } const customPlacements = document.querySelectorAll(".hitsy-progress-block"); customPlacements.forEach((placement) => { if (!placement.hasChildNodes()) { const container = document.createElement(hitsyContainer); container.setAttribute("configIndex", hitsyConfigIndex); container.setAttribute("marginPosition", ""); container.style.display = "block"; placement.appendChild(container); } }); if (customPlacements.length !== 0) startSync = true; if (startSync) { startHitsyCartSync(hitsyConfig); startFreeGiftSync(hitsyConfig); } } function observeCartDrawer() { const observer = new MutationObserver((mutations) => { mutations.forEach(() => checkForHitsyContainer()); }); const cartDrawer = getCartDrawer(); if (cartDrawer) { observer.observe(cartDrawer, { childList: true, subtree: true, attributes: true, }); } checkForHitsyContainer(); } function observeCartNotification() { const observer = new MutationObserver((mutations) => { mutations.forEach(() => renderHitsyContainerInNotification()); }); const cartNotification = getAvailableElement( window.HitsyProgress.cartNotificationContainerSelectors, ); if (cartNotification) { observer.observe(cartNotification, { childList: true, subtree: true, attributes: true, }); } renderHitsyContainerInNotification(); } function checkForHitsyContainer() { const cartDrawer = getCartDrawer(); if (cartDrawer) { const hitsyContainer = cartDrawer.querySelector("hitsy-container"); if (!hitsyContainer) { renderProgressInDrawer(); } } } function renderHitsyContainerInNotification() { const cartNotification = getAvailableElement( window.HitsyProgress.cartNotificationContainerSelectors, ); if (cartNotification) { const currentProgress = cartNotification.querySelector(hitsyContainer); if (!currentProgress) { const container = document.createElement(hitsyContainer); container.setAttribute("configIndex", hitsyConfigIndex); container.setAttribute("marginPosition", "top|bottom"); container.style.display = "block"; const targetElement = getAvailableElement(window.HitsyProgress.cartNotificationTargets); if (targetElement) { targetElement.parentNode.insertBefore(container, targetElement); } } } } function renderProgress() { window.HitsyProgress.productTargets.some((target) => { const targetElement = document.querySelector(target); if (targetElement) { const container = document.createElement(hitsyContainer); container.setAttribute("configIndex", hitsyConfigIndex); container.setAttribute("marginPosition", "top"); if (window.HitsyProgress.productMaxWidth) { container.setAttribute( "maxWidth", window.HitsyProgress.productMaxWidth, ); } container.style.display = "block"; targetElement.appendChild(container); return true; } return false; }); } function renderProgressInCartNotificationOrDrawer() { observeCartDrawer(); observeCartNotification(); } function renderProgressInDrawer() { window.HitsyProgress.cartDrawerTargets.some((target) => { const targetElement = document.querySelector(target); if (targetElement) { const container = document.createElement(hitsyContainer); container.setAttribute("configIndex", hitsyConfigIndex); container.setAttribute("marginPosition", "bottom"); container.style.display = "block"; targetElement.parentNode.insertBefore(container, targetElement); return true; } return false; }); } function isProductExcluded(config) { if (config.productExcludeType === "no-exclude") return false; const productIds = config.excludedProducts; if (productIds.length === 0) return false; const isInList = productIds.some((id) => { const split = id.split("/"); return split[split.length - 1] === window.hitsyProduct.id.toString(); }); if (config.productExcludeType === "exclude-products") return isInList; if (config.productExcludeType === "include-products") return !isInList; return false; }