1/** 2 * Mailchimp Pixel Tracking for WooCommerce 3 * 4 * Listens for WooCommerce events and sends them to the Mailchimp Pixel SDK. 5 * Only tracks if window.$mcSite.pixel.api is available (MC.js loaded). 6 * 7 * @package MailChimp_WooCommerce 8 * @since 1.0.0 9 */ 10 11(function ($) { 12 'use strict'; 13 14 /** 15 * Default config for waitForPixelSDK exponential backoff 16 */ 17 const PIXEL_SDK_WAIT_CONFIG = { 18 initialDelayMs: 100, 19 maxDelayMs: 5000, 20 maxAttempts: 20 21 }; 22 23 /** 24 * How long PRODUCT_VIEWED waits on a variable product for WooCommerce to resolve a 25 * default or deep-linked variation before settling for the parent. 26 */ 27 const VARIATION_RESOLVE_WAIT_MS = 1200; 28 29 /** 30 * Settle time after a form interaction before re-reading the variation id input. 31 * The blocks form writes it through the Interactivity API on the next render. 32 */ 33 const VARIATION_INPUT_SETTLE_MS = 150; 34 35 /** 36 * Mailchimp Pixel Tracking Handler 37 */ 38 const MailchimpPixelTracking = { 39 // Debounce timers for cart fetch calls 40 _atcTimer: null, 41 _rfcTimer: null, 42 _atcFetching: false, 43 _rfcFetching: false, 44 // PRODUCT_VIEWED state for a variable product being resolved to a variation 45 _viewTimer: null, 46 _viewedParent: null, 47 _viewedKeys: null, 48 _inputTimer: null, 49 50 /** 51 * Initialize tracking. 52 * Waits for Pixel SDK with exponential backoff, then sends page events and attaches handlers. 53 */ 54 init: function () { 55 const self = this; 56 57 // Registered before the SDK wait on purpose: that wait runs for seconds, and an 58 // integration that resolves its variation early must not race us. 59 window.mcPixel = window.mcPixel || {}; 60 window.mcPixel.setVariation = function (variationId, product) { 61 self.setVariation(variationId, product); 62 }; 63 document.addEventListener('mailchimp_woocommerce_set_variation', function (event) { 64 const detail = event.detail || {}; 65 self.setVariation(detail.variationId, detail.product); 66 }); 67 68 this.waitForPixelSDK(PIXEL_SDK_WAIT_CONFIG) 69 .then(function () { 70 self.sendPageEvents(); 71 self.attachCartEventListeners(); 72 self.interceptStoreApiRequests(); 73 console.log('Mailchimp Pixel SDK loaded.'); 74 }) 75 .catch(function (e) { 76 console.log('Mailchimp Pixel SDK not loaded within timeout. Tracking disabled.', e); 77 }); 78 }, 79 80 /** 81 * Wait for Pixel SDK to become available using exponential backoff. 82 * 83 * @param {Object} options Optional config 84 * @param {number} options.initialDelayMs First delay in ms (default 100) 85 * @param {number} options.maxDelayMs Cap on delay in ms (default 5000) 86 * @param {number} options.maxAttempts Max number of attempts (default 20) 87 * @return {Promise<void>} Resolves when SDK is available, rejects after maxAttempts 88 */ 89 waitForPixelSDK: function (options) { 90 const config = options || {}; 91 const initialDelayMs = config.initialDelayMs !== undefined ? config.initialDelayMs : PIXEL_SDK_WAIT_CONFIG.initialDelayMs; 92 const maxDelayMs = config.maxDelayMs !== undefined ? config.maxDelayMs : PIXEL_SDK_WAIT_CONFIG.maxDelayMs; 93 const maxAttempts = config.maxAttempts !== undefined ? config.maxAttempts : PIXEL_SDK_WAIT_CONFIG.maxAttempts; 94 95 function isSDKReady() { 96 return typeof window.$mcSite !== 'undefined' && 97 window.$mcSite.pixel && 98 typeof window.$mcSite.pixel.api !== 'undefined' && 99 typeof window.$mcSite.pixel.api.track === 'function' && 100 window.$mcSite.pixel.installed === true; 101 } 102 103 return new Promise(function (resolve, reject) { 104 let attempt = 0; 105 106 function scheduleCheck() { 107 if (isSDKReady()) { 108 // FIXME: temporary wait to accommodate for not being able to detect ready state 109 console.warn('Pixel SDK - remediation for pixel ready issue') 110 setTimeout(function () { 111 resolve(); 112 }, 1000); 113 return; 114 } 115 if (attempt >= maxAttempts) { 116 reject(new Error('Pixel SDK not available')); 117 return; 118 } 119 120 const delay = Math.min( 121 initialDelayMs * Math.pow(2, attempt), 122 maxDelayMs 123 ); 124 attempt += 1; 125 126 setTimeout(scheduleCheck, delay); 127 } 128 129 scheduleCheck(); 130 }); 131 }, 132 133 /** 134 * Synchronous check if Pixel SDK is available (e.g. before each track call). 135 * 136 * @return {boolean} True if SDK is available 137 */ 138 isPixelSDKReady: function () { 139 return typeof window.$mcSite !== 'undefined' && 140 window.$mcSite.pixel && 141 typeof window.$mcSite.pixel.api !== 'undefined' && 142 typeof window.$mcSite.pixel.api.track === 'function'; 143 }, 144 145 /** 146 * Get cart ID from window.mcPixel 147 * 148 * @return {string} Cart ID 149 */ 150 getCartId: function () { 151 return window.mcPixel && window.mcPixel.cartId ? window.mcPixel.cartId : ''; 152 }, 153 154 /** 155 * Get the REST API base URL from the localized config. 156 * 157 * @return {string} REST base URL 158 */ 159 getRestBase: function () { 160 return (window.mcPixelConfig && window.mcPixelConfig.restBase) || '/wp-json/mailchimp-for-woocommerce/v1/'; 161 }, 162 163 /** 164 * Send page-level events based on pre-populated data 165 */ 166 sendPageEvents: function () { 167 if (!window.mcPixel || !window.mcPixel.data) { 168 return; 169 } 170 171 const data = window.mcPixel.data; 172 const events = data.events || []; 173 174 // Send events based on what was set by PHP
175 events.forEach((eventType) => { 176 //console.log('Mailchimp Pixel: Sending event', eventType, data); 177 switch (eventType) { 178 case 'PRODUCT_ADDED_TO_CART': 179 if (data.added_to_cart) { 180 var atcItems = Array.isArray(data.added_to_cart) ? data.added_to_cart : [data.added_to_cart]; 181 for (var ai = 0; ai < atcItems.length; ai++) { 182 this.sendProductAddedToCart(atcItems[ai]); 183 } 184 } 185 break; 186 case 'PRODUCT_REMOVED_FROM_CART': 187 if (data.removed_from_cart) { 188 var rfcItems = Array.isArray(data.removed_from_cart) ? data.removed_from_cart : [data.removed_from_cart]; 189 for (var ri = 0; ri < rfcItems.length; ri++) { 190 this.sendProductRemovedFromCart(rfcItems[ri]); 191 } 192 } 193 break; 194 case 'IDENTITY': 195 if (data.identity && data.identity.email) { 196 this.sendIdentityEvent(data.identity.email); 197 } 198 break; 199 case 'PRODUCT_VIEWED': 200 // Skip if an add-to-cart or remove-from-cart already fired for the same product. 201 // The cart action implies the view, so firing both is redundant. 202 if (data.product) { 203 const viewedId = String(data.product.productId || data.product.id); 204 const atcArr = Array.isArray(data.added_to_cart) ? data.added_to_cart : (data.added_to_cart ? [data.added_to_cart] : []); 205 const rfcArr = Array.isArray(data.removed_from_cart) ? data.removed_from_cart : (data.removed_from_cart ? [data.removed_from_cart] : []); 206 const atcMatch = atcArr.some(function (p) { return String(p.productId || p.id
206) === viewedId; }); 207 const rfcMatch = rfcArr.some(function (p) { return String(p.productId || p.id) === viewedId; }); 208 209 if ( 210 (events.includes('PRODUCT_ADDED_TO_CART') && atcMatch) || 211 (events.includes('PRODUCT_REMOVED_FROM_CART') && rfcMatch) 212 ) { 213 //console.log('Mailchimp Pixel: Skipping PRODUCT_VIEWED (superseded by cart event for same product)'); 214 break; 215 } 216 this.trackProductViewed(data.product); 217 } 218 break; 219 case 'CART_VIEWED': 220 if (data.cart) { 221 this.sendCartViewed(data.cart); 222 } 223 break; 224 case 'CHECKOUT_STARTED': 225 if (data.checkout) { 226 this.sendCheckoutStarted(data.checkout); 227 window.mcPixel._handled.checkout = true; 228 } 229 break; 230 case 'PURCHASED': 231 if (data.order) { 232 this.sendPurchased(data.order); 233 } 234 break; 235 case 'PRODUCT_CATEGORY_VIEWED': 236 if (data.category) { 237 this.sendCategoryViewed(data.category); 238 window.mcPixel._handled.category = true; 239 } 240 break; 241 case 'SEARCH_SUBMITTED': 242 if (data.search) { 243 this.sendSearchSubmitted(data.search); 244 window.mcPixel._handled.search = true; 245 } 246 break; 247 } 248 }); 249 }, 250 251 /** 252 * PRODUCT_VIEWED for the single product page. 253 * 254 * A variable product renders before the customer has picked anything, so the 255 * server-side payload only knows the parent. WooCommerce announces the chosen 256 * variation on the .variations_form with found_variation, so we hold the event 257 * until that fires - or until it's clear nothing is going to resolve. 258 * 259 * @param {Object} product Parent product data from PHP 260 */ 261 trackProductViewed: function (product) { 262 const self = this; 263 this._viewedKeys = this._viewedKeys || {}; 264 265 // not a variable product - the server already knew everything there is to know 266 if (!window.mcPixel.data.product_variations) { 267 this.sendProductViewedOnce(product, product.id); 268 return; 269 } 270 271 this._viewedParent = product; 272 this.attachVariationListeners(); 273 274 const resolved = this.currentVariationId(); 275 if (resolved) { 276 this.sendProductViewedOnce(this.resolveVariationPayload({ variation_id: resolved }), resolved); 277 return; 278 } 279 280 // nothing picked yet, but a default variation or ?attribute_* deep link still 281 // resolves a tick after the form initialises - give WooCommerce that tick 282 // before settling for the parent 283 this._viewTimer = setTimeout(function () { 284 self.sendProductViewedOnce(product, product.id); 285 }, VARIATION_RESOLVE_WAIT_MS); 286 }, 287 288 /** 289 * One PRODUCT_VIEWED per thing actually viewed: the parent, then each variation 290 * the customer lands on. Switching back to an earlier pick sends nothing, so 291 * clicking through a dropdown doesn't turn into a burst of events. 292 * 293 * @param {Object} product Product data to send 294 * @param {string} key Dedup key - the variation id, or the parent id 295 */ 296 sendProductViewedOnce: function (product, key) { 297 clearTimeout(this._viewTimer); 298 if (this._viewedKeys[key]) { 299 return; 300 } 301 // only claim the key once it actually went out - an integration can call 302 // setVariation() before the SDK is up, and that must not count as sent 303 if (this.sendProductViewed(product)) { 304 this._viewedKeys[key] = true; 305 } 306 }, 307 308 /** 309 * Public API for themes, swatch plugins and custom templates. 310 * 311 * Detection covers what WooCommerce itself ships: the classic variations form, 312 * and the hidden variation_id input the block form maintains. Anything else - a 313 * custom swatch UI, a page builder widget, a headless front end - can announce 314 * the selection itself, either directly: 315 * 316 * window.mcPixel.setVariation( 123 ); 317 * 318 * or, when it cannot reach our global, by dispatching an event: 319 * 320 * document.dispatchEvent( new CustomEvent( 321 * 'mailchimp_woocommerce_set_variation', 322 * { detail: { variationId: 123 } } 323 * ) ); 324 * 325 * Pass a second argument to supply the whole product payload instead of letting 326 * us resolve it. Sending the same variation twice is a no-op either way. 327 * 328 * @param {number|string} variationId Variation being viewed 329 * @param {Object} [product] Optional product payload, sent as-is 330 */ 331 setVariation: function (variationId, product) { 332 const id = String(variationId || ''); 333 334 if (!id || id === '0') { 335 return; 336 } 337 // an integration may beat PRODUCT_VIEWED to it - nothing to dedup against yet 338 this._viewedKeys = this._viewedKeys || {}; 339 this.sendProductViewedOnce( 340 product || this.resolveVariationPayload({ variation_id: id }), 341 id 342 ); 343 }, 344 345 /** 346 * found_variation is WooCommerce's own signal that every attribute is chosen and 347 * a variation matched. Bound delegated off body so a form rendered late - blocks, 348 * lazy themes, quick-view modals - is still covered. 349 */ 350 attachVariationListeners: function () { 351 const self = this; 352 353 $(document.body).on('found_variation', '.variations_form', function (event, variation) { 354 if (!variation || !variation.variation_id) { 355 return; 356 } 357 self.sendProductViewedOnce( 358 self.resolveVariationPayload(variation), 359 String(variation.variation_id) 360 ); 361 }); 362 363 // covers the block form, which fires no event at all 364 this.watchVariationInput(); 365 }, 366 367 /** 368 * WooCommerce often resolves a variation before we boot - the SDK wait runs over a 369 * second, and the form fires found_variation during its own init. The form parks
370 * the answer in its variation_id input, so read that rather than assuming we were 371 * listening at the right moment. 372 * 373 * @return {string} Resolved variation id, or '' when nothing is selected 374 */ 375 currentVariationId: function () { 376 // Classic puts this in .variations_form, blocks puts it in .single_variation_wrap 377 // inside its own form element - so key off the input, not either wrapper. 378 const inputs = document.querySelectorAll('form input[name="variation_id"], form input.variation_id'); 379 380 for (let i = 0; i < inputs.length; i++) { 381 const id = parseInt(inputs[i].value, 10); 382 if (id > 0) { 383 return String(id); 384 } 385 } 386 387 return ''; 388 }, 389 390 /** 391 * Catch variation changes on templates that announce nothing. 392 * 393 * The blocks "Add to Cart with Options" form runs on the Interactivity API and 394 * fires no jQuery event and no CustomEvent, so found_variation never arrives. 395 * What it does keep is the hidden variation_id input - WooCommerce renders it 396 * specifically so "extensions or Express Payment methods" can read the form 397 * state - so re-read that whenever the customer touches the form. Its value is 398 * set as a property by the Interactivity API, which a MutationObserver would 399 * never see, hence driving off the interaction instead. 400 */ 401 watchVariationInput: function () { 402 const self = this; 403 404 const recheck = function (event) { 405 // blocks renders its attribute options as buttons, so change alone misses them 406 if (!event.target || !event.target.closest || !event.target.closest('form')) { 407 return; 408 } 409 clearTimeout(self._inputTimer); 410 self._inputTimer = setTimeout(function () { 411 const id = self.currentVariationId(); 412 if (id) { 413 self.sendProductViewedOnce(self.resolveVariationPayload({ variation_id: id }), id); 414 } 415 }, VARIATION_INPUT_SETTLE_MS); 416 }; 417 418 document.addEventListener('change', recheck, true); 419 document.addEventListener('click', recheck, true); 420 }, 421 422 /** 423 * Turn a resolved variation into the product shape the pixel expects. 424 * 425 * Prefer the server-rendered variation: it carries the sku, categories and 426 * parent-derived permalink that the event payload has no idea about, and it 427 * matches what add-to-cart sends for the same variation. Past the pre-render cap 428 * there is nothing to look up, so patch the parent with what the event does give 429 * us - the id being right matters more than the rest. 430 * 431 * @param {Object} variation found_variation payload (or just {variation_id}) 432 * @return {Object} Product data for the pixel 433 */ 434 resolveVariationPayload: function (variation) { 435 const parent = this._viewedParent || {}; 436 const id = String(variation.variation_id); 437 const rendered = window.mcPixel.data.product_variations[id]; 438 439 if (rendered) { 440 return rendered; 441 } 442 443 const patched = $.extend({}, parent, { 444 id: id, 445 productId: parent.productId || parent.id || id 446 }); 447 if (variation.sku) { 448 patched.sku = variation.sku; 449 } 450 if (variation.display_price !== undefined && variation.display_price !== null) { 451 patched.price = parseFloat(variation.display_price); 452 } 453 if (variation.image && variation.image.full_src) { 454 patched.imageUrl = variation.image.full_src; 455 } 456 457 return patched; 458 }, 459 460 /** 461 * Send PRODUCT_VIEWED event 462 * 463 * @param {Object} product Product data 464 */ 465 sendProductViewed: function (product) { 466 if (!this.isPixelSDKReady()) return false; 467 468 window.$mcSite.pixel.api.track('PRODUCT_VIEWED', { 469 product: product 470 }).catch((error) => { 471 console.error('Mailchimp Pixel: Error tracking PRODUCT_VIEWED', error); 472 }); 473 474 return true; 475 }, 476 477 /** 478 * Send PRODUCT_ADDED_TO_CART event 479 * 480 * @param {Object} product Product data 481 */ 482 sendProductAddedToCart: function (product) { 483 if (!this.isPixelSDKReady()) return; 484 485 const cartId = this.getCartId(); 486 const eventData = { 487 cartId: cartId, 488 product: { 489 item: { 490 id: product.id, 491 productId: product.productId, 492 title: product.title, 493 price: product.price, 494 currency: product.currency, 495 sku: product.sku || '' 496 }, 497 quantity: product.quantity || 1, 498 price: product.price * (product.quantity || 1), 499 currency: product.currency 500 } 501 }; 502 503 window.$mcSite.pixel.api.track('PRODUCT_ADDED_TO_CART', eventData).catch((error) => { 504 console.error('Mailchimp Pixel: Error tracking PRODUCT_ADDED_TO_CART', error); 505 }); 506 }, 507 508 /** 509 * Send PRODUCT_REMOVED_FROM_CART event 510 * 511 * @param {Object} product Product data 512 */ 513 sendProductRemovedFromCart: function (product) { 514 if (!this.isPixelSDKReady()) return; 515 516 const cartId = this.getCartId(); 517 const eventData = { 518 cartId: cartId, 519 product: { 520 item: { 521 id: product.id, 522 productId: product.productId, 523 title: product.title, 524 price: product.price, 525 currency: product.currency, 526 sku: product.sku || '' 527 }, 528 quantity: product.quantity || 1, 529 price: product.price * (product.quantity || 1), 530 currency: product.currency 531 } 532 }; 533 534 window.$mcSite.pixel.api.track('PRODUCT_REMOVED_FROM_CART', eventData).catch((error) => { 535 console.error('Mailchimp Pixel: Error tracking PRODUCT_REMOVED_FROM_CART', error); 536 }); 537 }, 538 539 /** 540 * Send CART_VIEWED event 541 * 542 * @param {Object} cart Cart data 543 */ 544 sendCartViewed: function (cart) { 545 if (!this.isPixelSDKReady()) return; 546 547 window.$mcSite.pixel.api.track('CART_VIEWED', { 548 cart: cart 549 }).catch((error) => { 550 console.error('Mailchimp Pixel: Error tracking CART_VIEWED', error); 551 }); 552 }, 553 554 /** 555 * Send CHECKOUT_STARTED event 556 * 557 * @param {Object} checkout Checkout data 558 */ 559 sendCheckoutStarted: function (checkout) { 560 if (!this.isPixelSDKReady()) return; 561 562 window.$mcSite.pixel.api.track('CHECKOUT_STARTED', { 563 checkout: checkout 564 }).catch((error) => { 565 console.error('Mailchimp Pixel: Error tracking CHECKOUT_STARTED', error); 566 }); 567 }, 568 569 sendIdentityEvent: function(email) { 570 if (!this.isPixelSDKReady()) return; 571 window.$mcSite.pixel.api.identify({ 572 type: 'EMAIL', 573 value: email 574 }); 575 }, 576 577 /** 578 * Send PURCHASED event 579 * 580 * @param {Object} order Order data 581 */ 582 sendPurchased: function (order) { 583 if (!this.isPixelSDKReady()) return; 584 585 window.$mcSite.pixel.api.track('PURCHASED', { 586 order: order 587 }).catch((error) => { 588 console.error('Mailchimp Pixel: Error tracking PURCHASED', error); 589 }); 590 }, 591 592 /** 593 * Send PRODUCT_CATEGORY_VIEWED event 594 * 595 * @param {Object} category Category data 596 */ 597 sendCategoryViewed: function (category) { 598 if (!this.isPixelSDKReady()) return; 599 600 window.$mcSite.pixel.api.track('PRODUCT_CATEGORY_VIEWED', category).catch((error) => { 601 console.error('Mailchimp Pixel: Error tracking PRODUCT_CATEGORY_VIEWED', error); 602 }); 603 }, 604 605 /** 606 * Send SEARCH_SUBMITTED event 607 * 608 * @param {Object} search Search data 609 */ 610 sendSearchSubmitted: function (search) { 611 if (!this.isPixelSDKReady()) return; 612 613 window.$mcSite.pixel.api.track('SEARCH_SUBMITTED', search).catch((error) => { 614 console.error('Mailchimp Pixel: Error tracking SEARCH_SUBMITTED', error); 615 }); 616 }, 617 618 /** 619 * Find product by ID from pre-loaded products 620 * 621 * @param {number|string} productId Product ID 622 * @return {Object|null} Product data or null 623 */ 624 findProductById: function (productId) { 625 if (!window.mcPixel || !window.mcPixel.data || !window.mcPixel.data.products) { 626 return null; 627 } 628 629 const products = window.mcPixel.data.products; 630 const id = String(productId); 631 632 for (let i = 0; i < products.length; i++) { 633 if (String(products[i].id) === id) { 634 return products[i]; 635 } 636 } 637 638 return null; 639 }, 640 641 /** 642 * Debounced fetch for add-to-cart events. 643 * Each call resets the timer so rapid-fire triggers coalesce into one 644 * fetch that drains the entire PHP queue. 645 * If a fetch is already in flight, schedule another drain after it completes. 646 */ 647 fetchAndTrackAddToCart: function () { 648 var self = this; 649 if (!this.isPixelSDKReady()) return; 650 651 clearTimeout(this._atcTimer); 652 this._atcTimer = setTimeout(function () { 653 self._drainAddToCartQueue(); 654 }, 600); 655 }, 656 657 _drainAddToCartQueue: async function () { 658 if (this._atcFetching) return; 659 this._atcFetching = true; 660 661 try { 662 var res = await fetch(this.getRestBase() + 'pixel/atc', { 663 method: 'GET', 664 credentials: 'same-origin', 665 headers: { 'Accept': 'application/json' }, 666 }); 667 668 if (!res.ok) return; 669 670 var data = await res.json(); 671 if (!data) return; 672 673 var items = Array.isArray(data) ? data : [data]; 674 for (var i = 0; i < items.length; i++) { 675 this.sendProductAddedToCart(items[i]); 676 } 677 } catch (e) { 678 // no-op 679 } finally { 680 this._atcFetching = false;
681 } 682 }, 683 684 /** 685 * Debounced fetch for remove-from-cart events. 686 * Same debounce pattern as add-to-cart. 687 */ 688 fetchAndTrackRemoveFromCart: function () { 689 var self = this; 690 if (!this.isPixelSDKReady()) return; 691 692 clearTimeout(this._rfcTimer); 693 this._rfcTimer = setTimeout(function () { 694 self._drainRemoveFromCartQueue(); 695 }, 600); 696 }, 697 698 _drainRemoveFromCartQueue: async function () { 699 if (this._rfcFetching) return; 700 this._rfcFetching = true; 701 702 try { 703 var res = await fetch(this.getRestBase() + 'pixel/rfc', { 704 method: 'GET', 705 credentials: 'same-origin', 706 headers: { 'Accept': 'application/json' }, 707 }); 708 709 if (!res.ok) return; 710 711 var data = await res.json(); 712 if (!data) return; 713 714 var items = Array.isArray(data) ? data : [data]; 715 for (var i = 0; i < items.length; i++) { 716 this.sendProductRemovedFromCart(items[i]); 717 } 718 } catch (e) { 719 // no-op 720 } finally { 721 this._rfcFetching = false; 722 } 723 }, 724 725 /** 726 * Attach DOM and jQuery event listeners for cart add/remove. 727 * These serve as fallback triggers alongside the fetch interceptor. 728 */ 729 attachCartEventListeners: function () { 730 const self = this; 731 732 // Classic themes (jQuery events fired by WooCommerce's add-to-cart.js) 733 $(document.body).on('added_to_cart', function () { 734 self.fetchAndTrackAddToCart(); 735 }); 736 $(document.body).on('removed_from_cart', function () { 737 self.fetchAndTrackRemoveFromCart(); 738 }); 739 740 // WC Blocks DOM CustomEvents 741 document.body.addEventListener('wc-blocks_added_to_cart', function () { 742 self.fetchAndTrackAddToCart(); 743 }); 744 document.body.addEventListener('wc-blocks_removed_from_cart', function () { 745 self.fetchAndTrackRemoveFromCart(); 746 }); 747 }, 748 749 /** 750 * Intercept window.fetch to detect WooCommerce Store API cart mutations. 751 * 752 * This is the most reliable method for catching add-to-cart and remove-from-cart 753 * in block-based WooCommerce setups, where DOM events may not fire consistently. 754 * Watches for successful POST requests to the Store API cart endpoints. 755 */ 756 interceptStoreApiRequests: function () { 757 const self = this; 758 const originalFetch = window.fetch; 759 760 window.fetch = function (input, init) { 761 var promise = originalFetch.apply(this, arguments); 762 763 promise.then(function (response) { 764 try { 765 if (!response.ok) return; 766 767 var method = (init && init.method) ? init.method.toUpperCase() : 768 (input instanceof Request ? input.method.toUpperCase() : 'GET'); 769 if (method !== 'POST') return; 770 771 var url = typeof input === 'string' ? input : 772 (input instanceof Request ? input.url : String(input)); 773 774 if (/wc\/store\/v1\/cart\/add-item/.test(url) || /wc\/store\/v1\/cart\/update-item/.test(url)) { 775 self.fetchAndTrackAddToCart(); 776 } else if (/wc\/store\/v1\/cart\/remove-item/.test(url)) { 777 self.fetchAndTrackRemoveFromCart(); 778 } 779 } catch (e) { 780 // Silently ignore interceptor errors 781 } 782 }).catch(function () { 783 // Ignore - the original caller handles fetch errors 784 }); 785 786 return promise; 787 }; 788 }, 789 }; 790 791 // Initialize when DOM is ready 792 $(document).ready(function () { 793 MailchimpPixelTracking.init(); 794 }); 795 796})(jQuery);
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.