1(function() { 2 'use strict'; 3 4 function getOrigin() { 5 return window.location.protocol + '//' + window.location.host; 6 } 7 8 function getStorefront() { 9 if (window.mfsPricing && window.mfsPricing.storefront) { 10 return window.mfsPricing.storefront; 11 } 12 return document.body ? document.body.getAttribute('data-storefront') : null; 13 } 14 15 // Storefront discount percentages come from /wp-json/mfs/v1/pricing-config 16 // (the WP admin "Storefront Pricing" settings) â never from values baked 17 // into page HTML. Until the fetch completes, both are 0 and buttons show 18 // FastSpring list prices. 19 var discountConfig = { naadaDiscountPercent: 0, geoshredDiscountPercent: 0 }; 20 21 function getNaadaDiscountPercent() { 22 var parsed = parseFloat(discountConfig.naadaDiscountPercent); 23 return isNaN(parsed) ? 0 : parsed; 24 } 25 26 function getGeoshredDiscountPercent() { 27 var parsed = parseFloat(discountConfig.geoshredDiscountPercent); 28 return isNaN(parsed) ? 0 : parsed; 29 } 30 31 function requestPricingConfig(onDone) { 32 var url = getOrigin() + '/wp-json/mfs/v1/pricing-config'; 33 34 function applyConfig(config) { 35 if (config && typeof config === 'object') { 36 discountConfig = config; 37 } 38 onDone(); 39 } 40 41 function failConfig(error) { 42 console.error('MFS Pricing: Failed to load pricing config:', error); 43 onDone(); 44 } 45 46 if (typeof fetch === 'function') { 47 fetch(url) 48 .then(function(response) { 49 if (!response.ok) throw new Error('API error: ' + response.status); 50 return response.json(); 51 }) 52 .then(applyConfig) 53 .catch(failConfig); 54 return; 55 } 56 57 try { 58 var xhr = new XMLHttpRequest(); 59 xhr.open('GET', url, true); 60 xhr.onreadystatechange = function() { 61 if (xhr.readyState !== 4) return; 62 63 if (xhr.status >= 200 && xhr.status < 300) { 64 try { 65 applyConfig(JSON.parse(xhr.responseText)); 66 } catch (e) { 67 failConfig(e); 68 } 69 } else { 70 failConfig(new Error('API error: ' + xhr.status)); 71 } 72 }; 73 xhr.send(); 74 } catch (e) { 75 failConfig(e); 76 } 77 } 78 79 function closestElement(start, selector) { 80 var element = start; 81 while (element && element.nodeType === 1) { 82 if (element.matches && element.matches(selector)) { 83 return element; 84 } 85 element = element.parentElement; 86 } 87 return null; 88 } 89 90 function addUniqueElement(list, element) { 91 if (!element) { 92 return; 93 } 94 for (var i = 0; i < list.length; i++) { 95 if (list[i] === element) { 96 return; 97 } 98 } 99 list.push(element); 100 } 101 102 function clearNode(node) { 103 while (node.firstChild) { 104 node.removeChild(node.firstChild); 105 } 106 } 107 108 function getPortalSession() { 109 try { 110 var session = localStorage.getItem('mfs_portal_session'); 111 var email = localStorage.getItem('mfs_portal_email'); 112 if (!session || !email) { 113 return null; 114 } 115 return { session: session, email: email }; 116 } catch (e) { 117 return null; 118 } 119 } 120 121 function getPortalRedirect() { 122 var path = window.location.pathname || '/'; 123 var search = window.location.search || ''; 124 var hash = window.location.hash || ''; 125 return path + search + hash; 126 } 127 128 function hideLoginGate() { 129 var overlay = document.getElementById('mfs-login-gate'); 130 if (overlay) { 131 overlay.style.display = 'none'; 132 } 133 } 134 135 function ensureLoginGate() { 136 var overlay = document.getElementById('mfs-login-gate'); 137 if (!overlay) { 138 overlay = document.createElement('div'); 139 overlay.id = 'mfs-login-gate'; 140 overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:999999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);padding:20px;'; 141 overlay.innerHTML = '' 142 + '<div style="position:relative;width:100%;max-width:520px;background:#f5f5f5 !important;border-radius:8px;padding:32px 24px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,0.25);">' 143 + ' <button type="button" data-mfs-close="1" aria-label="Close" style="position:absolute;top:6px;right:10px;background:transparent;border:none;font-size:28px;color:#888;cursor:pointer;line-height:1;">×</button>' 144 + ' <h3 style="margin:0 0 12px;color:#333 !important;">Sign in to continue</h3>' 145 + ' <p style="margin:0 0 20px;color:#666 !important;">Create an account or sign in to purchase.</p>' 146 + ' <a data-mfs-portal-link="1" class="wp-block-button__link" style="display:inline-block;padding:12px 24px;background:#ff5722 !important;color:white !important;text-decoration:none;border-radius:4px;">Create or Login to Your Account</a>' 147 + '</div>'; 148 overlay.addEventListener('click', function(e) { 149 if (e.target === overlay) { 150 hideLoginGate(); 151 } 152 }); 153 document.body.appendChild(overlay); 154 155 var closeBtn = overlay.querySelector('[data-mfs-close]'); 156 if (closeBtn) { 157 closeBtn.addEventListener('click', function(e) { 158 e.preventDefault(); 159 hideLoginGate(); 160 }); 161 } 162 } 163 164 var link = overlay.querySelector('[data-mfs-portal-link]'); 165 if (link) { 166 link.href = '/customer-portal?redirect=' + encodeURIComponent(getPortalRedirect()); 167 } 168 169 overlay.style.display = 'flex'; 170 } 171 172 function getCurrencyFractionDigits(currency) { 173 try { 174 return new Intl.NumberFormat(navigator.language, { 175 style: 'currency', 176 currency: currency 177 }).resolvedOptions().maximumFractionDigits; 178 } catch (e) { 179 return 2; 180 } 181 } 182 183 function roundCurrency(value, currency) { 184 var digits = getCurrencyFractionDigits(currency); 185 var factor = Math.pow(10, digits); 186 return Math.round(value * factor) / factor; 187 } 188 189 function formatCurrency(value, currency) { 190 try { 191 return new Intl.NumberFormat(navigator.language, { 192 style: 'currency', 193 currency: currency 194 }).format(value); 195 } catch (e) { 196 return currency + ' ' + value; 197 } 198 } 199 200 function parseFormattedPrice(formatted) { 201 if (!formatted) { 202 return null; 203 } 204 205 var raw = String(formatted).replace(/\u00a0/g, ' ').trim(); 206 var firstDigit = raw.search(/[0-9]/); 207 if (firstDigit === -1) { 208 return null; 209 } 210 211 var lastDigit = -1; 212 for (var i = raw.length - 1; i >= 0; i--) { 213 if (raw[i] >= '0' && raw[i] <= '9') { 214 lastDigit = i; 215 break; 216 } 217 } 218 if (lastDigit === -1) { 219 return null; 220 } 221 222 var prefix = raw.slice(0, firstDigit); 223 var suffix = raw.slice(lastDigit + 1); 224 var numberPart = raw.slice(firstDigit, lastDigit + 1); 225 226 // Determine decimal separator as the last '.' or ',' that has digits after it. 227 var lastDot = numberPart.lastIndexOf('.'); 228 var lastComma = numberPart.lastIndexOf(','); 229 var decimalSep = null; 230 var decimalIndex = -1; 231 232 if (lastDot !== -1 && lastComma !== -1) { 233 decimalIndex = lastDot >
233 lastComma ? lastDot : lastComma; 234 decimalSep = numberPart[decimalIndex]; 235 } else if (lastDot !== -1) { 236 decimalIndex = lastDot; 237 decimalSep = '.'; 238 } else if (lastComma !== -1) { 239 decimalIndex = lastComma; 240 decimalSep = ','; 241 } 242 243 var decimals = 0; 244 if (decimalIndex !== -1) { 245 decimals = numberPart.length - decimalIndex - 1; 246 } 247 248 // Determine thousands separator by looking for the "other" separator if both are present. 249 var thousandsSep = null; 250 if (decimalSep) { 251 thousandsSep = decimalSep === '.' ? ',' : '.'; 252 if (numberPart.indexOf(thousandsSep) === -1) { 253 thousandsSep = null; 254 } 255 } else { 256 if (numberPart.indexOf(',') !== -1) thousandsSep = ','; 257 else if (numberPart.indexOf('.') !== -1) thousandsSep = '.'; 258 else if (numberPart.indexOf(' ') !== -1) thousandsSep = ' '; 259 } 260 261 var normalized = numberPart; 262 if (thousandsSep) { 263 var reThousands = new RegExp('\\' + thousandsSep, 'g'); 264 normalized = normalized.replace(reThousands, ''); 265 } 266 normalized = normalized.replace(/ /g, ''); 267 if (decimalSep && decimalSep !== '.') { 268 normalized = normalized.replace(decimalSep, '.'); 269 } 270 normalized = normalized.replace(/[^\d.-]/g, ''); 271 272 var value = parseFloat(normalized); 273 if (isNaN(value)) { 274 return null; 275 } 276 277 return { 278 prefix: prefix, 279 suffix: suffix, 280 value: value, 281 decimals: decimals, 282 decimalSep: decimalSep || '.', 283 thousandsSep: thousandsSep 284 }; 285 } 286 287 function formatLikeOriginal(originalInfo, value) { 288 var decimals = originalInfo.decimals; 289 var decimalSep = originalInfo.decimalSep || '.'; 290 var thousandsSep = originalInfo.thousandsSep; 291 292 var rounded = decimals > 0 ? value.toFixed(decimals) : String(Math.round(value)); 293 var parts = rounded.split('.'); 294 var integerPart = parts[0]; 295 var fractionalPart = parts.length > 1 ? parts[1] : ''; 296 297 if (thousandsSep && thousandsSep !== ' ') { 298 var sign = ''; 299 if (integerPart[0] === '-') { 300 sign = '-'; 301 integerPart = integerPart.slice(1); 302 } 303 integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep); 304 integerPart = sign + integerPart; 305 } 306 307 var formattedNumber = integerPart; 308 if (decimals > 0) { 309 formattedNumber += decimalSep + fractionalPart; 310 } 311 312 return originalInfo.prefix + formattedNumber + originalInfo.suffix; 313 } 314 315 function roundToDecimals(value, decimals) { 316 var d = typeof decimals === 'number' ? decimals : 2; 317 var factor = Math.pow(10, d); 318 return Math.round(value * factor) / factor; 319 } 320 321 function requestBatchPrices(productPaths, onSuccess, onError) { 322 var url = getOrigin() + '/wp-json/mfs/v1/batch-pricing'; 323 324 if (typeof fetch === 'function') { 325 fetch(url, { 326 method: 'POST', 327 headers: { 328 'Content-Type': 'application/json' 329 }, 330 body: JSON.stringify(productPaths) 331 }) 332 .then(function(response) { 333 if (!response.ok) throw new Error('API error: ' + response.status); 334 return response.json(); 335 }) 336 .then(onSuccess) 337 .catch(onError); 338 return; 339 } 340 341 try { 342 var xhr = new XMLHttpRequest(); 343 xhr.open('POST', url, true); 344 xhr.setRequestHeader('Content-Type', 'application/json'); 345 xhr.onreadystatechange = function() { 346 if (xhr.readyState !== 4) return; 347 348 if (xhr.status >= 200 && xhr.status < 300) { 349 try { 350 var parsed = JSON.parse(xhr.responseText); 351 onSuccess(parsed); 352 } catch (e) { 353 onError(e); 354 } 355 } else { 356 onError(new Error('API error: ' + xhr.status)); 357 } 358 }; 359 xhr.send(JSON.stringify(productPaths)); 360 } catch (e) { 361 onError(e); 362 } 363 } 364 365 function setButtonPriceHtml(button, currentFormatted, originalFormatted, productPath) { 366 // Prefer preserving FastSpring's price span so FastSpring/theme scripts don't overwrite our button text later. 367 var originalSpan = button.querySelector('[data-fsc-item-price]'); 368 if (!originalSpan) { 369 originalSpan = document.createElement('span'); 370 if (productPath) { 371 originalSpan.setAttribute('data-fsc-item-path', productPath); 372 } 373 originalSpan.setAttribute('data-fsc-item-price', ''); 374 } 375 376 originalSpan.classList.remove('mfs-price-original'); 377 originalSpan.textContent = currentFormatted; 378 379 if (productPath) { 380 button.setAttribute('data-mfs-product-path', productPath); 381 } 382 383 isMutatingDom = true; 384 clearNode(button); 385 button.appendChild(document.createTextNode('Buy Now ')); 386 button.appendChild(originalSpan); 387 388 if (originalFormatted) { 389 var strikeSpan = document.createElement('span');
390 strikeSpan.className = 'mfs-price-original'; 391 strikeSpan.textContent = originalFormatted; 392 393 // Keep ONLY the bracketed original "($Y)" together; let the rest of the 394 // label wrap freely so a narrow button wraps to fit instead of clipping. 395 button.appendChild(document.createTextNode(' ')); 396 var wasWrap = document.createElement('span'); 397 wasWrap.style.whiteSpace = 'nowrap'; 398 wasWrap.appendChild(document.createTextNode('(')); 399 wasWrap.appendChild(strikeSpan); 400 wasWrap.appendChild(document.createTextNode(')')); 401 button.appendChild(wasWrap); 402 } 403 isMutatingDom = false; 404 } 405 406 function setButtonSaleHtml(button, discountedFormatted, originalFormatted, productPath) { 407 var originalSpan = button.querySelector('[data-fsc-item-price]'); 408 if (!originalSpan) { 409 originalSpan = document.createElement('span'); 410 if (productPath) { 411 originalSpan.setAttribute('data-fsc-item-path', productPath); 412 } 413 originalSpan.setAttribute('data-fsc-item-price', ''); 414 } 415 416 originalSpan.classList.add('mfs-price-original'); 417 // Apply strikethrough inline too: the static storefront pages load this JS 418 // but NOT product-pages.css, so the .mfs-price-original CSS rule never 419 // reaches them. Inline style guarantees the struck-through original price. 420 originalSpan.style.textDecoration = 'line-through'; 421 if (originalFormatted) { 422 originalSpan.textContent = originalFormatted; 423 } 424 425 // Preserve the hidden unitPrice carrier span across the rebuild so FastSpring 426 // (SBL) keeps the discounted value live, e.g. on currency/country change. 427 var unitSpan = button.querySelector('[data-fsc-item-unitprice]'); 428 429 if (productPath) { 430 button.setAttribute('data-mfs-product-path', productPath); 431 } 432 433 isMutatingDom = true; 434 clearNode(button); 435 if (unitSpan) { 436 unitSpan.style.display = 'none'; 437 button.appendChild(unitSpan); 438 } 439 button.appendChild(document.createTextNode('Discounted to ')); 440 441 var currentSpan = document.createElement('span'); 442 currentSpan.className = 'mfs-price-current'; 443 currentSpan.appendChild(document.createTextNode(discountedFormatted)); 444 button.appendChild(currentSpan); 445 446 // Keep ONLY the bracketed original "($Y)" together; let "Discounted to $X" 447 // wrap freely so a narrow button wraps to fit instead of overflowing/clipping. 448 button.appendChild(document.createTextNode(' ')); 449 450 var saleWasWrap = document.createElement('span'); 451 saleWasWrap.style.whiteSpace = 'nowrap'; 452 saleWasWrap.appendChild(document.createTextNode('(')); 453 saleWasWrap.appendChild(originalSpan); 454 saleWasWrap.appendChild(document.createTextNode(')')); 455 button.appendChild(saleWasWrap); 456 isMutatingDom = false; 457 } 458 459 var salePriceByProductPath = {}; 460 var cachedPriceByProductPath = {}; 461 var isApplyingPrices = false; 462 var reapplyTimer = null; 463 var scanTimer = null; 464 var isFetchingPrices = false; 465 var isMutatingDom = false; 466 467 function getButtonProductPath(button) { 468 var productPath = 469 button.getAttribute('data-mfs-product-path') || 470 button.getAttribute('data-product-path') || 471 button.getAttribute('data-fsc-item-path-value'); 472 if (productPath) { 473 return productPath; 474 } 475 476 var span = button.querySelector('[data-fsc-item-path]'); 477 if (span) { 478 return span.getAttribute('data-fsc-item-path'); 479 } 480 481 var onclick = button.getAttribute('onclick') || ''; 482 if (onclick) { 483 var match = onclick.match(/checkout(?:Naada|Geoshred)\(\s*['"]([^'"]+)['"]/); 484 if (match && match[1]) { 485 return match[1]; 486 } 487 } 488 489 return null; 490 } 491 492 function collectBuyButtons() { 493 var result = []; 494 495 var explicitButtons = document.querySelectorAll('[data-product-path], [data-fsc-item-path-value]'); 496 for (var i = 0; i < explicitButtons.length; i++) { 497 var el = explicitButtons[i]; 498 if (el && (el.tagName === 'A' || el.tagName === 'BUTTON')) { 499 addUniqueElement(result, el); 500 } 501 } 502 503 var priceSpans = document.querySelectorAll('span[data-fsc-item-price]'); 504 for (var j = 0; j < priceSpans.length; j++) { 505 var maybeButton = closestElement(priceSpans[j], 'a,button'); 506 if (maybeButton) { 507 addUniqueElement(result, maybeButton); 508 } 509 } 510 511 var pathSpans = document.querySelectorAll('span[data-fsc-item-path]'); 512 for (var k = 0; k < pathSpans.length; k++) { 513 var maybeButton2 = closestElement(pathSpans[k], 'a,button'); 514 if (maybeButton2) { 515 addUniqueElement(result, maybeButton2); 516 } 517 } 518 519 var fsBuyLinks = document.querySelectorAll('a.fs-buy'); 520 for (var m = 0; m < fsBuyLinks.length; m++) { 521 addUniqueElement(result, fsBuyLinks[m]); 522 } 523 524 return result; 525 } 526
527 function applySalePricesToButtons() { 528 if (isApplyingPrices) { 529 return; 530 } 531 532 isApplyingPrices = true; 533 isMutatingDom = true; 534 try { 535 var buttons = collectBuyButtons(); 536 for (var i = 0; i < buttons.length; i++) { 537 var button = buttons[i]; 538 var productPath = getButtonProductPath(button); 539 if (!productPath || !salePriceByProductPath[productPath]) { 540 continue; 541 } 542 543 var info = salePriceByProductPath[productPath]; 544 var currentNode = button.querySelector('.mfs-price-current'); 545 var originalNode = button.querySelector('.mfs-price-original'); 546 547 if ( 548 currentNode && 549 originalNode && 550 currentNode.textContent === info.discountedFormatted && 551 originalNode.textContent === info.originalFormatted 552 ) { 553 continue; 554 } 555 556 setButtonSaleHtml(button, info.discountedFormatted, info.originalFormatted, productPath); 557 button.setAttribute('data-mfs-sale-applied', '1'); 558 } 559 } finally { 560 isApplyingPrices = false; 561 isMutatingDom = false; 562 } 563 } 564 565 function scheduleReapplySalePrices(delayMs) { 566 if (reapplyTimer) { 567 clearTimeout(reapplyTimer); 568 } 569 reapplyTimer = setTimeout(function() { 570 reapplyTimer = null; 571 applySalePricesToButtons(); 572 }, delayMs || 50); 573 } 574 575 function scheduleScanForButtons(delayMs) { 576 if (scanTimer) { 577 clearTimeout(scanTimer); 578 } 579 scanTimer = setTimeout(function() { 580 scanTimer = null; 581 updateButtonPrices(); 582 }, delayMs || 100); 583 } 584 585 function getActiveDiscountPercent() { 586 var storefront = getStorefront(); 587 var naadaDiscountPercent = getNaadaDiscountPercent(); 588 var geoshredDiscountPercent = getGeoshredDiscountPercent(); 589 590 if (storefront === 'naada' && naadaDiscountPercent > 0) { 591 return naadaDiscountPercent; 592 } 593 if (storefront === 'geoshred' && geoshredDiscountPercent > 0) { 594 return geoshredDiscountPercent; 595 } 596 return 0; 597 } 598 599 function applyDiscountFromFscPriceSpan(priceSpan) { 600 if (!priceSpan) { 601 return; 602 } 603 604 var originalFormatted = String(priceSpan.textContent || '').trim(); 605 if (!originalFormatted) { 606 return; 607 } 608 609 var button = closestElement(priceSpan, 'a,button'); 610 if (!button) { 611 return; 612 } 613 614 var productPath = priceSpan.getAttribute('data-fsc-item-path') || getButtonProductPath(button); 615 if (!productPath) { 616 return; 617 } 618 619 // The discounted price comes straight from FastSpring: SBL populates the 620 // sibling data-fsc-item-unitPrice span (localized, already reflecting any 621 // active product discount). No client-side percentage math. 622 var unitSpan = button.querySelector('[data-fsc-item-unitprice]'); 623 var discountedFormatted = unitSpan ? String(unitSpan.textContent || '').trim() : ''; 624 625 // Wait until SBL has populated the discounted value. 626 if (!discountedFormatted) { 627 return; 628 } 629 630 // No active discount (unit price equals list price): leave the button's 631 // natural "Buy <price>" render untouched. 632 if (discountedFormatted === originalFormatted) { 633 return; 634 } 635 636 var alreadyCurrent = button.querySelector('.mfs-price-current'); 637 var alreadyOriginal = button.querySelector('.mfs-price-original'); 638 if ( 639 alreadyCurrent && 640 alreadyOriginal && 641 alreadyCurrent.textContent === discountedFormatted && 642 alreadyOriginal.textContent === originalFormatted 643 ) { 644 return; 645 } 646 647 salePriceByProductPath[productPath] = { 648 discountedFormatted: discountedFormatted, 649 originalFormatted: originalFormatted 650 }; 651 652 setButtonSaleHtml(button, discountedFormatted, originalFormatted, productPath); 653 } 654 655 function setupDiscountFromFscPriceSpans() { 656 var discountScanTimer = null; 657 658 function scanAndApply() { 659 if (isMutatingDom) { 660 return; 661 } 662 var spans = document.querySelectorAll('span[data-fsc-item-price]'); 663 for (var i = 0; i < spans.length; i++) { 664 applyDiscountFromFscPriceSpan(spans[i]); 665 } 666 } 667 668 function scheduleScan(delayMs) { 669 if (discountScanTimer) { 670 clearTimeout(discountScanTimer); 671 } 672 discountScanTimer = setTimeout(function() { 673 discountScanTimer = null; 674 scanAndApply(); 675 }, delayMs || 50); 676 } 677 678 // Initial scan, then a couple early retries for slow FastSpring injection. 679 scanAndApply(); 680 scheduleScan(100); 681 scheduleScan(400); 682 scheduleScan(1200); 683 684 if (typeof MutationObserver === 'undefined') { 685 return; 686 } 687 688 try { 689 var observer = new MutationObserver(function() { 690 if (isMutatingDom) { 691 return; 692 } 693 scheduleScan(100); 694 }); 695 696 observer.observe(document.body, { 697 childList: true, 698 subtree: true, 699 characterData: false 700 }); 701 } catch (e) { 702 console.error('MFS Pricing: Failed to start discount observer:', e); 703 } 704 } 705 706 function setupSaleReapplyObserver() { 707 if (typeof MutationObserver === 'undefined') { 708 return; 709 } 710 711 try { 712 var observer = new MutationObserver(function() { 713 if (isApplyingPrices || isMutatingDom) { 714 return; 715 }
716 scheduleReapplySalePrices(50); 717 }); 718 719 observer.observe(document.body, { 720 childList: true, 721 subtree: true, 722 characterData: false 723 }); 724 } catch (e) { 725 console.error('MFS Pricing: Failed to start MutationObserver:', e); 726 } 727 } 728 729 function updateButtonPrices() { 730 try { 731 if (isFetchingPrices) { 732 return; 733 } 734 735 var buttons = collectBuyButtons(); 736 console.log('MFS Pricing: Found ' + buttons.length + ' buttons'); 737 738 if (buttons.length === 0) return; 739 740 var storefront = getStorefront(); 741 var naadaDiscountPercent = getNaadaDiscountPercent(); 742 var geoshredDiscountPercent = getGeoshredDiscountPercent(); 743 744 var activeDiscountPercent = 0; 745 if (storefront === 'naada' && naadaDiscountPercent > 0) { 746 activeDiscountPercent = naadaDiscountPercent; 747 } else if (storefront === 'geoshred' && geoshredDiscountPercent > 0) { 748 activeDiscountPercent = geoshredDiscountPercent; 749 } 750 751 var useStorefrontDiscount = activeDiscountPercent > 0; 752 753 // Collect all unique product paths 754 var productPaths = []; 755 var buttonsByProduct = {}; 756 757 for (var i = 0; i < buttons.length; i++) { 758 var button = buttons[i]; 759 var productPath = getButtonProductPath(button); 760 if (!productPath) { 761 continue; 762 } 763 if (productPath && productPaths.indexOf(productPath) === -1) { 764 productPaths.push(productPath); 765 } 766 if (!buttonsByProduct[productPath]) { 767 buttonsByProduct[productPath] = []; 768 } 769 buttonsByProduct[productPath].push(button); 770 } 771 772 var productPathsToFetch = []; 773 for (var p = 0; p < productPaths.length; p++) { 774 var pp = productPaths[p]; 775 if (!cachedPriceByProductPath[pp]) { 776 productPathsToFetch.push(pp); 777 } 778 } 779 780 function renderButtons() { 781 for (var productPath in buttonsByProduct) { 782 var data = cachedPriceByProductPath[productPath]; 783 784 if (!buttonsByProduct[productPath]) { 785 continue; 786 } 787 788 if (!data || !data.currency || data.price === undefined || data.price === null) { 789 buttonsByProduct[productPath].forEach(function(button) { 790 setButtonPriceHtml(button, '???', null, productPath); 791 }); 792 continue; 793 } 794 795 var currency = data.currency; 796 var basePrice = parseFloat(data.price); 797 if (isNaN(basePrice)) { 798 buttonsByProduct[productPath].forEach(function(button) { 799 setButtonPriceHtml(button, '???', null, productPath); 800 }); 801 continue; 802 } 803 804 var originalFormatted = data.formatted || formatCurrency(basePrice, currency); 805 806 if (useStorefrontDiscount) { 807 var discountMultiplier = 1 - (activeDiscountPercent / 100); 808 var discountedPrice = roundCurrency(basePrice * discountMultiplier, currency); 809 var discountedFormatted = formatCurrency(discountedPrice, currency); 810 811 salePriceByProductPath[productPath] = { 812 discountedFormatted: discountedFormatted, 813 originalFormatted: originalFormatted 814 }; 815 816 buttonsByProduct[productPath].forEach(function(button) { 817 setButtonSaleHtml(button, discountedFormatted, originalFormatted, productPath); 818 }); 819 } else { 820 var currentFormatted = data.formatted || formatCurrency(basePrice, currency); 821 buttonsByProduct[productPath].forEach(function(button) { 822 setButtonPriceHtml(button, currentFormatted, null, productPath); 823 }); 824 } 825 } 826 827 if (useStorefrontDiscount) { 828 applySalePricesToButtons(); 829 scheduleReapplySalePrices(250); 830 scheduleReapplySalePrices(1000); 831 scheduleReapplySalePrices(3000); 832 } 833 } 834 835 // Batch fetch all prices in one API call 836 if (productPathsToFetch.length === 0) { 837 renderButtons(); 838 return; 839 } 840 841 isFetchingPrices = true;
842 requestBatchPrices(productPathsToFetch, function(allPrices) { 843 console.log('MFS Pricing: Received batch prices:', allPrices); 844 for (var key in allPrices) { 845 cachedPriceByProductPath[key] = allPrices[key]; 846 } 847 isFetchingPrices = false; 848 renderButtons(); 849 }, function(error) { 850 isFetchingPrices = false; 851 console.error('MFS Pricing: Failed to load batch prices:', error); 852 }); 853 } catch (e) { 854 isFetchingPrices = false; 855 console.error('MFS Pricing: Exception in updateButtonPrices:', e); 856 console.error('MFS Pricing: Exception details:', e.message, e.stack); 857 } 858 } 859 860 function setupPopupCheckout() { 861 console.log('MFS Pricing: Setting up popup checkout'); 862 863 document.addEventListener('click', function(e) { 864 var link = closestElement(e.target, 'a[data-fsc-action*="Checkout"]'); 865 if (link) { 866 var productPath = link.getAttribute('data-fsc-item-path-value'); 867 if (!productPath) { 868 var productSpan = link.querySelector('[data-fsc-item-path]'); 869 if (productSpan) { 870 productPath = productSpan.getAttribute('data-fsc-item-path'); 871 } 872 } 873 874 if (typeof window.mfsTrackEvent === 'function') { 875 window.mfsTrackEvent('click_buy', { 876 product_path: productPath || '', 877 sku: productPath || '' 878 }); 879 } 880 881 console.log('MFS Pricing: Pre-tagging checkout for:', productPath); 882 883 window._mfsCheckoutOpen = true; 884 885 if (typeof fastspring !== 'undefined' && fastspring.builder) { 886 if (typeof window.getMoForteTags === 'function') { 887 var tags = window.getMoForteTags(productPath); 888 console.log('MFS Pricing: Setting tags before FastSpring processes click:', tags); 889 fastspring.builder.tag(tags); 890 } else { 891 console.warn('MFS Pricing: getMoForteTags not available'); 892 } 893 894 try { 895 var promoParam = new URLSearchParams(window.location.search).get('promo'); 896 if (promoParam) { 897 console.log('MFS Pricing: Applying promo code from URL:', promoParam); 898 fastspring.builder.promo(promoParam); 899 } 900 } catch (promoErr) { 901 console.error('MFS Pricing: Failed to apply promo:', promoErr); 902 } 903 } else { 904 console.error('MFS Pricing: FastSpring builder not available'); 905 } 906 } 907 }, true); 908 } 909 910 function init() { 911 console.log('MFS Pricing: Initializing...'); 912 setupPopupCheckout(); 913 914 // Discounts are driven entirely by FastSpring product discounts: SBL fills 915 // each button's data-fsc-item-price (original) and data-fsc-item-unitPrice 916 // (discounted) spans, already localized for the visitor's country. We render 917 // "Discounted to X (Y)" from those values â no client-side percentage math, 918 // and it auto-reverts to the plain price when a sale's dates lapse. 919 setupDiscountFromFscPriceSpans(); 920 setupSaleReapplyObserver(); 921 if (document.readyState !== 'complete') { 922 window.addEventListener('load', function() { 923 scheduleReapplySalePrices(50); 924 }); 925 } else { 926 scheduleReapplySalePrices(50); 927 } 928 929 // Fallback: pages without FastSpring price spans use the server-side API. 930 if (document.querySelectorAll('span[data-fsc-item-price]').length === 0) { 931 updateButtonPrices(); 932 scheduleScanForButtons(250); 933 } 934 } 935 936 if (document.readyState === 'loading') { 937 document.addEventListener('DOMContentLoaded', init); 938 } else { 939 init(); 940 } 941 942 // Custom popup closed handler - polls for order completion then reloads 943 window.onFSPopupClosed = function(orderReference) { 944 console.log('MFS: onFSPopupClosed called with:', orderReference); 945 window._mfsCheckoutOpen = false; 946 if (!orderReference || !orderReference.id) { 947 var abandonedProduct = window._mfsLastCheckoutProduct || ''; 948 console.log('MFS: Checkout abandoned for product:', abandonedProduct); 949 if (typeof window.mfsTrackEvent === 'function') { 950 window.mfsTrackEvent('checkout_abandon', { 951 product_path: abandonedProduct, 952 sku: abandonedProduct 953 }); 954 } 955 return; 956 } 957 if (orderReference && orderReference.id) { 958 var orderRef = orderReference.reference || orderReference.id; 959 console.log('MFS: Order completed:', orderReference.id, 'reference:', orderRef); 960 961 var pollAttempts = 0; 962 var maxAttempts = 10; 963 var pollInterval = 1000; 964 965 function pollForOrder() { 966 pollAttempts++; 967 console.log('MFS: Polling for order (attempt ' + pollAttempts + '/' + maxAttempts + ')'); 968 969 var url = getOrigin() + '/wp-json/mfs/v1/order-details/' + encodeURIComponent(orderRef); 970 971 var xhr = new XMLHttpRequest(); 972 xhr.open('GET', url, true); 973 xhr.onreadystatechange = function() { 974 if (xhr.readyState !== 4) return; 975 976 if (xhr.status >= 200 && xhr.status < 300) { 977 try { 978 var data = JSON.parse(xhr.responseText); 979 console.log('MFS: Order details received:', data); 980 if (data.success && data.sku) { 981 if (typeof window.mfsTrackEvent === 'function') {
982 // GA4 ecommerce-compliant purchase: transaction_id (GA4's 983 // dedup key) + an items[] array so the order populates GA4's 984 // Monetization/revenue reports, not just a custom event. 985 // product_path/sku/order_id stay for the existing tagging. 986 var purchaseItem = { item_id: data.sku, item_name: data.sku, quantity: 1 }; 987 var purchasePayload = { 988 transaction_id: orderRef || orderReference.id, 989 product_path: data.sku, 990 sku: data.sku, 991 order_id: orderReference.id, 992 items: [purchaseItem] 993 }; 994 if (typeof data.value === 'number' && !isNaN(data.value)) { 995 purchasePayload.value = data.value; 996 purchaseItem.price = data.value; 997 } 998 if (data.currency) { 999 purchasePayload.currency = data.currency; 1000 } 1001 window.mfsTrackEvent('purchase', purchasePayload); 1002 } 1003 console.log('MFS: Order found! Reloading to show purchase...'); 1004 window.location.reload(); 1005 return; 1006 } 1007 } catch (e) { 1008 console.error('MFS: Failed to parse order details:', e); 1009 } 1010 } else { 1011 console.error('MFS: Order details response status:', xhr.status); 1012 } 1013 1014 if (pollAttempts < maxAttempts) { 1015 console.log('MFS: Order not ready yet, retrying in ' + pollInterval + 'ms...'); 1016 setTimeout(pollForOrder, pollInterval); 1017 } else { 1018 console.log('MFS: Polling timeout reached, reloading anyway...'); 1019 window.location.reload(); 1020 } 1021 }; 1022 xhr.send(); 1023 } 1024 1025 pollForOrder(); 1026 } 1027 }; 1028 1029 function updateButtonToPurchased(sku, invoiceUrl) { 1030 console.log('MFS: Updating button for SKU:', sku, 'with invoice:', invoiceUrl); 1031 1032 var buttons = document.querySelectorAll('a.fs-buy[data-fsc-item-path-value="' + sku + '"]'); 1033 console.log('MFS: Found', buttons.length, 'buttons to update'); 1034 1035 for (var i = 0; i < buttons.length; i++) { 1036 var button = buttons[i]; 1037 button.classList.remove('fs-buy'); 1038 button.removeAttribute('data-fsc-action'); 1039 button.removeAttribute('data-fsc-item-path-value'); 1040 1041 button.classList.add('mfs-purchased-button'); 1042 1043 while (button.firstChild) { 1044 button.removeChild(button.firstChild); 1045 } 1046 button.appendChild(document.createTextNode('Purchased')); 1047 1048 if (invoiceUrl) { 1049 button.setAttribute('href', invoiceUrl); 1050 button.setAttribute('target', '_blank'); 1051 button.setAttribute('title', 'Click to view your invoice'); 1052 1053 button.addEventListener('click', function(e) { 1054 e.preventDefault(); 1055 e.stopPropagation(); 1056 e.stopImmediatePropagation(); 1057 window.open(invoiceUrl, '_blank'); 1058 }); 1059 } else { 1060 button.setAttribute('href', '#'); 1061 button.addEventListener('click', function(e) { 1062 e.preventDefault(); 1063 e.stopPropagation(); 1064 alert('You own this product! Invoice URL not available.'); 1065 }); 1066 } 1067 1068 console.log('MFS: Button updated for', sku); 1069 } 1070 } 1071})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.