1/* global pysOptions */ 2 3 4! function ($, options) { 5 if (options.debug) { 6 console.log('PYS:', options); 7 } 8 var uniqueId = {}; 9 10 let gtm_variables = {}; 11 let gtm_datalayername = "dynamicVariable"; 12 13 // Resolved by cookieDomain() on first use, not here. 14 var probedCookieDomain = null; 15 16 /** 17 * Domain attribute for cookies we write. Probed once, on first use (not 18 * at load) so the probe's own write lands behind whatever consent gate 19 * the caller is behind. 20 * 21 * Not suppressed by options.cookie.disabled_all_cookie: deleting a 22 * cookie needs the same domain it was written on, so suppressing here 23 * would leave parent-scoped cookies in place while appearing removed. 24 * 25 * Memoised only after the probe runs, so a disabled_all_cookie or option 26 * arriving later over REST is still honored. 27 * 28 * An answer already reported to PHP in pys_cd is reused, so the probe 29 * runs once per visitor, not once per page load. 30 */ 31 function cookieDomain() { 32 if (probedCookieDomain !== null) { 33 return probedCookieDomain; 34 } 35 if (!options || !options.hasOwnProperty("track_cookie_for_subdomains") 36 || !options.track_cookie_for_subdomains) { 37 return ''; 38 } 39 probedCookieDomain = reportedCookieDomain(); 40 if (probedCookieDomain === null) { 41 probedCookieDomain = probeCookieDomain(); 42 } 43 return probedCookieDomain; 44 } 45 46 /** 47 * The domain an earlier page load verified and stored in pys_cd, or 48 * null when there is nothing usable and the probe has to run. 49 * 50 * The browser sending us pys_cd scoped to that domain is itself proof 51 * the scope is accepted, so no probe is needed. The value is checked 52 * against the current host on a label boundary; anything else (another 53 * site's value, a tampered one) is ignored and the probe decides. 54 * 55 * A value with the shape of a public suffix (co.uk, github.io) is 56 * test-written before it is trusted: the value is not the cookie's own 57 * scope, so a sibling subdomain can plant one, and reusing it would 58 * make every PYS cookie this visitor gets vanish. PHP validates the 59 * same value only for its own writes; this covers ours. The test costs 60 * one throwaway write, and only for values of that shape â see 61 * suspiciousCookieDomain(). 62 * 63 * 'host-only' is deliberately not reused: it may be a false negative 64 * from a full cookie jar, and reportCookieDomain() replaces it once a 65 * probe succeeds, so re-probing lets it self-heal. The other false 66 * negative â a domain narrower than the registrable one, from the same 67 * full jar â is accepted as is: it is reused and stays until pys_cd 68 * expires (365 days) or is cleared, because re-probing it would put the 69 * throwaway write back on every page load for every visitor. 70 */ 71 function reportedCookieDomain() { 72 var reported, host; 73 try { 74 reported = Cookies.get('pys_cd'); 75 host = String(location.hostname || '').toLowerCase(); 76 } catch (e) { 77 return null; 78 } 79 if (!reported || reported === 'host-only') return null; 80 reported = String(reported).toLowerCase(); 81 if (reported.indexOf('.') === -1 || !host) return null; 82 if (host !== reported && host.slice(-(reported.length + 1)) !== '.' + reported) { 83 return null; 84 } 85 if (suspiciousCookieDomain(reported) && !cookieDomainAccepted('.' + reported)) { 86 return null; 87 } 88 return '.' + reported; 89 } 90 91 /** 92 * Whether a reported domain has the shape of a public suffix and so 93 * needs a test write before it is reused: two labels that are both 94 * short (co.uk, com.au, net.br, ac.jp), or one of the multi-tenant 95 * hosts whose registrable part is a subdomain (github.io, vercel.app). 96 * A real two-label domain of that shape (hp.com) pays one throwaway 97 * write per page load â what every site paid before pys_cd was reused. 98 */ 99 function suspiciousCookieDomain(domain) { 100 var labels = String(domain).split('.'); 101 if (labels.length === 2 && labels[0].length <= 3 && labels[1].length <= 3) { 102 return true; 103 } 104 var known = ['github.io', 'vercel.app', 'pages.dev', 'wpcomstaging.com', 105 'appspot.com', 'netlify.app', 'herokuapp.com', 'myshopify.com']; 106 return known.indexOf(domain) !== -1; 107 } 108 109 /** 110 * Find the broadest domain this browser will actually accept a cookie 111 * on, walking from the broadest candidate inward. 112 * 113 * We used to rely on tldjs.getDomain(), but its bundled Public Suffix 114 * List predates ~2018 and returns the public suffix itself for newer 115 * domains (vercel.app, pages.dev, wpcomstaging.com), so cookies scoped 116 * there were silently dropped. Asking the browser instead keeps us in 117 * sync with its own current PSL. 118 * 119 * The probe cookie is written and removed synchronously, carries no 120 * data, and never outlives this call. If a consent tool blocks the 121 * write, cookieDomainAccepted() reports it rejected and we fall through 122 * to host-only â the correct outcome. 123 * 124 * Returns '' when nothing works (IP/single-label host, or all writes 125 * blocked); host-only is never rejected, so it's the safe floor. 126 */ 127 function probeCookieDomain() { 128 var host = ''; 129 try { host = String(location.hostname || ''); } catch (e) { return ''; } 130 131 // No Domain attribute is valid for an IP literal or a dotless host. 132 if (!host || host.indexOf('.') === -1) return ''; 133 if (host.indexOf(':') !== -1) return ''; // IPv6 literal 134 if (/^[\d.]+$/.test(host)) return ''; // IPv4 literal 135 136 var labels = host.split('.'); 137 // Broadest first: last two labels, then three, ... up to the full host. 138 for (var take = 2; take <= labels.length; take++) { 139 var candidate = '.' + labels.slice(labels.length - take).join('.'); 140 if (cookieDomainAccepted(candidate)) return candidate; 141 } 142 return ''; 143 } 144 145 /** 146 * Write a throwaway cookie at $candidate and report whether it stuck. 147 * A false negative is possible if the jar is at its per-domain limit; 148 * we then fall through to a narrower candidate and ultimately host-only. 149 * 150 * The name is fixed: consent scanners (Cookiebot and the like) record 151 * every write, not the jar afterwards, and a random name made each 152 * crawled page a new entry in the site's cookie declaration. The random 153 * part lives in the value instead, which still tells this write apart
154 * from a stale cookie left behind by an earlier, interrupted probe. 155 */ 156 function cookieDomainAccepted(candidate) { 157 var name = 'pys_dt'; 158 var value = String(Math.floor(Math.random() * 1e9)); 159 var accepted = false; 160 try { 161 document.cookie = name + '=' + value + '; path=/; domain=' + candidate + '; SameSite=Lax'; 162 accepted = ('; ' + document.cookie + ';').indexOf('; ' + name + '=' + value + ';') !== -1; 163 } catch (e) { 164 return false; 165 } finally { 166 // Remove either way; deleting an unset cookie is a no-op. 167 try { 168 document.cookie = name + '=; path=/; domain=' + candidate + '; max-age=0'; 169 } catch (e) {} 170 } 171 return accepted; 172 } 173 174 /** 175 * Hand PHP the cookie domain that probeCookieDomain() verified, since 176 * PHP has no Public Suffix List and guesses wrong on hosts like 177 * shop.example.co.uk or mysite.github.io â scoping its cookies to a 178 * public suffix the browser then drops. 179 * 180 * Value is the site's registrable domain (no PII, same for every 181 * visitor); PHP validates it against the request host before use (see 182 * pys_cookie_domain_resolve). 183 * 184 * Written from manageCookies() under the same consent gates as every 185 * other cookie, not unconditionally. 186 * 187 * No domain at all is an answer too â see reportHostOnlyCookieDomain(). 188 */ 189 function reportCookieDomain(resolved) { 190 var bare = resolved ? String(resolved).replace(/^\./, '') : ''; 191 if (!bare || bare.indexOf('.') === -1) { 192 reportHostOnlyCookieDomain(); 193 return; 194 } 195 try { 196 if (Cookies.get('pys_cd') === bare) return; // already reported 197 // A 'host-only' answer from an earlier load is a separate jar 198 // entry (no Domain attribute), and PHP reads whichever the 199 // browser sends first â so it is removed, not outranked. 200 Cookies.remove('pys_cd', { path: '/', domain: '' }); 201 Cookies.set('pys_cd', bare, { 202 expires: 365, 203 path: '/', 204 domain: resolved, 205 sameSite: 'Lax' 206 }); 207 } catch (e) {} 208 } 209 210 /** 211 * Tell PHP that host-only is the scope in use: the option asks for a 212 * Domain attribute, but probeCookieDomain() found none this browser 213 * accepts (a public-suffix host such as example.github.io, or every 214 * candidate write refused). Without this PHP cannot tell "not answered 215 * yet" from "never will", and withholds its own cookies for ever. 216 * 217 * Only with the option on: off, PHP writes host-only already and waits 218 * for nothing. Never overwrites an existing report â a domain accepted 219 * on an earlier load outranks a probe that failed on this one (a full 220 * cookie jar gives a false negative). 221 */ 222 function reportHostOnlyCookieDomain() { 223 if (!options || !options.track_cookie_for_subdomains) return; 224 try { 225 if (Cookies.get('pys_cd') !== undefined) return; // an answer exists 226 Cookies.set('pys_cd', 'host-only', { 227 expires: 365, 228 path: '/', 229 sameSite: 'Lax' 230 }); 231 } catch (e) {} 232 } 233 234 // Per-page referrer for Facebook CAPI events. Captured once per script 235 // load so SPA-style navigations within the same HTML document do not 236 // overwrite it. Internal/external is NOT filtered â Meta expects this 237 // field to describe the URL of the previous page for THIS event. 238 var pageReferrer = ''; 239 try { 240 pageReferrer = stripUrlQuery((document.referrer || '').toString()); 241 } catch (e) {} 242 243 // Session-entry referrer (fallback). Captured at session start the first 244 // time the visitor lands from an EXTERNAL site, mirrored to a session 245 // cookie so PHP can read it for orphan events (async/cron) that arrive 246 // without a per-page payload. Never the primary source.
247 var PYS_SESSION_ENTRY_REFERRER_KEY = 'pys_session_entry_referrer'; 248 function stripUrlQuery(url) { 249 if (!url) return ''; 250 var clean = String(url); 251 var q = clean.indexOf('?'); 252 if (q !== -1) clean = clean.substring(0, q); 253 var h = clean.indexOf('#'); 254 if (h !== -1) clean = clean.substring(0, h); 255 return clean; 256 } 257 var sessionEntryReferrer = ''; 258 function getOrInitSessionEntryReferrer(consentGranted) { 259 var value = ''; 260 try { 261 if (typeof window.sessionStorage !== 'undefined') { 262 value = window.sessionStorage.getItem(PYS_SESSION_ENTRY_REFERRER_KEY) || ''; 263 } 264 } catch (e) {} 265 266 if (!value) { 267 var cookieValue = Cookies.get(PYS_SESSION_ENTRY_REFERRER_KEY); 268 if (cookieValue && cookieValue !== 'undefined') { 269 value = cookieValue; 270 try { 271 if (typeof window.sessionStorage !== 'undefined') { 272 window.sessionStorage.setItem(PYS_SESSION_ENTRY_REFERRER_KEY, value); 273 } 274 } catch (e) {} 275 } 276 } 277 278 if (!value) { 279 var raw = ''; 280 try { 281 raw = stripUrlQuery((document.referrer || '').toString()); 282 } catch (e) {} 283 if (raw) { 284 var refHost = ''; 285 try { 286 refHost = new URL(raw).hostname; 287 } catch (e) {} 288 if (refHost && refHost !== window.location.hostname) { 289 value = raw; 290 try { 291 if (typeof window.sessionStorage !== 'undefined') { 292 window.sessionStorage.setItem(PYS_SESSION_ENTRY_REFERRER_KEY, value); 293 } 294 } catch (e) {} 295 } 296 } 297 } 298 299 // Mirror to a session cookie only once consent is given. The cookie 300 // is session-scoped (no expires) and dies with the browser, just 301 // like the sessionStorage entry. 302 if (value && consentGranted && options && options.cookie 303 && !options.cookie.disabled_all_cookie) { 304 var existingCookie = Cookies.get(PYS_SESSION_ENTRY_REFERRER_KEY); 305 if (!existingCookie || existingCookie === 'undefined') { 306 Cookies.set(PYS_SESSION_ENTRY_REFERRER_KEY, value, { path: '/', domain: cookieDomain() }); 307 } 308 } 309 310 sessionEntryReferrer = value; 311 return value; 312 } 313 314 // Populate sessionStorage / sessionEntryReferrer immediately. Cookie 315 // mirroring is deferred to the consent-gated path in Facebook.loadPixel(). 316 getOrInitSessionEntryReferrer(false); 317 318 // Per-page event referrer (session cookie). Snapshot of document.referrer 319 // for the CURRENT page, refreshed on every page load. Read server-side 320 // by saveFbTagsInOrder() to capture the buyer's checkout-page referrer 321 // for orphan CAPI events (APT, gateway webhooks, manual status changes). 322 // Internal URLs are intentionally NOT filtered â the goal is the page 323 // that led the buyer to checkout, which is usually internal. 324 // If document.referrer is empty (direct navigation), the previous value 325 // is preserved so a session that began with an external referrer keeps 326 // it across same-tab navigations. 327 var PYS_EVENT_REFERRER_KEY = 'pys_event_referrer'; 328 function writeEventReferrerCookie() { 329 if (!pageReferrer) { 330 return; 331 } 332 if (!options || !options.cookie || options.cookie.disabled_all_cookie) { 333 return; 334 } 335 try { 336 Cookies.set(PYS_EVENT_REFERRER_KEY, pageReferrer, { path: '/', domain: cookieDomain() }); 337 } catch (e) {} 338 } 339 /** 340 * Resolve parameter value based on mode (static or dynamic) 341 * 342 * @param {Object|string} param - Parameter object with { value, selector } or string value 343 * @param {string} key - Parameter key name 344 * @returns {string|null} - Resolved value 345 */ 346 function resolveParamValue(param, key = '') { 347 // 1ï¸â£ null / undefined - skip this parameter 348 if (param === null || param === undefined) { 349 return null; 350 } 351 352 // 2ï¸â£ Handle primitive types (string, number, boolean) 353 if (typeof param !== 'object') { 354 return param; 355 } 356 357 // 3ï¸â£ Handle arrays - process each element recursively 358 if (Array.isArray(param)) { 359 return param.map(item => resolveParamValue(item, key)); 360 } 361 362 // 4ï¸â£ Check if this is a configuration object (has value/selector/dynamic/input_type) 363 const isConfigObject = 364 ('value' in param || 'selector' in param || 'dynamic' in param || 'input_type' in param) && 365 Object.keys(param).every(k => 366 ['value', 'selector', 'dynamic', 'input_type', 'name'].includes(k) 367 ); 368 369 if (isConfigObject) { 370 // STATIC MODE: selector is empty or dynamic is false 371 const isStatic = 372 (!param.dynamic || param.dynamic === false) || 373 (!param.selector || param.selector.trim() === ''); 374 375 if (isStatic) { 376 let value = param.value ?? null; 377 378 // Apply numeric conversion if needed 379 if (param.input_type === "float" || param.input_type === "int") { 380 value = extractNumericValue(value, param.input_type === "int"); 381 } 382 383 return value; 384 } 385 386 // DYNAMIC MODE: selector is present 387 try { 388 const el = document.querySelector(param.selector); 389 if (!el) { 390 return null; 391 } 392 393 let value = 394 el.value || 395 el.innerText || 396 el.textContent || 397 el.getAttribute("content") || 398 el.getAttribute("data-value") || 399 null; 400 401 // Apply numeric conversion if needed 402 if (param.input_type === "float" || param.input_type === "int") { 403 value = extractNumericValue(value, param.input_type === "int"); 404 } 405 406 return value; 407 } catch (e) { 408 return null; 409 } 410 } 411 412 // 5ï¸â£ Plain object (like ecommerce data) - recursively process nested values 413 return Object.fromEntries( 414 Object.entries(param) 415 .map(([k, v]) => [k, resolveParamValue(v, k)]) 416 .filter(([k, v]) => v !== null && v !== undefined) 417 ); 418 } 419 420 function extractNumericValue(value, isInt = false) { 421 if (value === null || value === undefined) { 422 return null; 423 } 424 if (isInt) { 425 return parseInt(value); 426 } 427 return parseFloat(value); 428 } 429 var dummyPinterest = function () { 430 431 /** 432 * Public API 433 */ 434 return { 435 // Must mirror the real module's surface. Utils.fireEventForAllPixel() 436 // calls tag() on every pixel unconditionally, so a stand-in without it 437 // throws "Pinterest.tag is not a function" whenever the add-on is not 438 // installed and this dummy stays in place. 439 tag: function () { 440 return "pinterest"; 441 }, 442 isEnabled: function () {}, 443 disable: function () {}, 444 loadPixel: function () {}, 445 fireEvent: function (name, data) { 446 return false;
447 }, 448 onAdSenseEvent: function (event) {}, 449 onClickEvent: function (params) {}, 450 onWatchVideo: function (params) {}, 451 onCommentEvent: function () {}, 452 onDownloadEvent: function (params) {}, 453 onFormEvent: function (params) {}, 454 onWooAddToCartOnButtonEvent: function (product_id) {}, 455 onWooAddToCartOnSingleEvent: function (product_id, qty, is_variable, is_external, $form) {}, 456 onWooRemoveFromCartEvent: function (cart_item_hash) {}, 457 onWooAffiliateEvent: function (product_id) {}, 458 onWooPayPalEvent: function (event) {}, 459 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) {}, 460 onEddRemoveFromCartEvent: function (item) {}, 461 onPageScroll: function (event) {}, 462 onTime: function (event) {}, 463 464 } 465 466 }(); 467 468 var dummyBing = function () { 469 470 /** 471 * Public API 472 */ 473 return { 474 // See dummyPinterest: tag() is required by Utils.fireEventForAllPixel(). 475 tag: function () { 476 return "bing"; 477 }, 478 isEnabled: function () {}, 479 disable: function () {}, 480 loadPixel: function () {}, 481 fireEvent: function (name, data) { 482 return false; 483 }, 484 onAdSenseEvent: function () {}, 485 onClickEvent: function (params) {}, 486 onWatchVideo: function (params) {}, 487 onCommentEvent: function () {}, 488 onFormEvent: function (params) {}, 489 onDownloadEvent: function (params) {}, 490 onWooAddToCartOnButtonEvent: function (product_id) {}, 491 onWooAddToCartOnSingleEvent: function (product_id, qty, is_variable, is_external, $form) {}, 492 onWooRemoveFromCartEvent: function (cart_item_hash) {}, 493 onWooAffiliateEvent: function (product_id) {}, 494 onWooPayPalEvent: function () {}, 495 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) {}, 496 onEddRemoveFromCartEvent: function (item) {}, 497 onPageScroll: function (event) {}, 498 onTime: function (event) {}, 499 } 500 501 }(); 502 503 var dummyReddit = function () { 504 /** 505 * Public API 506 */ 507 return { 508 tag: function () { 509 return "reddit"; 510 }, 511 isEnabled: function () {}, 512 disable: function () {}, 513 loadPixel: function () {}, 514 fireEvent: function ( name, data ) { 515 return false; 516 }, 517 onAdSenseEvent: function ( event ) {}, 518 onClickEvent: function ( params ) {}, 519 onWatchVideo: function ( params ) {}, 520 onCommentEvent: function ( event ) {}, 521 onFormEvent: function ( params ) {}, 522 onDownloadEvent: function ( params ) {}, 523 onWooAddToCartOnButtonEvent: function ( product_id ) {}, 524 onWooAddToCartOnSingleEvent: function ( product_id, qty, product_type, is_external, $form ) {}, 525 onWooRemoveFromCartEvent: function ( cart_item_hash ) {}, 526 onWooAffiliateEvent: function ( product_id ) {}, 527 onWooPayPalEvent: function ( event ) {}, 528 onEddAddToCartOnButtonEvent: function ( download_id, price_index, qty ) {}, 529 onEddRemoveFromCartEvent: function ( item ) {}, 530 onPageScroll: function ( event ) {}, 531 onTime: function ( event ) {}, 532 } 533 }(); 534 535 536 var Utils = function (options) { 537 538 var Pinterest = dummyPinterest; 539 540 var Bing = dummyBing; 541 542 var Reddit = dummyReddit; 543 544 var gtag_loaded = false; 545 546 var gtm_loaded = false; 547 548 // The GTM container's consent state is sent once per page load. Two code 549 // paths reach it (loadGTMScript() and the gtm_just_data_layer branch of 550 // GTM.loadPixel(), which can both run for the same page), and Consent Mode 551 // expects a single transition, not a stream of them. 552 var gtm_consent_state_pushed = false; 553 554 let isNewSession = checkSession(); 555 556 // In-flight pys_get_pbid request. Several code paths call manageCookies() 557 // before the first reply lands; one request is enough for all of them. 558 var pbidRequest = null; 559 // True once a pixel has waited for that reply (or given up on it), so 560 // the hold in holdForIdentity() happens at most once per page. 561 var pbidSettled = false;
562 var PBID_WAIT_MS = 1000; 563 564 var utmTerms = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; 565 566 var utmId = ['fbadid', 'gadid', 'padid', 'bingid']; 567 568 let dataLayerName = 'dataLayerPYS'; 569 570 let GTMdataLayerName = 'dataLayer'; 571 572 function validateEmail(email) { 573 var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 574 return re.test(email); 575 } 576 function getDomain(url) { 577 578 url = url.replace(/(https?:\/\/)?(www.)?/i, ''); 579 580 if (url.indexOf('/') !== -1) { 581 return url.split('/')[0]; 582 } 583 584 return url; 585 } 586 function loadPixels() { 587 588 Utils.refreshAddonPixels(); 589 590 if (!options.gdpr.all_disabled_by_api) { 591 592 if (!options.gdpr.facebook_disabled_by_api) { 593 Facebook.loadPixel(); 594 } 595 596 if (!options.gdpr.analytics_disabled_by_api) { 597 Analytics.loadPixel(); 598 } 599 600 if (!options.gdpr.analytics_disabled_by_api) { 601 GTM.loadPixel(); 602 } 603 604 if (!options.gdpr.pinterest_disabled_by_api) { 605 Pinterest.loadPixel(); 606 } 607 608 if (!options.gdpr.bing_disabled_by_api) { 609 Bing.loadPixel(); 610 } 611 612 if (!options.gdpr.openai_disabled_by_api) { 613 OpenAI.loadPixel(); 614 } 615 616 if (!options.gdpr.reddit_disabled_by_api) { 617 Reddit.loadPixel(); 618 } 619 } 620 if (options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined") { 621 if (typeof CS_Data.cs_google_analytics_consent_mode !== "undefined" && CS_Data.cs_google_analytics_consent_mode == 1) { 622 Analytics.loadPixel(); 623 } 624 } 625 626 } 627 628 function checkSession() { 629 if( Cookies.get('pys_start_session') === undefined || 630 Cookies.get('pys_session_limit') === undefined) { 631 firstVisit = true; 632 return true 633 } 634 return false 635 636 } 637 638 function getTrafficSource() { 639 640 try { 641 642 let referrer = stripUrlQuery(document.referrer.toString()), 643 source; 644 645 let direct = referrer.length === 0; 646 let internal = direct ? false : referrer.indexOf(options.siteUrl) === 0; 647 let external = !direct && !internal; 648 649 if (external === false) { 650 source = 'direct'; 651 } else { 652 source = referrer; 653 } 654 655 if (source !== 'direct') { 656 // leave only domain (Issue #70) 657 return getDomain(source); 658 } else { 659 return source; 660 } 661 662 } catch (e) { 663 console.error(e); 664 return 'direct'; 665 } 666 667 } 668 669 /** 670 * Return query variables object with where property name is query variable 671 * and property value is query variable value. 672 */ 673 function getQueryVars() { 674 675 try { 676 677 var result = {}, 678 tmp = []; 679 680 window.location.search 681 .substr(1) 682 .split("&") 683 .forEach(function (item) { 684 685 tmp = item.split('='); 686 687 if (tmp.length > 1) { 688 result[tmp[0]] = tmp[1]; 689 } 690 691 }); 692 693 return result; 694 695 } catch (e) { 696 console.error(e); 697 return {}; 698 } 699 700 } 701 702 703 /**
704 * @param useCurrentUrlFallback Pass false where an empty value is better 705 * than a guess, because a more reliable fallback exists downstream. 706 */ 707 function getLandingPageValue(useCurrentUrlFallback = true) { 708 let name = "pys_landing_page" 709 if(options.visit_data_model === "last_visit") { 710 name = "last_pys_landing_page" 711 } 712 if(Cookies.get(name) && Cookies.get(name) !== "undefined") { 713 return Cookies.get(name); 714 } 715 // No cookie of our own. The dynamic-options endpoint may still know: 716 // it answers per visitor and is never cached, so it is allowed to carry 717 // the PHP session copy â which is all a visitor whose cookies a consent 718 // manager has blocked ever has. In the HTML this value is always empty, 719 // and an empty value means "nothing to say", so this branch is simply 720 // skipped unless the endpoint filled it in. 721 if ( options.hasOwnProperty( "tracking_analytics" ) 722 && options.tracking_analytics 723 && options.tracking_analytics.TrafficLanding ) { 724 return options.tracking_analytics.TrafficLanding; 725 } 726 727 // Nobody knows better than the current URL: it is what the cookie would 728 // have been set to a moment ago. 729 return useCurrentUrlFallback ? window.location.href : ""; 730 } 731 function getTrafficSourceValue() { 732 let name = "pysTrafficSource" 733 if(options.visit_data_model === "last_visit") { 734 name = "last_pysTrafficSource" 735 } 736 if(Cookies.get(name) && Cookies.get(name) !== "undefined") { 737 return Cookies.get(name); 738 } 739 // See getLandingPageValue(): filled by the dynamic-options endpoint only. 740 if ( options.hasOwnProperty( "tracking_analytics" ) 741 && options.tracking_analytics 742 && options.tracking_analytics.TrafficSource ) { 743 return options.tracking_analytics.TrafficSource; 744 } 745 return ""; 746 } 747 748 function getUTMId(useLast = false) { 749 try { 750 let cookiePrefix = 'pys_' 751 let terms = []; 752 if (useLast) { 753 cookiePrefix = 'last_pys_' 754 } 755 // Fall back to this request's own query string, not to the server 756 // options: cached HTML carries whatever the visitor who generated 757 // the cache entry arrived with. 758 let queryVars = getQueryVars(); 759 $.each(utmId, function (index, name) { 760 if (Cookies.get(cookiePrefix + name)) { 761 terms[name] = Cookies.get(cookiePrefix + name) 762 } 763 else if(options.hasOwnProperty("tracking_analytics") 764 && options.tracking_analytics 765 && options.tracking_analytics.TrafficUtmsId 766 && options.tracking_analytics.TrafficUtmsId[name]) { 767 terms[name] = filterEmails(options.tracking_analytics.TrafficUtmsId[name]) 768 } 769 else if(queryVars.hasOwnProperty(name)) { 770 terms[name] = filterEmails(queryVars[name]) 771 } 772 }); 773 return terms; 774 } catch (e) { 775 console.error(e); 776 return []; 777 } 778 } 779 /** 780 * Return UTM terms from request query variables or from cookies. 781 */ 782 function getUTMs(useLast = false) { 783 784 try { 785 let cookiePrefix = 'pys_' 786 if(useLast) { 787 cookiePrefix = 'last_pys_' 788 } 789 let terms = []; 790 // See getUTMId(): the query string, never the server options. 791 let queryVars = getQueryVars(); 792 $.each(utmTerms, function (index, name) { 793 if (Cookies.get(cookiePrefix + name)) { 794 let value = Cookies.get(cookiePrefix + name); 795 terms[name] = filterEmails(value); // do not allow email in request params (Issue #70) 796 } 797 else if(options.hasOwnProperty("tracking_analytics") 798 && options.tracking_analytics 799 && options.tracking_analytics.TrafficUtms 800 && options.tracking_analytics.TrafficUtms[name]) { 801 terms[name] = filterEmails(options.tracking_analytics.TrafficUtms[name]) 802 } 803 else if(queryVars.hasOwnProperty(name)) { 804 terms[name] = filterEmails(queryVars[name]) 805 } 806 }); 807 808 return terms; 809 810 } catch (e) { 811 console.error(e); 812 return []; 813 } 814 815 } 816 817 function getDateTime() { 818 var dateTime = new Array(); 819 var date = new Date(), 820 days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 821 months = ['January', 'February', 'March', 'April', 'May', 'June', 822 'July', 'August', 'September', 'October', 'November', 'December' 823 ], 824 hours = ['00-01', '01-02', '02-03', '03-04', '04-05', '05-06', '06-07', '07-08', 825 '08-09', '09-10', '10-11', '11-12', '12-13', '13-14', '14-15', '15-16', '16-17', 826 '17-18', '18-19', '19-20', '20-21', '21-22', '22-23', '23-24' 827 ]; 828 dateTime.push(hours[date.getHours()]); 829 dateTime.push(days[date.getDay()]); 830 dateTime.push(months[date.getMonth()]); 831 return dateTime; 832 } 833 834 function filterEmails(value) { 835 return validateEmail(value) ? undefined : value; 836 } 837 838 /** 839 * PUBLIC API 840 */ 841 return { 842 PRODUCT_SIMPLE : 0, 843 PRODUCT_VARIABLE : 1, 844 PRODUCT_BUNDLE : 2, 845 PRODUCT_GROUPED : 3, 846 utmTerms : utmTerms, 847 utmId : utmId, 848 fireEventForAllPixel:function(functionName,events){ 849 Utils.refreshAddonPixels(); 850 851 if (events.hasOwnProperty(Facebook.tag())) 852 Facebook[functionName](events[Facebook.tag()]); 853 if (events.hasOwnProperty(Analytics.tag())) 854 Analytics[functionName](events[Analytics.tag()]); 855 if (events.hasOwnProperty(Pinterest.tag())) 856 Pinterest[functionName](events[Pinterest.tag()]); 857 if (events.hasOwnProperty(Bing.tag())) 858 Bing[functionName](events[Bing.tag()]); 859 if (events.hasOwnProperty(Reddit.tag())) 860 Reddit[functionName](events[Reddit.tag()]); 861 862 if (events.hasOwnProperty(GTM.tag())) 863 GTM[functionName](events[GTM.tag()]); 864 }, 865 866 /** 867 * Re-read the pixels that live in separate plugins. 868 */ 869 refreshAddonPixels: function () { 870 Pinterest = Utils.setupPinterestObject(); 871 Bing = Utils.setupBingObject(); 872 Reddit = Utils.setupRedditObject(); 873 }, 874 875 /** 876 * Is one of ConsentMagic's switches actually on? 877 */ 878 csModeEnabled: function ( value ) { 879 return Number( value ) === 1; 880 }, 881 882 setupPinterestObject: function () { 883 Pinterest = window.pys.Pinterest || Pinterest; 884 return Pinterest; 885 }, 886 887 setupBingObject: function () { 888 Bing = window.pys.Bing || Bing; 889 return Bing; 890 }, 891 892 setupRedditObject: function () { 893 Reddit = window.pys.Reddit || Reddit; 894 return Reddit; 895 }, 896 897 // Clone all object members to another and return it 898 copyProperties: function (from, to) { 899 for (var key in from) { 900 if("function" == typeof from[key]) { 901 continue; 902 } 903 to[key] = from[key]; 904 } 905 return to; 906 }, 907 908 /** 909 * Ask PHP for this browser's external_id (single-flight). 910 * 911 * The cookie keeps the id across a network change, so it is 912 * requested whenever external_id is on -- not only when server 913 * events go over AJAX. Returns the in-flight jqXHR, or null when no
914 * request is warranted (feature off, cookie already there, no 915 * Meta pixel). 916 */ 917 requestPbid: function () { 918 if (!options.send_external_id || Cookies.get('pbid') || !Facebook.isEnabled()) { 919 return null; 920 } 921 922 if (!pbidRequest) { 923 pbidRequest = jQuery.ajax({ 924 url: options.ajaxUrl, 925 dataType: 'json', 926 data: { 927 action: 'pys_get_pbid' 928 }, 929 success: function (res) { 930 if (res.data && res.data.pbid != false) { 931 if(!(options.cookie.disabled_all_cookie || options.cookie.externalID_disabled_by_api)){ 932 var expires = parseInt(options.external_id_expire || 180); 933 Cookies.set('pbid', res.data.pbid, { expires: expires, path: '/',domain: cookieDomain() }); 934 } 935 936 if(options.hasOwnProperty('facebook')) { 937 options.facebook.advancedMatching = { 938 ...options.facebook.advancedMatching, // spread current advancedMatching values 939 external_id: res.data.pbid 940 }; 941 } 942 } 943 } 944 }).always(function () { 945 pbidRequest = null; 946 }); 947 } 948 949 return pbidRequest; 950 }, 951 952 /** 953 * Defer `fn` until this browser's external_id is known, or until a 954 * short ceiling passes. Returns true when `fn` was deferred (it will 955 * be called back), false when the caller may go ahead right now. 956 * 957 * Pixels read external_id when they initialise. On a first visit the 958 * cookie does not exist yet, and the inline value in the page may 959 * belong to whoever rendered a cached copy of it; the reply to 960 * pys_get_pbid is never cached and names this browser, so the first 961 * PageView waits for it. Sites that fetch identity over REST already 962 * have the right value by now and do not wait again. 963 */ 964 holdForIdentity: function (fn) { 965 if (pbidSettled || !options.send_external_id || Cookies.get('pbid')) { 966 return false; 967 } 968 969 if (options.dynamicDataUrl && options.facebook && options.facebook.advancedMatching 970 && options.facebook.advancedMatching.external_id) { 971 return false; 972 } 973 974 var request = Utils.requestPbid(); 975 if (!request) { 976 return false; 977 } 978 979 var done = false; 980 var finish = function () { 981 if (done) { 982 return; 983 } 984 done = true; 985 pbidSettled = true; 986 fn(); 987 }; 988 989 request.always(finish); 990 setTimeout(finish, PBID_WAIT_MS); 991 992 return true; 993 }, 994 995 manageCookies: function () { 996 997 if ( typeof options.cookie === 'undefined' ) { 998 return; 999 } 1000 1001 if (options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined') { 1002 if (Cookiebot.consented === false && !Cookiebot.consent['marketing'] && !Cookiebot.consent['statistics']) { 1003 return; 1004 } 1005 } 1006 let cm_consent_not_expressed = false; 1007 if ( options.gdpr.consent_magic_integration_enabled && window.CS_Data !== undefined && window.CS_Data.cs_refresh_after_consent == 1 ) { 1008 if ( Cookies.get( 'cs_viewed_cookie_policy' ) === undefined ) { 1009 cm_consent_not_expressed = true; 1010 } 1011 } 1012 1013 // Hand PHP the browser-verified cookie domain, under the same 1014 // consent gates as every cookie written below it. 1015 if( !cm_consent_not_expressed && !options.cookie.disabled_all_cookie ) { 1016 reportCookieDomain(cookieDomain()); 1017 } 1018 1019 if( !cm_consent_not_expressed && isNewSession && !options.cookie.disabled_all_cookie && !options.cookie.disabled_start_session_cookie) { 1020 let duration = options.last_visit_duration * 60000 1021 var now = new Date(); 1022 now.setTime(now.getTime() + duration); 1023 Cookies.set('pys_session_limit', true,{ expires: now, path: '/',domain: cookieDomain() }) 1024 Cookies.set('pys_start_session', true,{path: '/',domain: cookieDomain()}); 1025 } 1026 1027 if (!Cookies.get('pbid')) { 1028 Utils.requestPbid(); 1029 } else if (Facebook.isEnabled()){ 1030 if(Facebook.advancedMatching() && Facebook.advancedMatching().external_id && !(options.cookie.disabled_all_cookie || options.cookie.externalID_disabled_by_api)){ 1031 let expires = parseInt(options.external_id_expire || 180); 1032 Cookies.set('pbid', Facebook.advancedMatching().external_id, { expires: expires, path: '/',domain: cookieDomain() }); 1033 } 1034 } 1035 1036 let expires = parseInt(options.cookie_duration); // days 1037 let queryVars = getQueryVars(); 1038 let landing = window.location.href.split('?')[0]; 1039 try { 1040 // save data for first visit 1041 if(Cookies.get('pys_first_visit') === undefined && (!options.cookie.disabled_all_cookie)) { 1042 1043 if(!options.cookie.disabled_first_visit_cookie) 1044 { 1045 Cookies.set('pys_first_visit', true, { expires: expires, path: '/',domain: cookieDomain() }); 1046 } 1047 else { 1048 Cookies.remove('pys_first_visit', { path: '/',domain: cookieDomain() }) 1049 } 1050 1051 if(!options.cookie.disabled_trafficsource_cookie) 1052 { 1053 Cookies.set('pysTrafficSource', getTrafficSource(), { expires: expires,path: '/',domain: cookieDomain() }); 1054 } 1055 else { 1056 Cookies.remove('pysTrafficSource', { path: '/',domain: cookieDomain() }) 1057 } 1058 1059 if(!options.cookie.disabled_landing_page_cookie) 1060 { 1061 Cookies.set('pys_landing_page',landing,{ expires: expires,path: '/',domain: cookieDomain() }); 1062 } 1063 else { 1064 Cookies.remove('pys_landing_page', { path: '/',domain: cookieDomain() }) 1065 } 1066 1067 if(!options.cookie.disabled_utmTerms_cookie) 1068 { 1069 $.each(utmTerms, function (index, name) { 1070 if (queryVars.hasOwnProperty(name)) { 1071 Cookies.set('pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 1072 } else { 1073 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 1074 } 1075 }); 1076 } 1077 else { 1078 $.each(utmTerms, function (index, name) { 1079 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 1080 }); 1081 } 1082 1083 if(!options.cookie.disabled_utmId_cookie) 1084 { 1085 $.each(utmId,function(index,name) { 1086 if (queryVars.hasOwnProperty(name)) { 1087 Cookies.set('pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 1088 } else { 1089 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 1090 } 1091 }) 1092 } 1093 else { 1094 $.each(utmId, function (index, name) { 1095 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 1096 }); 1097 } 1098 } 1099 1100 // save data for last visit if it new session 1101 if(isNewSession && (!options.cookie.disabled_all_cookie)) { 1102 if(!options.cookie.disabled_trafficsource_cookie) 1103 { 1104 Cookies.set('last_pysTrafficSource', getTrafficSource(), { expires: expires,path: '/',domain: cookieDomain() }); 1105 } 1106 else { 1107 Cookies.remove('last_pysTrafficSource', { path: '/',domain: cookieDomain() }) 1108 } 1109 1110 if(!options.cookie.disabled_landing_page_cookie) 1111 { 1112 Cookies.set('last_pys_landing_page',landing,{ expires: expires,path: '/',domain: cookieDomain() }); 1113 } 1114 else { 1115 Cookies.remove('last_pys_landing_page', { path: '/',domain: cookieDomain() }) 1116 } 1117 1118 if(!options.cookie.disabled_utmTerms_cookie) 1119 { 1120 $.each(utmTerms, function (index, name) { 1121 if (queryVars.hasOwnProperty(name)) { 1122 Cookies.set('last_pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 1123 } else { 1124 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 1125 } 1126 }); 1127 } 1128 else { 1129 $.each(utmTerms, function (index, name) { 1130 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 1131 }); 1132 } 1133 1134 if(!options.cookie.disabled_utmId_cookie) 1135 { 1136 $.each(utmId,function(index,name) { 1137 if (queryVars.hasOwnProperty(name)) { 1138 Cookies.set('last_pys_' + name, queryVars[name], { expires: expires,path: '/',domain: cookieDomain() }); 1139 } else { 1140 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 1141 } 1142 }) 1143 } 1144 else { 1145 $.each(utmId, function (index, name) { 1146 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 1147 }); 1148 } 1149 1150 } 1151 if(options.cookie.disabled_start_session_cookie) { 1152 Cookies.remove('pys_start_session', { path: '/',domain: cookieDomain() }) 1153 Cookies.remove('pys_session_limit', { path: '/',domain: cookieDomain() }) 1154 } 1155 if(options.cookie.disabled_all_cookie) 1156 { 1157 Cookies.remove('pys_first_visit', { path: '/',domain: cookieDomain() }) 1158 Cookies.remove('pysTrafficSource', { path: '/',domain: cookieDomain() }) 1159 Cookies.remove('pys_landing_page', { path: '/',domain: cookieDomain() }) 1160 Cookies.remove('last_pys_landing_page', { path: '/',domain: cookieDomain() }) 1161 Cookies.remove('last_pysTrafficSource', { path: '/',domain: cookieDomain() }) 1162 Cookies.remove('pys_start_session', { path: '/',domain: cookieDomain() }) 1163 Cookies.remove('pys_session_limit', { path: '/',domain: cookieDomain() }) 1164 $.each(Utils.utmTerms, function (index, name) { 1165 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 1166 }); 1167 $.each(Utils.utmId,function(index,name) { 1168 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 1169 }) 1170 $.each(Utils.utmTerms, function (index, name) { 1171 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 1172 }); 1173 $.each(Utils.utmId,function(index,name) { 1174 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 1175 }); 1176 } 1177 } catch (e) { 1178 console.error(e); 1179 } 1180 }, 1181 /** 1182 * Is this Meta pixel opted into Meta-Enabled Conversion API? 1183 */ 1184 isMetaEnabledCapiPixel: function (pixelId) { 1185 var pixels = options.facebook && options.facebook.metaEnabledCapiPixels; 1186 return Array.isArray(pixels) && pixels.indexOf(pixelId) !== -1; 1187 }, 1188 /** 1189 * Generate unique ID 1190 */ 1191 generateUniqueId : function (event) { 1192 if(event.eventID.length == 0 || (event.type == "static" && options.ajaxForServerStaticEvent) || (event.type !== "static" && options.ajaxForServerEvent)) { 1193 let idKey = event.hasOwnProperty('custom_event_post_id') ? event.custom_event_post_id : event.e_id; 1194 if (!uniqueId.hasOwnProperty(idKey)) { 1195 uniqueId[idKey] = pys_generate_token(); 1196 } 1197 return uniqueId[idKey]; 1198 } 1199 else if(event.eventID.length !== 0) 1200 { 1201 return event.eventID; 1202 } 1203 }, 1204 1205 // Flatten object for sendBeacon compatibility 1206 flattenObject: function (obj, prefix = '', res = {}) { 1207 for (const [key, value] of Object.entries(obj)) { 1208 const prefixedKey = prefix 1209 ? (Array.isArray(obj) ? `${prefix}[${key}]` : `${prefix}[${key}]`)
1210 : key; 1211 if (value !== null && typeof value === 'object' && !(value instanceof File)) { 1212 this.flattenObject(value, prefixedKey, res); 1213 } else { 1214 res[prefixedKey] = value; 1215 } 1216 } 1217 return res; 1218 }, 1219 1220 sendServerAjaxRequest : function(url, data) { 1221 if (data.action === 'pys_api_event' && data.pixel === 'facebook' && window.pysFacebookRest) { 1222 // Use Facebook REST API 1223 this.sendRestAPIRequest(data, 'facebook'); 1224 return; 1225 } 1226 1227 if (data.action === 'pys_openai_api_event' && window.pysOpenAIRest) { 1228 this.sendRestAPIRequest(data, 'openai'); 1229 return; 1230 } 1231 1232 // Check if sendBeacon is enabled and supported 1233 if (options.useSendBeacon && navigator.sendBeacon) { 1234 try { 1235 // Flatten the data object for sendBeacon compatibility 1236 const flattenedData = this.flattenObject(data); 1237 const formData = new URLSearchParams(); 1238 1239 // Convert flattened data to URLSearchParams 1240 for (const [key, value] of Object.entries(flattenedData)) { 1241 if (value !== null && value !== undefined) { 1242 formData.append(key, value); 1243 } 1244 } 1245 1246 // Try to send using sendBeacon 1247 const success = navigator.sendBeacon(url, formData); 1248 if (success) { 1249 return; // Successfully sent via sendBeacon 1250 } 1251 } catch (e) { 1252 // If sendBeacon fails, fall back to jQuery.ajax 1253 if (options.debug) { 1254 console.log('PYS: sendBeacon failed, falling back to jQuery.ajax:', e); 1255 } 1256 } 1257 } 1258 1259 // Fallback to jQuery.ajax 1260 jQuery.ajax( { 1261 type: 'POST', 1262 url: url, 1263 data: data, 1264 headers: { 1265 'Cache-Control': 'no-cache' 1266 }, 1267 success: function () { 1268 }, 1269 } ); 1270 }, 1271 1272 sendRestAPIRequest: function (data, platform) { 1273 let restApiUrl; 1274 1275 // Get platform-specific REST API configuration 1276 switch (platform) { 1277 case 'facebook': 1278 restApiUrl = window.pysFacebookRest ? window.pysFacebookRest.restApiUrl : '/wp-json/pys-facebook/v1/event'; 1279 break; 1280 case 'openai': 1281 restApiUrl = window.pysOpenAIRest ? window.pysOpenAIRest.restApiUrl : '/wp-json/pys-openai/v1/event'; 1282 break; 1283 default: 1284 console.error('PYS: Unknown platform for REST API:', platform); 1285 this.sendAjaxFallback(data); 1286 return; 1287 } 1288 1289 // Prepare data for REST API 1290 const restApiData = { 1291 event: data.event, 1292 event_slug: data.event_slug || '', 1293 data: JSON.stringify(data.data || {}), 1294 ids: JSON.stringify(data.ids || []), 1295 eventID: data.event_id || data.eventID || '', 1296 woo_order: data.woo_order || '0', 1297 edd_order: data.edd_order || '0', 1298 order_key: data.order_key || '' 1299 }; 1300 1301 const forwardedReferrer = data.referrer_url || pageReferrer; 1302 if (forwardedReferrer) { 1303 restApiData.referrer_url = forwardedReferrer; 1304 } 1305 1306 // Variant A: send the actual page URL where the event happened 1307 // so the server sets event_source_url to this page, not the 1308 // REST endpoint. PHP applies enable_remove_source_url_params. 1309 restApiData.event_source_url = data.url || window.location.href; 1310 1311 // Try to send using sendBeacon first (if enabled) 1312 if (options.useSendBeacon && navigator.sendBeacon) { 1313 try { 1314 const formData = new URLSearchParams(); 1315 for (const [key, value] of Object.entries(restApiData)) { 1316 formData.append(key, value); 1317 } 1318 1319 if (navigator.sendBeacon(restApiUrl, formData)) { 1320 return; 1321 } 1322 } catch (e) { 1323 // sendBeacon failed, continue to fetch 1324 } 1325 } 1326 1327 // Try to send using fetch with REST API 1328 if (window.fetch) { 1329 const headers = { 1330 'Content-Type': 'application/json' 1331 }; 1332 1333 fetch(restApiUrl, { 1334 method: 'POST', 1335 headers: headers, 1336 body: JSON.stringify(restApiData) 1337 }) 1338 .then(response => { 1339 if (!response.ok) {
1340 throw new Error(platform + ' REST API request failed: ' + response.status); 1341 } 1342 }) 1343 .catch(error => { 1344 // Fallback to AJAX if REST API fails 1345 if (options.debug) { 1346 console.log('PYS: ' + platform + ' REST API failed, falling back to AJAX:', error); 1347 } 1348 this.sendAjaxFallback(data); 1349 }); 1350 } else { 1351 // Fallback to AJAX if fetch is not supported 1352 this.sendAjaxFallback(data); 1353 } 1354 }, 1355 1356 1357 1358 // Fallback AJAX method 1359 sendAjaxFallback: function (data) { 1360 jQuery.ajax({ 1361 type: 'POST', 1362 url: options.ajaxUrl, 1363 data: data, 1364 headers: { 1365 'Cache-Control': 'no-cache' 1366 }, 1367 success: function () { 1368 }, 1369 }); 1370 }, 1371 // clone object 1372 clone: function(obj) { 1373 var copy; 1374 1375 // Handle the 3 simple types, and null or undefined 1376 if (null == obj || "object" != typeof obj) return obj; 1377 1378 // Handle Date 1379 if (obj instanceof Date) { 1380 copy = new Date(); 1381 copy.setTime(obj.getTime()); 1382 return copy; 1383 } 1384 1385 // Handle Array 1386 if (obj instanceof Array) { 1387 copy = []; 1388 for (var i = 0, len = obj.length; i < len; i++) { 1389 if("function" == typeof obj[i]) { 1390 continue; 1391 } 1392 copy[i] = Utils.clone(obj[i]); 1393 } 1394 return copy; 1395 } 1396 1397 // Handle Object 1398 if (obj instanceof Object) { 1399 copy = {}; 1400 for (var attr in obj) { 1401 if (obj.hasOwnProperty(attr)) { 1402 if("function" == typeof obj[attr]) { 1403 continue; 1404 } 1405 copy[attr] = Utils.clone(obj[attr]); 1406 } 1407 } 1408 return copy; 1409 } 1410 1411 return obj; 1412 }, 1413 1414 // Returns array of elements with given tag name 1415 getTagsAsArray: function (tag) { 1416 return [].slice.call(document.getElementsByTagName(tag)); 1417 }, 1418
1419 getRequestParams: function () { 1420 return []; 1421 }, 1422 1423 /** 1424 * CUSTOM EVENTS 1425 */ 1426 1427 setupMouseOverClickEvents: function (eventId, triggers) { 1428 1429 // Non-default binding used to avoid situations when some code in external js 1430 // stopping events propagation, eg. returns false, and our handler will never called 1431 document.addEventListener('mouseover', function(event) { 1432 var matchedElements = Array.from(document.querySelectorAll(triggers)); 1433 var clickedElement = event.target; 1434 var closestMatch = clickedElement.closest(triggers); 1435 if (matchedElements.includes(clickedElement) || closestMatch){ 1436 if (event.target.classList.contains('pys-mouse-over-' + eventId)) { 1437 return true; 1438 } else { 1439 event.target.classList.add('pys-mouse-over-' + eventId); 1440 } 1441 1442 Utils.fireTriggerEvent(eventId); 1443 } 1444 }); 1445 1446 }, 1447 1448 setupCSSClickEvents: function (eventId, triggers) { 1449 // Non-default binding used to avoid situations when some code in external js 1450 // stopping events propagation, eg. returns false, and our handler will never called 1451 document.addEventListener('click', function(event) { 1452 let matchedElements = Array.from(document.querySelectorAll(triggers)), 1453 clickedElement = event.target, 1454 closestMatch = clickedElement.closest(triggers); 1455 1456 if (matchedElements.includes(clickedElement) || closestMatch){ 1457 Utils.fireTriggerEvent(eventId); 1458 } 1459 }, true); 1460 }, 1461 1462 setupURLClickEvents: function () { 1463 1464 if( !options.triggerEventTypes.hasOwnProperty('url_click') ) { 1465 return; 1466 } 1467 // Non-default binding used to avoid situations when some code in external js 1468 // stopping events propagation, eg. returns false, and our handler will never called 1469 $('a[data-pys-event-id]').onFirst('click', function (evt) { 1470 1471 $(this).attr('data-pys-event-id').split(',').forEach(function (eventId) { 1472 1473 eventId = parseInt(eventId); 1474 1475 if (isNaN(eventId) === false) { 1476 Utils.fireTriggerEvent(eventId); 1477 } 1478 1479 }); 1480 1481 }); 1482 1483 1484 }, 1485 1486 setupScrollPosEvents: function (eventId, triggers) { 1487 1488 var scrollPosThresholds = {}, 1489 docHeight = $(document).height() - $(window).height(); 1490 1491 // convert % to absolute positions 1492 $.each(triggers, function (index, scrollPos) { 1493 1494 // convert % to pixels 1495 scrollPos = docHeight * scrollPos / 100; 1496 scrollPos = Math.round(scrollPos); 1497 1498 scrollPosThresholds[scrollPos] = eventId; 1499 1500 }); 1501 1502 $(document).on("scroll",function () { 1503 1504 var scrollPos = $(window).scrollTop(); 1505 1506 $.each(scrollPosThresholds, function (threshold, eventId) { 1507 1508 // position has not reached yes 1509 if (scrollPos <= threshold) { 1510 return true; 1511 } 1512 1513 // fire event only once 1514 if (eventId === null) { 1515 return true; 1516 } else { 1517 scrollPosThresholds[threshold] = null; 1518 } 1519 1520 Utils.fireTriggerEvent(eventId); 1521 1522 }); 1523 1524 }); 1525 1526 1527 }, 1528 setupCommentEvents : function (eventId,triggers) { 1529 $('form.comment-form').on("submit",function () { 1530 Utils.fireTriggerEvent(eventId); 1531 }); 1532 }, 1533 1534 /** 1535 * Events 1536 */ 1537 1538 fireTriggerEvent: function (eventId) { 1539 1540 if (!options.triggerEvents.hasOwnProperty(eventId)) { 1541 return; 1542 } 1543 1544 var event = {}; 1545 var events = options.triggerEvents[eventId]; 1546 1547 if (events.hasOwnProperty('facebook')) { 1548 event = Utils.getFormFilledData(events.facebook); 1549 Facebook.fireEvent(event.name, event); 1550 } 1551 1552 if (events.hasOwnProperty('ga')) { 1553 event = Utils.getFormFilledData(events.ga); 1554 Analytics.fireEvent(event.name, event); 1555 } 1556 1557 if (events.hasOwnProperty('pinterest')) { 1558 event = Utils.getFormFilledData(events.pinterest); 1559 Pinterest.fireEvent(event.name, event); 1560 } 1561 1562 if (events.hasOwnProperty('bing')) { 1563 event = Utils.getFormFilledData(events.bing); 1564 Bing.fireEvent(event.name, event); 1565 } 1566 1567 if (events.hasOwnProperty('reddit')) { 1568 event = Utils.getFormFilledData(events.reddit); 1569 Reddit.fireEvent(event.name, event); 1570 } 1571
1572 if (events.hasOwnProperty('openai')) { 1573 event = Utils.getFormFilledData(events.openai); 1574 OpenAI.fireEvent(event.name, event); 1575 } 1576 1577 if (events.hasOwnProperty('gtm')) { 1578 event = Utils.getFormFilledData(events.gtm); 1579 GTM.fireEvent(event.name, event); 1580 } 1581 }, 1582 1583 fireStaticEvents: function (pixel) { 1584 1585 Utils.refreshAddonPixels(); 1586 1587 if (options.staticEvents.hasOwnProperty(pixel)) { 1588 1589 $.each(options.staticEvents[pixel], function (eventName, events) { 1590 $.each(events, function (index, eventData) { 1591 1592 eventData.fired = eventData.fired || false; 1593 1594 if (!eventData.fired) { 1595 eventData = Utils.getFormFilledData(eventData); 1596 var fired = false; 1597 1598 // fire event 1599 if ('facebook' === pixel) { 1600 fired = Facebook.fireEvent(eventData.name, eventData); 1601 } else if ('ga' === pixel) { 1602 fired = Analytics.fireEvent(eventData.name, eventData); 1603 } else if ('pinterest' === pixel) { 1604 fired = Pinterest.fireEvent(eventData.name, eventData); 1605 } else if ('bing' === pixel) { 1606 fired = Bing.fireEvent(eventData.name, eventData); 1607 } else if ('gtm' === pixel) { 1608 fired = GTM.fireEvent(eventData.name, eventData); 1609 } else if ('reddit' === pixel) { 1610 fired = Reddit.fireEvent(eventData.name, eventData); 1611 } else if ('openai' === pixel) { 1612 fired = OpenAI.fireEvent(eventData.name, eventData); 1613 } 1614 1615 // prevent event double event firing 1616 eventData.fired = fired; 1617 1618 } 1619 1620 }); 1621 }); 1622 1623 } 1624 1625 }, 1626 1627 /** 1628 * Load tag's JS 1629 * 1630 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/ 1631 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dims-mets 1632 */ 1633 loadGoogleTag: function (id) { 1634 1635 if (!gtag_loaded) { 1636 let dataLayerName = this.resolveGaDataLayerName(); 1637 this.dataLayerName = dataLayerName; 1638 (function (window, document, src) { 1639 var a = document.createElement('script'), 1640 m = document.getElementsByTagName('script')[0]; 1641 a.async = 1; 1642 a.src = src; 1643 m.parentNode.insertBefore(a, m); 1644 })(window, document, '//www.googletagmanager.com/gtag/js?id=' + id+'&l='+this.dataLayerName); 1645 1646 window[dataLayerName] = window[dataLayerName] || []; 1647 // Published only so custom client code and third-party snippets 1648 // have a gtag() to call. PYS itself must never route through it: 1649 // the `||` means an earlier definition (Site Kit, a theme, the 1650 // GTM bootstrap) keeps ownership and would point at its own data 1651 // layer. Use Utils.gtagPush() instead. 1652 window.gtag = window.gtag || function gtag() { 1653 window[dataLayerName].push(arguments); 1654 }; 1655 1656 if ( options.google_consent_mode ) { 1657 let data = this.resolveConsentValues(); 1658 1659 // Normally this data layer is ours alone and still empty, so a 1660 // `default` is what belongs here. But the GA and GTM scopes 1661 // collapse into one layer when their names resolve to the same 1662 // value (gtm_dataLayer_name left at 'dataLayerPYS', or the GA 1663 // layer set to "Disable name transformation"), and then the GTM 1664 // <head> snippet has already written a strict `denied` default 1665 // here. Repeating `default` would leave GA denied; `update` is 1666 // the documented transition once a default exists. 1667 this.pushConsent( dataLayerName, this.layerHasConsentDefault( dataLayerName ) ? 'update' : 'default', data ); 1668 } 1669 1670 Utils.gtagPush('js', new Date()); 1671 gtag_loaded = true; 1672 1673 } 1674 1675 }, 1676 1677 /** 1678 * Name of the data layer gtag.js is loaded with, see loadGoogleTag(
1678). 1679 * Pure, so it can be resolved before the tag is loaded. 1680 */ 1681 resolveGaDataLayerName: function () { 1682 if ( options.hasOwnProperty( 'GATags' ) ) { 1683 switch ( options.GATags.ga_datalayer_type ) { 1684 case 'default': 1685 return 'dataLayerPYS'; 1686 case 'custom': 1687 return options.GATags.ga_datalayer_name; 1688 default: 1689 return 'dataLayer'; 1690 } 1691 } 1692 return this.dataLayerName || 'dataLayer'; 1693 }, 1694 1695 /** 1696 * Name of the GTM container's data layer. Mirrors GTM::pys_wp_header_top() 1697 * in modules/google_gtm/gtm.php â the two must agree, or the <head> consent 1698 * default and the footer consent update land in different layers. `??` 1699 * alone is not enough: the option can be saved as an empty string, and 1700 * window[''] is a silent dead end. 1701 */ 1702 resolveGtmDataLayerName: function () { 1703 var name = options.hasOwnProperty( 'gtm' ) ? options.gtm.gtm_dataLayer_name : ''; 1704 return ( typeof name === 'string' && name.length ) ? name : 'dataLayer'; 1705 }, 1706 1707 /** 1708 * Sends a gtag command (js/config/set/event/consent) to the GA data layer. 1709 * 1710 * Deliberately does NOT call window.gtag. That global is created with 1711 * `window.gtag = window.gtag || ...`, so whoever defined it first owns it: 1712 * if a theme, Site Kit or the GTM bootstrap got there first, our config 1713 * and event commands would silently land in their data layer while the 1714 * gtag.js instance we loaded (&l=<GA layer>) never receives a config â 1715 * Tag Assistant then reports "Some hits will only be sent after a config 1716 * command is issued". Pushing the arguments object is exactly what gtag() 1717 * does internally, so the payload is identical. 1718 */ 1719 gtagPush: function () { 1720 var name = this.dataLayerName || this.resolveGaDataLayerName(); 1721 window[ name ] = window[ name ] || []; 1722 window[ name ].push( arguments ); 1723 }, 1724 1725 /** 1726 * Whether a send_to value actually points at a destination. 1727 * 1728 * gtag('event', name, { send_to: '' }) has no destination, so gtag.js 1729 * parks the hit until a matching config command appears. Tag Assistant 1730 * reports that as "Some hits will only be sent after a config command 1731 * is issued via gtag('config')". Callers must skip the event instead. 1732 */ 1733 hasSendToTarget: function (target) { 1734 if (Array.isArray(target)) { 1735 return target.length > 0; 1736 } 1737 return typeof target === 'string' && target !== ''; 1738 }, 1739 1740 /** 1741 * Back-compat alias of the old GA-scope consent sender. Exposed through 1742 * window.pys.Utils, so custom client code may call it. New code uses 1743 * pushConsent() with an explicit data layer name. 1744 */ 1745 loadDefaultConsent: function() { 1746 this.gtagPush.apply( this, arguments ); 1747 }, 1748 1749 /** 1750 * Sends one Consent Mode command ('default' or 'update') to a named data 1751 * layer. The single place that writes consent commands. 1752 * 1753 * The IIFE is not decoration: gtag() pushes its own `arguments` object, 1754 * and gtag.js / GTM read the command by index off that object. Pushing a 1755 * plain array works today but is not what the tag emits, so we reproduce 1756 * the exact shape â same reason gtm.php builds its <head> command this way. 1757 */ 1758 pushConsent: function ( layerName, command, values ) { 1759 if ( !layerName ) { 1760 return; 1761 } 1762 window[ layerName ] = window[ layerName ] || []; 1763 ( function () { 1764 window[ layerName ].push( arguments ); 1765 } )( 'consent', command, values ); 1766 }, 1767 1768 /** 1769 * Whether a Consent Mode `default` has already been written to this data 1770 * layer â by our own <head> snippet, by a consent plugin, or by a third 1771 * party (Site Kit, a theme). Decides `default` vs `update`: Consent Mode 1772 * documents exactly one `default` per layer, and a repeated one is not a 1773 * defined transition. Consent Magic reads the layer the same way 1774 * (cs-public.js, CS_Consent_Mode.setDefaultConsent). 1775 */ 1776 layerHasConsentDefault: function ( layerName ) { 1777 var layer = window[ layerName ]; 1778 if ( !layer || typeof layer.length !== 'number' ) { 1779 return false;
1780 } 1781 for ( var i = 0; i < layer.length; i++ ) { 1782 var item = layer[ i ]; 1783 // Entries pushed by other integrations are plain objects; indexing 1784 // them just yields undefined. 1785 if ( item && item[ 0 ] === 'consent' && item[ 1 ] === 'default' ) { 1786 return true; 1787 } 1788 } 1789 return false; 1790 }, 1791 1792 /** 1793 * The four Consent Mode signals as PYS currently resolves them. `enabled` 1794 * is false when consent mode is off for that signal, in which case Google's 1795 * absence of a value means granted anyway â say so explicitly. 1796 */ 1797 resolveConsentValues: function () { 1798 return { 1799 'analytics_storage': options.gdpr.analytics_storage.enabled ? options.gdpr.analytics_storage.value : 'granted', 1800 'ad_storage': options.gdpr.ad_storage.enabled ? options.gdpr.ad_storage.value : 'granted', 1801 'ad_user_data': options.gdpr.ad_user_data.enabled ? options.gdpr.ad_user_data.value : 'granted', 1802 'ad_personalization': options.gdpr.ad_personalization.enabled ? options.gdpr.ad_personalization.value : 'granted', 1803 }; 1804 }, 1805 1806 loadGTMScript: function (id = '') { 1807 // Strip trailing slash to allow both "domain.com" and "domain.com/" inputs. 1808 const domain = ( options.gtm.gtm_container_domain ?? 'www.googletagmanager.com' ).replace( /\/$/, '' ); 1809 const loader = options.gtm.gtm_container_identifier ?? 'gtm'; 1810 const gtm_auth = options.gtm.gtm_auth ?? ''; 1811 const gtm_preview = options.gtm.gtm_preview ?? ''; 1812 const datalayer_name = this.resolveGtmDataLayerName(); 1813 1814 window[ datalayer_name ] = window[ datalayer_name ] || []; 1815 window.gtag = window.gtag || function gtag() { 1816 window[ datalayer_name ].push( arguments ); 1817 }; 1818 1819 // Send the consent state synchronously before the GTM script tag is 1820 // inserted, so it is in the dataLayer before GTM processes any events. 1821 this.pushGTMConsentState( datalayer_name ); 1822 1823 (function(w, d, s, l, i) { 1824 w[l] = w[l] || []; 1825 w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); 1826 const f = d.getElementsByTagName(s)[0]; 1827 const j = d.createElement(s); 1828 const dl = l !== 'dataLayer' ? '&l=' + l : ''; 1829 j.async = true; 1830 1831 // Detect domain format: bare hostname (legacy) vs full URL / relative path (new). 1832 // Legacy "www.googletagmanager.com" â prepend https:// and append .js. 1833 // New format "/metrics" or "https://custom.com" â use as-is. 1834 // Identifier with extension (e.g. "gtm.js") â file: domain/gtm.js?id= 1835 // Identifier without extension (e.g. "g4h25o") â path: domain/g4h25o/?id= 1836 const isNewFormat = domain.startsWith( 'http://' ) 1837 || domain.startsWith( 'https://' ) 1838 || domain.startsWith( '/' ); 1839 1840 if ( !isNewFormat ) { 1841 j.src = 'https://' + domain + '/' + loader + '.js?id=' + i + dl; 1842 } else { 1843 const hasExtension = loader.includes( '.' ); 1844 j.src = domain + '/' + loader + ( hasExtension ? '?' : '/?' ) + 'id=' + i + dl; 1845 } 1846 1847 if (gtm_auth && gtm_preview) { 1848 j.src += '>m_auth=' + gtm_auth + '>m_preview=' + gtm_preview + '>m_cookies_win=x'; 1849 } 1850 f.parentNode.insertBefore(j, f); 1851 })(window, document, 'script', datalayer_name, id); 1852 1853 }, 1854 1855 /** 1856 * Back-compat aliases. Both are reachable as window.pys.Utils.* and may be 1857 * called by custom client code, so they keep working; internally everything 1858 * goes through pushConsent() / pushGTMConsentState() with an explicit layer 1859 * name instead of the implicit this.GTMdataLayerName state. 1860 */ 1861 loadDefaultGTMConsent: function() { 1862 window[ this.GTMdataLayerName ] = window[ this.GTMdataLayerName ] || []; 1863 window[ this.GTMdataLayerName ].push( arguments ); 1864 }, 1865 pushGTMConsentDefaults: function( datalayer_name ) { 1866 this.pushGTMConsentState( datalayer_name ); 1867 }, 1868 1869 /** 1870 * Sends the GTM container's consent state, once per page load. 1871 * 1872 * Called from loadGTMScript() (PYS loads the container itself) and from 1873 * GTM.loadPixel() when gtm_just_data_layer is on, so the state is sent 1874 * whichever path runs â and only once when both do. 1875 * 1876 * Emits `update`, not a second `default`, whenever a default already sits 1877 * in the layer. modules/google_gtm/gtm.php writes a strict all-denied 1878 * default with wait_for_update:500 in <head>, before the container boots; 1879 * that is correct Consent Mode practice, and it is what makes PYS's own 1880 * prior-consent gate work â this function runs downstream of 1881 * Utils.consentGiven('analytics'), so nothing is granted until consent 1882 * actually exists. What was missing is the other half of the contract: 1883 * the update that resolves the wait_for_update window. A repeated 1884 * `default` is not a defined transition and left GTM-managed tags denied. 1885 */ 1886 pushGTMConsentState: function( datalayer_name ) { 1887 if ( !options.google_consent_mode || gtm_consent_state_pushed ) { 1888 return; 1889 } 1890 1891 // A consent management plugin owns the transition: it issues its own 1892 // update from the user's live choice, while ours carries whatever the 1893 // cookies said when the page was rendered. Consent Magic diffs against 1894 // the last consent values in the layer, so a late push from us either 1895 // suppresses its update or overwrites the choice the user just made. 1896 // The <head> default already carries the CMP's resolved values. 1897 if ( options.consent_mode_cmp_active ) { 1898 return; 1899 } 1900 1901 gtm_consent_state_pushed = true; 1902 // Kept in sync for the loadDefaultGTMConsent() back-compat alias. 1903 this.GTMdataLayerName = datalayer_name; 1904 this.pushConsent( 1905 datalayer_name, 1906 this.layerHasConsentDefault( datalayer_name ) ? 'update' : 'default', 1907 this.resolveConsentValues() 1908 ); 1909 }, 1910 1911 /** 1912 * GDPR 1913 */ 1914 1915 loadPixels: function () { 1916 1917 if (options.gdpr.ajax_enabled && !options.gdpr.consent_magic_integration_enabled) { 1918 1919 // retrieves actual PYS GDPR filters values which allow to avoid cache issues 1920 $.get({ 1921 url: options.ajaxUrl, 1922 dataType: 'json', 1923 data: { 1924 action: 'pys_get_gdpr_filters_values' 1925 }, 1926 success: function (res) { 1927 1928 if (res.success) { 1929 1930 options.gdpr.all_disabled_by_api = res.data.all_disabled_by_api; 1931 options.gdpr.facebook_disabled_by_api = res.data.facebook_disabled_by_api; 1932 options.gdpr.analytics_disabled_by_api = res.data.analytics_disabled_by_api; 1933 options.gdpr.google_ads_disabled_by_api = res.data.google_ads_disabled_by_api; 1934 options.gdpr.pinterest_disabled_by_api = res.data.pinterest_disabled_by_api; 1935 options.gdpr.bing_disabled_by_api = res.data.bing_disabled_by_api; 1936 options.gdpr.reddit_disabled_by_api = res.data.reddit_disabled_by_api; 1937 options.gdpr.openai_disabled_by_api = res.data.openai_disabled_by_api; 1938 1939 options.cookie.externalID_disabled_by_api = res.data.externalID_disabled_by_api; 1940 options.cookie.disabled_all_cookie = res.data.disabled_all_cookie; 1941 options.cookie.disabled_advanced_form_data_cookie = res.data.disabled_advanced_form_data_cookie; 1942 options.cookie.disabled_landing_page_cookie = res.data.disabled_landing_page_cookie; 1943 options.cookie.disabled_first_visit_cookie = res.data.disabled_first_visit_cookie; 1944 options.cookie.disabled_trafficsource_cookie = res.data.disabled_trafficsource_cookie; 1945 options.cookie.disabled_utmTerms_cookie = res.data.disabled_utmTerms_cookie; 1946 options.cookie.disabled_utmId_cookie = res.data.disabled_utmId_cookie; 1947 } 1948 1949 loadPixels(); 1950 1951 } 1952 }); 1953 1954 } else { 1955 loadPixels(); 1956 } 1957 1958 }, 1959 1960 consentGiven: function (pixel) { 1961 1962 /** 1963 * ConsentMagic 1964 */ 1965 if ( options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined" ) { 1966 1967 let test_prefix = CS_Data.test_prefix; 1968 if ( 1969 ( Utils.csModeEnabled( CS_Data.cs_google_consent_mode_enabled ) && ( pixel == 'analytics' || pixel == 'google_ads' ) ) 1970 || ( Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) && pixel == 'facebook' ) 1971 || ( Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) && pixel == 'reddit' ) 1972 || ( Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) && pixel == 'openai' ) 1973 || ( Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) && pixel == 'bing' ) 1974 ) { 1975 if ( Number( CS_Data.cs_cache_enabled ) === 0 1976 || ( Utils.csModeEnabled( CS_Data.cs_cache_enabled ) && window.CS_Cache && window.CS_Cache.check_status ) ) { 1977 return true; 1978 } else { 1979 return false;
1980 } 1981 } 1982 1983 if( pixel == 'facebook' && ( CS_Data.cs_script_cat.facebook == 0 || CS_Data.cs_script_cat.facebook == CS_Data.cs_necessary_cat_id ) ) { 1984 return true; 1985 } else if( pixel == 'bing' && ( CS_Data.cs_script_cat.bing == 0 || CS_Data.cs_script_cat.bing == CS_Data.cs_necessary_cat_id ) ) { 1986 return true; 1987 } else if( pixel == 'analytics' && ( CS_Data.cs_script_cat.analytics == 0 || CS_Data.cs_script_cat.analytics == CS_Data.cs_necessary_cat_id ) ) { 1988 return true; 1989 } else if( pixel == 'google_ads' && ( CS_Data.cs_script_cat.gads == 0 || CS_Data.cs_script_cat.gads == CS_Data.cs_necessary_cat_id ) ) { 1990 return true; 1991 } else if( pixel == 'pinterest' && ( CS_Data.cs_script_cat.pinterest == 0 || CS_Data.cs_script_cat.pinterest == CS_Data.cs_necessary_cat_id ) ) { 1992 return true; 1993 } else if( pixel == 'tiktok' && ( CS_Data.cs_script_cat.tiktok == 0 || CS_Data.cs_script_cat.tiktok == CS_Data.cs_necessary_cat_id ) ) { 1994 return true; 1995 } else if( pixel == 'reddit' && ( CS_Data.cs_script_cat?.reddit == 0 || CS_Data.cs_script_cat?.reddit == CS_Data.cs_necessary_cat_id ) ) { 1996 return true; 1997 } else if( pixel == 'openai' && ( CS_Data.cs_script_cat?.openai == 0 || CS_Data.cs_script_cat?.openai == CS_Data.cs_necessary_cat_id ) ) { 1998 return true; 1999 } 2000 2001 let substring = "cs_enabled_cookie_term", 2002 theCookies = document.cookie.split( ';' ); 2003 2004 for ( let i = 1; i <= theCookies.length; i++ ) { 2005 if ( theCookies[ i - 1 ].indexOf( substring ) !== -1 ) { 2006 let categoryCookie = theCookies[ i - 1 ].replace( 'cs_enabled_cookie_term' + test_prefix + '_', '' ); 2007 categoryCookie = Number( categoryCookie.replace( /\D+/g, "" ) ); 2008 let cs_cookie_val = Cookies.get( 'cs_enabled_cookie_term' + test_prefix + '_' + categoryCookie ); 2009 2010 if ( categoryCookie === CS_Data.cs_script_cat.facebook && pixel == 'facebook' ) { 2011 return cs_cookie_val == 'yes'; 2012 } else if ( categoryCookie === CS_Data.cs_script_cat.bing && pixel == 'bing' ) { 2013 return cs_cookie_val == 'yes'; 2014 } else if ( categoryCookie === CS_Data.cs_script_cat.analytics && pixel == 'analytics' ) { 2015 return cs_cookie_val == 'yes'; 2016 } else if ( categoryCookie === CS_Data.cs_script_cat.gads && pixel == 'google_ads' ) { 2017 return cs_cookie_val == 'yes'; 2018 } else if ( categoryCookie === CS_Data.cs_script_cat.pinterest && pixel == 'pinterest' ) { 2019 return cs_cookie_val == 'yes'; 2020 } else if ( categoryCookie === CS_Data.cs_script_cat.tiktok && pixel == 'tiktok' ) { 2021 return cs_cookie_val == 'yes'; 2022 } else if ( categoryCookie === CS_Data.cs_script_cat?.reddit && pixel == 'reddit' ) { 2023 return cs_cookie_val == 'yes'; 2024 } else if ( categoryCookie === CS_Data.cs_script_cat?.openai && pixel == 'openai' ) { 2025 return cs_cookie_val == 'yes'; 2026 } 2027 } 2028 } 2029 2030 return false; 2031 } 2032 2033 /** 2034 * Real Cookie Banner 2035 */ 2036 if(options.gdpr.real_cookie_banner_integration_enabled) { 2037 var consentApi = window.consentApi; 2038 if (consentApi) { 2039 switch (pixel) { 2040 case "analytics": 2041 return consentApi.consentSync("http", "_ga", "*").cookieOptIn; 2042 case "facebook": 2043 return consentApi.consentSync("http", "_fbp", "*").cookieOptIn; 2044 case "pinterest": 2045 return consentApi.consentSync("http", "_pinterest_sess", ".pinterest.com").cookieOptIn; 2046 default: 2047 return true; 2048 } 2049 } 2050 } 2051 2052 /** 2053 * Cookiebot 2054 */ 2055 if (options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined') { 2056 2057 var cookiebot_consent_category = options.gdpr['cookiebot_' + pixel + '_consent_category']; 2058 2059 if (options.gdpr[pixel + '_prior_consent_enabled']) { 2060 if (Cookiebot.consented === true || Cookiebot.consent[cookiebot_consent_category]) { 2061 return true; 2062 } 2063 } else { 2064 if (Cookiebot.consent[cookiebot_consent_category]) { 2065 return true; 2066 } 2067 } 2068 2069 return false;
2070 2071 } 2072 2073 /** 2074 * Cookie Notice 2075 */ 2076 if (options.gdpr.cookie_notice_integration_enabled && typeof cnArgs !== 'undefined') { 2077 2078 var cn_cookie = Cookies.get(cnArgs.cookieName); 2079 2080 if (options.gdpr[pixel + '_prior_consent_enabled']) { 2081 if (typeof cn_cookie === 'undefined' || cn_cookie === 'true') { 2082 return true; 2083 } 2084 } else { 2085 if (cn_cookie === 'true') { 2086 return true; 2087 } 2088 } 2089 2090 return false; 2091 2092 } 2093 2094 /** 2095 * Cookie Law Info 2096 */ 2097 if (options.gdpr.cookie_law_info_integration_enabled) { 2098 var cli_cookie = Cookies.get('cookieyes-consent') ?? Cookies.get('wt_consent') ?? Cookies.get('viewed_cookie_policy'); 2099 if (options.gdpr[pixel + '_prior_consent_enabled']) { 2100 if (typeof cli_cookie === 'undefined') return true; 2101 if ( 2102 cli_cookie && 2103 (cli_cookie === Cookies.get('cookieyes-consent') || cli_cookie === Cookies.get('wt_consent')) 2104 ) { 2105 if (getCookieYes('analytics') === 'yes') { 2106 return true; 2107 } 2108 } else if (cli_cookie && cli_cookie === Cookies.get('viewed_cookie_policy')) { 2109 if (Cookies.get('viewed_cookie_policy') === 'yes') { 2110 return true; 2111 } 2112 } 2113 } else { 2114 if ( 2115 cli_cookie && 2116 (cli_cookie === Cookies.get('cookieyes-consent') || cli_cookie === Cookies.get('wt_consent')) 2117 ) { 2118 if (getCookieYes('analytics') === 'yes') { 2119 return true; 2120 } 2121 } else if (cli_cookie && cli_cookie === Cookies.get('viewed_cookie_policy')) { 2122 if (Cookies.get('viewed_cookie_policy') === 'yes') { 2123 return true; 2124 } 2125 } 2126 } 2127 return false; 2128 } 2129 2130 return true; 2131 2132 }, 2133 2134 setupGdprCallbacks: function () { 2135 /** 2136 * ConsentMagic 2137 */ 2138 if (options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined") { 2139 let test_prefix = CS_Data.test_prefix, 2140 cs_refresh_after_consent = false, 2141 substring = "cs_enabled_cookie_term"; 2142 2143 if (CS_Data.cs_refresh_after_consent == 1) { 2144 cs_refresh_after_consent = CS_Data.cs_refresh_after_consent; 2145 } 2146 2147 let consent_actions = function () { 2148 let theCookies = document.cookie.split( ';' ); 2149 2150 let consent = { 2151 facebook: true, 2152 ga: true, 2153 google_ads: true, 2154 tiktok: true, 2155 bing: true, 2156 pinterest: true, 2157 gtm: true, 2158 reddit: true, 2159 }; 2160 2161 for (let i = 1 ; i <= theCookies.length; i++) { 2162 if (theCookies[i-1].indexOf(substring) !== -1) { 2163 let categoryCookie = theCookies[i-1].replace('cs_enabled_cookie_term'+test_prefix+'_',''); 2164 categoryCookie = Number(categoryCookie.replace(/\D+/g,"")); 2165 let cs_cookie_val = Cookies.get('cs_enabled_cookie_term'+test_prefix+'_'+categoryCookie); 2166 if(cs_cookie_val == 'yes') { 2167 if ( ( categoryCookie === CS_Data.cs_script_cat.facebook ) || Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) ) { 2168 Facebook.loadPixel(); 2169 } 2170 2171 if ( categoryCookie === CS_Data.cs_script_cat.bing || Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) ) { 2172 Bing.loadPixel(); 2173 } 2174 2175 if (categoryCookie === CS_Data.cs_script_cat.analytics || (typeof CS_Data.cs_google_analytics_consent_mode !== "undefined" && CS_Data.cs_google_analytics_consent_mode == 1)
2175) { 2176 2177 Analytics.loadPixel(); 2178 } 2179 2180 if (categoryCookie === CS_Data.cs_script_cat.pinterest) { 2181 Pinterest.loadPixel(); 2182 } 2183 2184 if ( ( categoryCookie === CS_Data.cs_script_cat.reddit ) || Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) ) { 2185 Reddit.loadPixel(); 2186 } 2187 2188 if ( ( categoryCookie === CS_Data.cs_script_cat.openai ) || Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) ) { 2189 OpenAI.loadPixel(); 2190 } 2191 } else { 2192 if ( ( categoryCookie === CS_Data.cs_script_cat.facebook ) && ! Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) ) { 2193 Facebook.disable(); 2194 consent.facebook = false; 2195 } 2196 2197 if ( categoryCookie === CS_Data.cs_script_cat.bing && ! Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) ) { 2198 Bing.disable(); 2199 consent.bing = false; 2200 } 2201 if (categoryCookie === CS_Data.cs_script_cat.analytics && (typeof CS_Data.cs_google_analytics_consent_mode == "undefined" || CS_Data.cs_google_analytics_consent_mode == 0)) { 2202 Analytics.disable(); 2203 consent.ga = false; 2204 consent.gtm = false; 2205 } 2206 2207 if (categoryCookie === CS_Data.cs_script_cat.pinterest) { 2208 Pinterest.disable(); 2209 consent.pinterest = false; 2210 } 2211 2212 if ( ( categoryCookie === CS_Data.cs_script_cat.reddit ) && ! Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) ) { 2213 Reddit.disable(); 2214 consent.reddit = false; 2215 } 2216 2217 if ( ( categoryCookie === CS_Data.cs_script_cat.openai ) && ! Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) ) { 2218 OpenAI.disable(); 2219 consent.openai = false; 2220 } 2221 2222 if ( categoryCookie === CS_Data.cs_script_cat?.openai ) { 2223 OpenAI.disable(); 2224 consent.openai = false; 2225 } 2226 } 2227 if (Cookies.get('cs_enabled_advanced_matching') == 'yes') { 2228 Facebook.loadPixel(); 2229 } 2230 } 2231 } 2232 Utils.setupGDPRData( consent ); 2233 } 2234 2235 if (!cs_refresh_after_consent) { 2236 consent_actions(); 2237 2238 $(document).on('click','.cs_action_btn',function(e) { 2239 e.preventDefault(); 2240 2241 let consent = { 2242 facebook: true, 2243 ga: true, 2244 bing: true, 2245 pinterest: true, 2246 gtm: true, 2247 reddit: true, 2248 }; 2249 2250 let elm = $(this), 2251 button_action = elm.attr('data-cs_action'); 2252 2253 if(button_action === 'allow_all') { 2254 Facebook.loadPixel(); 2255 Bing.loadPixel(); 2256 Analytics.loadPixel(); 2257 Pinterest.loadPixel(); 2258 Reddit.loadPixel(); 2259 2260 consent.facebook = true; 2261 consent.bing = true; 2262 consent.ga = true; 2263 consent.pinterest = true; 2264 consent.reddit = true; 2265 consent.gtm = true; 2266 2267 Utils.setupGDPRData( consent ); 2268 } else if ( button_action === 'disable_all' ) { 2269 if ( ! Utils.csModeEnabled( CS_Data.cs_meta_ldu_mode ) ) { 2270 Facebook.disable(); 2271 consent.facebook = false;
2272 } 2273 2274 if ( ! Utils.csModeEnabled( CS_Data.cs_bing_consent_mode?.ad_storage?.enabled ) ) { 2275 Bing.disable(); 2276 consent.bing = false; 2277 } 2278 2279 if ( CS_Data.cs_google_analytics_consent_mode == 0 || typeof CS_Data.cs_google_analytics_consent_mode == "undefined" ) { 2280 Analytics.disable(); 2281 consent.ga = false; 2282 consent.gtm = false; 2283 } 2284 2285 if ( ! Utils.csModeEnabled( CS_Data.cs_reddit_ldu_mode ) ) { 2286 Reddit.disable(); 2287 consent.reddit = false; 2288 } 2289 2290 Pinterest.disable(); 2291 consent.pinterest = false; 2292 OpenAI.disable(); 2293 consent.openai = false; 2294 Utils.setupGDPRData( consent ); 2295 } else if ( button_action === 'cs_confirm' ) { 2296 consent_actions(); 2297 } 2298 }); 2299 } 2300 } 2301 /** 2302 * Real Cookie Banner 2303 */ 2304 if(options.gdpr.real_cookie_banner_integration_enabled) { 2305 let consentApi = window.consentApi; 2306 if (consentApi) { 2307 consentApi.consent("http", "_ga", "*") 2308 .then(Analytics.loadPixel.bind(Analytics), Analytics.disable.bind(Analytics)); 2309 consentApi.consent("http", "_fbp", "*") 2310 .then(Facebook.loadPixel.bind(Facebook), Facebook.disable.bind(Facebook)); 2311 consentApi.consent("http", "_pinterest_sess", ".pinterest.com") 2312 .then(Pinterest.loadPixel.bind(Pinterest), Pinterest.disable.bind(Pinterest)); 2313 consentApi.consent("http", "_uetsid", "*") 2314 .then(Bing.loadPixel.bind(Bing), Bing.disable.bind(Bing)); 2315 2316 let consent = { 2317 facebook: true, 2318 ga: true, 2319 bing: true, 2320 pinterest: true, 2321 gtm: true, 2322 }; 2323 2324 if (!consentApi.consentSync("http", "_ga", "*").cookieOptIn) { 2325 consent.ga = false; 2326 consent.gtm = false; 2327 } 2328 if (!consentApi.consentSync("http", "_fbp", "*").cookieOptIn) { 2329 consent.facebook = false; 2330 } 2331 if (!consentApi.consentSync("http", "_pinterest_sess", ".pinterest.com").cookieOptIn) { 2332 consent.pinterest = false; 2333 } 2334 if (!consentApi.consentSync("http", "_uetsid", "*").cookieOptIn) { 2335 consent.bing = false; 2336 } 2337 Utils.setupGDPRData( consent ); 2338 } 2339 } 2340 /** 2341 * Cookiebot 2342 */ 2343 if (options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined') { 2344 2345 window.addEventListener("CookiebotOnConsentReady", function() { 2346 2347 let consent = { 2348 facebook: true, 2349 ga: true, 2350 bing: true, 2351 pinterest: true, 2352 gtm: true, 2353 }; 2354 2355 Utils.manageCookies(); 2356 if (Cookiebot.consent.marketing) { 2357 Facebook.loadPixel(); 2358 Bing.loadPixel(); 2359 Pinterest.loadPixel(); 2360 2361 consent.facebook = true; 2362 consent.bing = true; 2363 consent.pinterest = true; 2364 } 2365 if (Cookiebot.consent.statistics) { 2366 Analytics.loadPixel(); 2367 2368 consent.ga = true; 2369 consent.gtm = true; 2370 } 2371 if (!Cookiebot.consent.marketing) { 2372 Facebook.disable(); 2373 Pinterest.disable(); 2374 Bing.disable(); 2375 2376 consent.facebook = false;
2377 consent.bing = false; 2378 consent.pinterest = false; 2379 } 2380 if (!Cookiebot.consent.statistics) { 2381 Analytics.disable(); 2382 2383 consent.ga = false; 2384 consent.gtm = false; 2385 } 2386 2387 Utils.setupGDPRData( consent ); 2388 }); 2389 2390 } 2391 2392 /** 2393 * Cookie Notice 2394 */ 2395 if (options.gdpr.cookie_notice_integration_enabled) { 2396 2397 $(document).onFirst('click', '.cn-set-cookie', function () { 2398 2399 let consent = {}; 2400 2401 if ($(this).data('cookie-set') === 'accept') { 2402 Facebook.loadPixel(); 2403 Analytics.loadPixel(); 2404 Pinterest.loadPixel(); 2405 Bing.loadPixel(); 2406 2407 consent = { 2408 facebook: true, 2409 ga: true, 2410 bing: true, 2411 pinterest: true, 2412 gtm: true, 2413 }; 2414 } else { 2415 Facebook.disable(); 2416 Analytics.disable(); 2417 Pinterest.disable(); 2418 Bing.disable(); 2419 2420 consent = { 2421 facebook: false, 2422 ga: false, 2423 bing: false, 2424 pinterest: false, 2425 gtm: false, 2426 }; 2427 } 2428 2429 Utils.setupGDPRData( consent ); 2430 }); 2431 2432 $(document).onFirst('click', '.cn-revoke-cookie', function () { 2433 Facebook.disable(); 2434 Analytics.disable(); 2435 Pinterest.disable(); 2436 Bing.disable(); 2437 2438 let consent = { 2439 facebook: false, 2440 ga: false, 2441 bing: false, 2442 pinterest: false, 2443 gtm: false, 2444 }; 2445 Utils.setupGDPRData( consent ); 2446 2447 }); 2448 2449 } 2450 2451 /** 2452 * Cookie Law Info 2453 */ 2454 if (options.gdpr.cookie_law_info_integration_enabled) { 2455 2456 $(document).onFirst('click', '#wt-cli-accept-all-btn,#cookie_action_close_header, .cky-btn-accept', function () { 2457 setTimeout(function (){ 2458 let cli_cookie = Cookies.get('cookieyes-consent') ?? Cookies.get('wt_consent') ?? Cookies.get('viewed_cookie_policy'); 2459 if (typeof cli_cookie !== 'undefined') { 2460 if (cli_cookie === Cookies.get('cookieyes-consent') || cli_cookie === Cookies.get('wt_consent')) { 2461 if (getCookieYes('analytics') === 'yes') { 2462 Utils.manageCookies(); 2463 } 2464 } else if (cli_cookie === Cookies.get('viewed_cookie_policy') && cli_cookie == 'yes') { 2465 Utils.manageCookies(); 2466 } 2467 } 2468 },1000) 2469 Facebook.loadPixel(); 2470 Analytics.loadPixel(); 2471 Pinterest.loadPixel(); 2472 Bing.loadPixel(); 2473 2474 let consent = { 2475 facebook: true, 2476 ga: true, 2477 bing: true, 2478 pinterest: true, 2479 gtm: true, 2480 }; 2481 Utils.setupGDPRData( consent ); 2482 }); 2483 2484 $(document).onFirst('click', '#cookie_action_close_header_reject, .cky-btn-reject', function () { 2485 Facebook.disable(); 2486 Analytics.disable(); 2487 Pinterest.disable(); 2488 Bing.disable(); 2489 2490 let consent = {
2491 facebook: false, 2492 ga: false, 2493 bing: false, 2494 pinterest: false, 2495 gtm: false, 2496 }; 2497 Utils.setupGDPRData( consent ); 2498 }); 2499 2500 } 2501 2502 }, 2503 2504 setupGDPRData: function ( consent ) { 2505 consent = window.btoa( JSON.stringify( consent ) ); 2506 Cookies.set( 'pys_consent', consent, { expires: 365, path: '/', domain: cookieDomain() } ); 2507 }, 2508 2509 /** 2510 * DOWNLOAD DOCS 2511 */ 2512 2513 getLinkExtension: function (link) { 2514 2515 // Remove anchor, query string and everything before last slash 2516 link = link.substring(0, (link.indexOf("#") === -1) ? link.length : link.indexOf("#")); 2517 link = link.substring(0, (link.indexOf("?") === -1) ? link.length : link.indexOf("?")); 2518 link = link.substring(link.lastIndexOf("/") + 1, link.length); 2519 2520 // If there's a period left in the URL, then there's a extension 2521 if (link.length > 0 && link.indexOf('.') !== -1) { 2522 link = link.substring(link.indexOf(".") + 1); // Remove everything but what's after the first period 2523 return link; 2524 } else { 2525 return ""; 2526 } 2527 }, 2528 2529 getLinkFilename: function (link) { 2530 2531 // Remove anchor, query string and everything before last slash 2532 link = link.substring(0, (link.indexOf("#") === -1) ? link.length : link.indexOf("#")); 2533 link = link.substring(0, (link.indexOf("?") === -1) ? link.length : link.indexOf("?")); 2534 link = link.substring(link.lastIndexOf("/") + 1, link.length); 2535 2536 // If there's a period left in the URL, then there's a extension 2537 if (link.length > 0 && link.indexOf('.') !== -1) { 2538 return link; 2539 } else { 2540 return ""; 2541 } 2542 }, 2543 /** 2544 * Enrich 2545 */ 2546 isCheckoutPage: function () { 2547 return $('body').hasClass('woocommerce-checkout') || document.querySelector('.woocommerce-checkout') || 2548 $('body').hasClass('edd-checkout'); 2549 }, 2550 addCheckoutFields : function() { 2551 var utm = ""; 2552 var utms = getUTMs() 2553 2554 $.each(utmTerms, function (index, name) { 2555 if(index > 0) { 2556 utm+="|"; 2557 } 2558 utm+=name+":"+utms[name]; 2559 }); 2560 var utmIdList = ""; 2561 var utmsIds = getUTMId() 2562 $.each(utmId, function (index, name) { 2563 if(index > 0) { 2564 utmIdList+="|"; 2565 } 2566 utmIdList+=name+":"+utmsIds[name]; 2567 }); 2568 var utmIdListLast = ""; 2569 var utmsIdsLast = getUTMId(true) 2570 $.each(utmId, function (index, name) { 2571 if(index > 0) { 2572 utmIdListLast+="|"; 2573 } 2574 utmIdListLast+=name+":"+utmsIdsLast[name]; 2575 }); 2576 2577 2578 var utmLast = ""; 2579 var utmsLast = getUTMs(true) 2580 $.each(utmTerms, function (index, name) { 2581 if(index > 0) { 2582 utmLast+="|"; 2583 } 2584 utmLast+=name+":"+utmsLast[name]; 2585 }); 2586 2587 var dateTime = getDateTime(); 2588 // No current-URL fallback here: an empty field lets 2589 // EnrichOrder::getRequestValue() use its own server-side default, 2590 // which is the visit data from the PHP session â more reliable than 2591 // the checkout URL. The checkout page is never HTML-cached, so 2592 // reading the session there is safe. 2593 var landing = getLandingPageValue(false); 2594 var lastLanding = getLandingPageValue(false); 2595 var trafic = getTrafficSourceValue(); 2596 var lastTrafic = getTrafficSourceValue(); 2597 2598 var $form = null; 2599 if($('body').hasClass('woocommerce-checkout')) { 2600 $form = $("form.woocommerce-checkout"); 2601 } else { 2602 $form = $("#edd_purchase_form"); 2603 } 2604 var inputs = {'pys_utm':utm, 2605 'pys_utm_id':utmIdList, 2606 'pys_browser_time':dateTime.join("|"), 2607 'pys_landing':landing, 2608 'pys_source':trafic, 2609 'pys_order_type': $(".wcf-optin-form").length > 0 ? "wcf-optin" : "normal", 2610 2611 'last_pys_landing':lastLanding, 2612 'last_pys_source':lastTrafic, 2613 'last_pys_utm':utmLast, 2614 'last_pys_utm_id':utmIdListLast, 2615 } 2616
2617 Object.keys(inputs).forEach(function(key,index) { 2618 $form.append("<input type='hidden' name='"+key+"' value='"+inputs[key]+"' /> "); 2619 }); 2620 2621 2622 }, 2623 getAdvancedFormData: function () { 2624 let dataStr = Cookies.get("pys_advanced_form_data"); 2625 if(dataStr === undefined) { 2626 return {'first_name':"",'last_name':"",'email':"",'phone':""}; 2627 } else { 2628 return JSON.parse(dataStr); 2629 } 2630 }, 2631 2632 /** 2633 * Advanced Matching that the server is no longer allowed to render. 2634 * 2635 * Page HTML can be served out of a shared cache, so the server 2636 * leaves the visitor's own email, phone and name out of pysOptions. 2637 * The values are on this device in pys_advanced_form_data, so the 2638 * browser puts them back just before each pixel initialises. 2639 * Normalisation mirrors the PHP side field for field -- if one 2640 * moves, both move. 2641 */ 2642 AdvancedMatching: { 2643 2644 normalizeEmail: function (value) { 2645 if (typeof value !== 'string') { 2646 return ''; 2647 } 2648 var email = value.trim().toLowerCase(); 2649 if (email.length === 0 || email.length > 254) { 2650 return ''; 2651 } 2652 return /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/.test(email) ? email : ''; 2653 }, 2654 2655 normalizePhone: function (value) { 2656 if (typeof value !== 'string') { 2657 return ''; 2658 } 2659 var digits = value.replace(/[^0-9]/g, ''); 2660 return digits.length > 0 ? digits : ''; 2661 }, 2662 2663 normalizeName: function (value) { 2664 if (typeof value !== 'string' || value.length > 1024) { 2665 return ''; 2666 } 2667 return value.trim().toLowerCase(); 2668 }, 2669 2670 /** 2671 * The identity this device carries, normalised and possibly 2672 * all-empty. Never throws when the cookie is absent or corrupt. 2673 */ 2674 cookieIdentity: function () { 2675 var data; 2676 try { 2677 data = Utils.getAdvancedFormData() || {}; 2678 } catch (e) { 2679 data = {}; 2680 } 2681 return { 2682 email: this.normalizeEmail(data.email || ''), 2683 phone: this.normalizePhone(data.phone || ''), 2684 first_name: this.normalizeName(data.first_name || ''), 2685 last_name: this.normalizeName(data.last_name || '') 2686 }; 2687 }, 2688 2689 /** 2690 * @param {object} target The pixel's advanced matching object, edited in place. 2691 * @param {object} mapping {targetKey: {field, hash}} 2692 * @param {object} identity Optional; defaults to cookieIdentity(). 2693 * @param {string} persistency Optional; defaults to options.data_persistency. 2694 */ 2695 merge: function (target, mapping, identity, persistency) { 2696 2697 if (!target || !mapping) { 2698 return target; 2699 } 2700 2701 identity = identity || this.cookieIdentity(); 2702 2703 if (typeof persistency === 'undefined') { 2704 persistency = typeof options !== 'undefined' ? options.data_persistency : ''; 2705 } 2706 2707 var keepData = persistency === 'keep_data'; 2708 2709 Object.keys(mapping).forEach(function (targetKey) { 2710 2711 var rule = mapping[targetKey]; 2712 var value = identity[rule.field]; 2713 2714 if (typeof value !== 'string' || value.length === 0) { 2715 return; 2716 } 2717 2718 var hasServerValue = typeof target[targetKey] === 'string' 2719 && target[targetKey].length > 0; 2720 2721 if (hasServerValue && !keepData) { 2722 return; 2723 } 2724 2725 if (rule.hash) { 2726 if (typeof sha256 !== 'function') { 2727 return; 2728 } 2729 target[targetKey] = sha256(value); 2730 } else { 2731 target[targetKey] = value; 2732 } 2733 }); 2734 2735 return target; 2736 } 2737 }, 2738 2739 getFormFilledData: function ( event ) { 2740 // First, resolve static/dynamic parameters 2741 if (event.params && Object.keys(event.params).length > 0) {
2742 Object.entries(event.params).forEach(([key, value]) => { 2743 // Resolve parameter value (static or dynamic) 2744 const resolvedValue = resolveParamValue(value, key); 2745 if (resolvedValue !== null) { 2746 event.params[key] = resolvedValue; 2747 } 2748 else { 2749 delete event.params[key]; 2750 } 2751 }); 2752 } 2753 return event; 2754 } 2755 }; 2756 2757 }(options); 2758 2759 var Facebook = function (options) { 2760 2761 2762 var defaultEventTypes = [ 2763 'PageView', 2764 'ViewContent', 2765 'Search', 2766 'AddToCart', 2767 'AddToWishlist', 2768 'InitiateCheckout', 2769 'AddPaymentInfo', 2770 'Purchase', 2771 'Lead', 2772 2773 'Subscribe', 2774 'CustomizeProduct', 2775 'FindLocation', 2776 'StartTrial', 2777 'SubmitApplication', 2778 'Schedule', 2779 'Contact', 2780 'Donate' 2781 ]; 2782 2783 var initialized = false; 2784 /** 2785 * The `version.subdomainIndex.` prefix Meta's format calls for. 2786 * subdomainIndex is how many labels the cookie's domain has below 2787 * the public suffix (com=0, example.com=1, www.example.com=2), so it 2788 * must track cookieDomain()/probeCookieDomain() rather than a 2789 * hardcoded `fb.1.`. Only new values are affected â an existing 2790 * cookie is returned untouched. 2791 */ 2792 var fbValuePrefix = function (){ 2793 var resolved = cookieDomain(); 2794 var scope = resolved ? String(resolved).replace(/^\./, '') : ''; 2795 if (!scope) { 2796 try { scope = String(location.hostname || ''); } catch (e) { scope = ''; } 2797 } 2798 var dots = scope ? scope.split('.').length - 1 : 0; 2799 return 'fb.' + dots + '.' + Date.now() + '.'; 2800 }; 2801 var genereateFbp = function (){ 2802 return !Cookies.get('_fbp') ? fbValuePrefix()+Math.floor(1000000000 + Math.random() * 9000000000) : Cookies.get('_fbp'); 2803 }; 2804 var genereateFbc = function (){ 2805 return getUrlParameter('fbclid') ? fbValuePrefix()+getUrlParameter('fbclid') : '' 2806 }; 2807 // fire server side event gdpr plugin installed 2808 var isApiDisabled = options.gdpr.all_disabled_by_api || 2809 options.gdpr.facebook_disabled_by_api || 2810 options.gdpr.cookiebot_integration_enabled || 2811 options.gdpr.consent_magic_integration_enabled || 2812 options.gdpr.cookie_notice_integration_enabled || 2813 options.gdpr.cookie_law_info_integration_enabled; 2814 2815 /** 2816 * 2817 * @param allData 2818 * @param params 2819 * @returns {string | null} 2820 */ 2821 function sendFbServerEvent(allData,name,params) { 2822 let eventId = null; 2823 // sendEventId is true when the token-based Conversion API is on OR 2824 // when Meta-Enabled CAPI is on: in both cases Meta receives a second, 2825 // API-side copy of this event and dedupes it by eventID. 2826 // Only the token-based API needs an actual request from us, so the 2827 // hook/AJAX branches below stay gated on serverApiEnabled. 2828 if(options.facebook.sendEventId) { 2829 2830 var isAtcFromHook = options.woo 2831 && options.woo.addToCartCatchMethod === "add_cart_hook"; 2832 2833 if(options.facebook.serverApiEnabled && allData.e_id === "woo_remove_from_cart") {// server event will sended from hook 2834 Facebook.updateEventId(allData.name); 2835 allData.eventID = Facebook.getEventId(allData.name); 2836 } else if(options.facebook.serverApiEnabled && allData.e_id === "woo_add_to_cart_on_button_click" && isAtcFromHook) { 2837 // Server event was already sent from the PHP hook 2838 // (trackWooAddToCartEvent -> FacebookServer()->sendEventsNow). 2839 // Keep the shared eventID PHP put on the event so the browser 2840 // pixel dedupes against that server copy â do NOT re-send here, 2841 // and do NOT regenerate the eventID. 2842 } else { 2843 // send event from server if they was bloc by gdpr or need send with delay 2844 allData.eventID = Utils.generateUniqueId(allData); 2845 2846 if(Cookies.get('_fbp')){ 2847 params._fbp = Cookies.get('_fbp'); 2848 } 2849 if(Cookies.get('_fbc')){ 2850 params._fbc = Cookies.get('_fbc'); 2851 } 2852 2853 if( options.facebook.serverApiEnabled && ( options.ajaxForServerEvent || isApiDisabled ) ){ 2854 var json = { 2855 action: 'pys_api_event', 2856 pixel: 'facebook', 2857 event: name, 2858 event_slug: allData.e_id, 2859 data:params, 2860 ids:options.facebook.pixelIds, 2861 eventID:allData.eventID, 2862 url:window.location.href, 2863 ajax_event:options.ajax_event 2864 }; 2865 if(allData.hasOwnProperty('woo_order')) { 2866 json['woo_order'] = allData.woo_order; 2867 } 2868 if(allData.hasOwnProperty('edd_order')) { 2869 json['edd_order'] = allData.edd_order; 2870 } 2871 if(allData.hasOwnProperty('order_key') && allData.order_key) { 2872 json['order_key'] = allData.order_key; 2873 } 2874 if (pageReferrer) { 2875 json.referrer_url = pageReferrer; 2876 } 2877 2878 if (allData.e_id === "automatic_event_internal_link" || allData.e_id === "automatic_event_outbound_link") { 2879 setTimeout(() => Utils.sendServerAjaxRequest(options.ajaxUrl, json), 500); 2880 } else if (allData.type != 'static') { 2881 Utils.sendServerAjaxRequest(options.ajaxUrl, json); 2882 } 2883 2884 if ( ( allData.type == 'static' && options.ajaxForServerStaticEvent ) || ( allData.hasOwnProperty( 'ajaxFire' ) && allData.ajaxFire ) ) { 2885 Utils.sendServerAjaxRequest( options.ajaxUrl, json ); 2886 } 2887 } 2888 } 2889 delete params._fbp; 2890 delete params._fbc; 2891 eventId = allData.eventID 2892 } 2893 return eventId; 2894 } 2895 2896 function fireEvent(name, allData) { 2897 2898 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name,'facebook')) { 2899 return; 2900 } 2901 2902 var actionType = defaultEventTypes.includes(name) ? 'track' : 'trackCustom'; 2903 var data = allData.params; 2904 var params = {}; 2905 var arg = {}; 2906 Utils.copyProperties(data, params); 2907 let eventId = sendFbServerEvent(allData,name,params) 2908 2909 2910 if("hCR" === name) { 2911 return; 2912 } 2913 2914 if (options.debug) { 2915 console.log('[Facebook] ' + name, params,"eventID",eventId); 2916 } 2917 2918 if(eventId != null) { 2919 arg.eventID = eventId; 2920 } 2921 2922 fbq(actionType, name, params, arg); 2923 2924 } 2925 2926 /** 2927 * Public API 2928 */ 2929 return { 2930 tag: function() { 2931 return "facebook"; 2932 }, 2933 isEnabled: function () { 2934 return options.hasOwnProperty('facebook'); 2935 }, 2936 2937 disable: function () { 2938 initialized = false;
2939 }, 2940 advancedMatching: function () { 2941 if(options.facebook.advancedMatchingEnabled) { 2942 let advancedMatching = {}; 2943 if(Object.keys(options.facebook.advancedMatching).length > 0) { 2944 advancedMatching = options.facebook.advancedMatching; 2945 } 2946 2947 Utils.AdvancedMatching.merge(advancedMatching, { 2948 em: { field: 'email', hash: false }, 2949 ph: { field: 'phone', hash: false }, 2950 fn: { field: 'first_name', hash: false }, 2951 ln: { field: 'last_name', hash: false } 2952 }); 2953 2954 if(!advancedMatching.hasOwnProperty("external_id")){ 2955 if (Cookies.get('pbid') || (options.hasOwnProperty('pbid') && options.pbid)) { 2956 advancedMatching["external_id"] = Cookies.get('pbid') ? Cookies.get('pbid') : options.pbid; 2957 } 2958 } 2959 else if(advancedMatching.external_id != Cookies.get('pbid')) 2960 { 2961 advancedMatching["external_id"] = Cookies.get('pbid') ? Cookies.get('pbid') : advancedMatching.external_id; 2962 } 2963 if(Object.keys(advancedMatching).length > 0) { 2964 return advancedMatching; 2965 } 2966 } 2967 return false 2968 }, 2969 /** 2970 * Load pixel's JS 2971 */ 2972 loadPixel: function () { 2973 2974 if (initialized || !this.isEnabled() || !Utils.consentGiven('facebook')) { 2975 return; 2976 } 2977 2978 // First visit: wait (briefly) for this browser's external_id, so 2979 // the very first PageView is not sent under the inline value. 2980 if (Utils.holdForIdentity(function () { Facebook.loadPixel(); })) { 2981 return; 2982 } 2983 2984 !function (f, b, e, v, n, t, s) { 2985 if (f.fbq) return; 2986 n = f.fbq = function () { 2987 n.callMethod ? 2988 n.callMethod.apply(n, arguments) : n.queue.push(arguments) 2989 }; 2990 if (!f._fbq) f._fbq = n; 2991 n.push = n; 2992 n.loaded = !0; 2993 n.version = '2.0'; 2994 n.agent = 'dvpixelyoursite'; 2995 n.queue = []; 2996 t = b.createElement(e); 2997 t.async = !0; 2998 t.src = v; 2999 s = b.getElementsByTagName(e)[0]; 3000 s.parentNode.insertBefore(t, s) 3001 }(window, 3002 document, 'script', 'https://connect.facebook.net/en_US/fbevents.js'); 3003 3004 // Report the cookie domain here too, not just from 3005 // manageCookies(): this runs under the `facebook` pixel's own 3006 // consent, which can differ from and outlast the `pys` 3007 // script's consent (e.g. under ConsentMagic's separate 3008 // category assignments), so PHP might otherwise never learn 3009 // the scope for a visitor who only accepted this pixel. 3010 // 3011 // `pys_cd` only ever appears here alongside a cookie we are 3012 // already writing at that scope, so it can never be the first 3013 // cookie a visitor receives. disabled_all_cookie is still 3014 // honoured, matching the call in manageCookies(). 3015 if (!options.cookie || !options.cookie.disabled_all_cookie) { 3016 reportCookieDomain(cookieDomain()); 3017 } 3018 3019 let expires = parseInt(options.cookie_duration); 3020 if(!Cookies.get('_fbp')) { 3021 Cookies.set('_fbp',genereateFbp(), { expires: expires,path: '/',domain: cookieDomain() }); 3022 } 3023 3024 if(getUrlParameter('fbclid')) { 3025 Cookies.set('_fbc',genereateFbc(), { expires: expires,path: '/',domain: cookieDomain() }); 3026 } 3027 3028 // Now that Facebook consent is confirmed, mirror the session 3029 // entry referrer (if any) into the session cookie so PHP can 3030 // use it as a fallback for orphan server-only events. 3031 getOrInitSessionEntryReferrer(true); 3032 3033 // Refresh the per-page event referrer cookie so checkout 3034 // hooks can snapshot the buyer's prior page. 3035 writeEventReferrerCookie(); 3036 3037 // initialize pixel 3038 options.facebook.pixelIds.forEach(function (pixelId) { 3039 if (options.facebook.removeMetadata) { 3040 fbq('set', 'autoConfig', false, pixelId); 3041 } 3042 let advancedMatching = Facebook.advancedMatching(); 3043 3044 if ( +options.facebook.meta_ldu === 1 ) { 3045 fbq( 'dataProcessingOptions', [ 'LDU' ], 0, 0 ); 3046 } 3047 3048 // Meta-Enabled Conversion API: let Meta build its own API 3049 // events out of this pixel's data. Must run right before init. 3050 if (Utils.isMetaEnabledCapiPixel(pixelId)) { 3051 fbq('optinMetaEnabledCapi', pixelId); 3052 } 3053 3054 if (options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined") { 3055 if(!advancedMatching) { 3056 fbq('init', pixelId); 3057 } else { 3058 var test_prefix = CS_Data.test_prefix; 3059 var cs_advanced_matching = Cookies.get('cs_enabled_advanced_matching'+test_prefix); 3060 if (jQuery('#cs_enabled_advanced_matching'+test_prefix).length > 0) { 3061 if (cs_advanced_matching == 'yes') { 3062 fbq('init', pixelId, advancedMatching); 3063 } else { 3064 fbq('init', pixelId); 3065 } 3066 } else { 3067 fbq('init', pixelId, advancedMatching); 3068 } 3069 } 3070 } else { 3071 if(!advancedMatching) { 3072 fbq('init', pixelId); 3073 } else { 3074 fbq('init', pixelId, advancedMatching); 3075 } 3076 } 3077 }); 3078 3079 initialized = true; 3080 3081 Utils.fireStaticEvents('facebook'); 3082 3083 }, 3084 3085 fireEvent: function (name, data) { 3086 3087 if (!initialized || !this.isEnabled()) { 3088 return false;
3089 } 3090 3091 data.delay = data.delay || 0; 3092 data.params = data.params || {}; 3093 3094 if (data.delay === 0) { 3095 3096 fireEvent(name, data); 3097 3098 } else { 3099 3100 setTimeout(function (name, params) { 3101 fireEvent(name, params); 3102 }, data.delay * 1000, name, data); 3103 3104 } 3105 3106 return true; 3107 3108 }, 3109 3110 onCommentEvent: function (event) { 3111 this.fireEvent(event.name, event); 3112 }, 3113 3114 onDownloadEvent: function (event) { 3115 this.fireEvent(event.name, event); 3116 }, 3117 3118 onFormEvent: function (event) { 3119 3120 this.fireEvent(event.name, event); 3121 3122 }, 3123 3124 onWooAddToCartOnButtonEvent: function (product_id) { 3125 3126 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3127 return; 3128 var event = options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]; 3129 window.pysWooProductData = window.pysWooProductData || []; 3130 if (window.pysWooProductData.hasOwnProperty(product_id)) { 3131 if (window.pysWooProductData[product_id].hasOwnProperty('facebook')) { 3132 event = Utils.copyProperties(event, {}) 3133 Utils.copyProperties(window.pysWooProductData[product_id]['facebook'].params, event.params) 3134 this.fireEvent(event.name, event); 3135 } 3136 } 3137 }, 3138 3139 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, $form) { 3140 3141 window.pysWooProductData = window.pysWooProductData || []; 3142 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3143 return; 3144 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 3145 3146 if (product_type === Utils.PRODUCT_VARIABLE && !options.facebook.wooVariableAsSimple) { 3147 product_id = parseInt($form.find('input[name="variation_id"]').val()); 3148 } 3149 3150 if (window.pysWooProductData.hasOwnProperty(product_id)) { 3151 if (window.pysWooProductData[product_id].hasOwnProperty('facebook')) { 3152 3153 3154 Utils.copyProperties(window.pysWooProductData[product_id]['facebook'].params, event.params); 3155 3156 var groupValue = 0; 3157 if(product_type === Utils.PRODUCT_GROUPED ) { 3158 $form.find(".woocommerce-grouped-product-list .qty").each(function(index){ 3159 var childId = $(this).attr('name').replaceAll("quantity[","").replaceAll("]",""); 3160 var quantity = parseInt($(this).val()); 3161 if(isNaN(quantity)) { 3162 quantity = 0; 3163 } 3164 var childItem = window.pysWooProductData[product_id]['facebook'].grouped[childId]; 3165 3166 if(quantity == 0) { 3167 event.params.content_ids.forEach(function(el,index,array) { 3168 if(el == childItem.content_id) { 3169 array.splice(index, 1); 3170 } 3171 }); 3172 } 3173 3174 if(event.params.hasOwnProperty('contents')) { 3175 event.params.contents.forEach(function(el,index,array) { 3176 if(el.id == childItem.content_id) { 3177 if(quantity > 0){ 3178 el.quantity = quantity; 3179 } else { 3180 array.splice(index, 1); 3181 } 3182 } 3183 }); 3184 } 3185 3186 3187 groupValue += childItem.price * quantity; 3188 }); 3189 if(groupValue == 0) return;
3189 // skip if no items selected 3190 } 3191 3192 // maybe customize value option 3193 if (options.woo.addToCartOnButtonValueEnabled && options.woo.addToCartOnButtonValueOption !== 'global') { 3194 3195 if(product_type === Utils.PRODUCT_GROUPED) { 3196 event.params.value = groupValue; 3197 } else if(product_type === Utils.PRODUCT_BUNDLE) { 3198 var data = $(".bundle_form .bundle_data").data("bundle_form_data"); 3199 var items_sum = getBundlePriceOnSingleProduct(data); 3200 event.params.value = (parseInt(data.base_price) + items_sum )* qty; 3201 } else { 3202 event.params.value = event.params.value * qty; 3203 } 3204 } 3205 3206 // only when non Facebook for WooCommerce logic used 3207 if (event.params.hasOwnProperty('contents') && product_type !== Utils.PRODUCT_GROUPED) { 3208 event.params.contents[0].quantity = qty; 3209 } 3210 3211 this.fireEvent(event.name, event); 3212 3213 } 3214 } 3215 }, 3216 3217 onWooRemoveFromCartEvent: function (event) { 3218 this.fireEvent(event.name, event); 3219 }, 3220 3221 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 3222 3223 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3224 return; 3225 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 3226 3227 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 3228 3229 var index; 3230 3231 if (price_index) { 3232 index = download_id + '_' + price_index; 3233 } else { 3234 index = download_id; 3235 } 3236 3237 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 3238 if (window.pysEddProductData[download_id][index].hasOwnProperty('facebook')) { 3239 3240 3241 Utils.copyProperties(window.pysEddProductData[download_id][index]['facebook']["params"], event.params) 3242 3243 // maybe customize value option 3244 if (options.edd.addToCartOnButtonValueEnabled && options.edd.addToCartOnButtonValueOption !== 'global') { 3245 event.params.value = event.params.value * qty; 3246 } 3247 3248 // update contents qty param 3249 var contents = event.params.contents; 3250 contents[0].quantity = qty; 3251 event.params.contents = contents; 3252 3253 this.fireEvent(event.name,event); 3254 3255 } 3256 } 3257 3258 } 3259 3260 }, 3261 3262 onEddRemoveFromCartEvent: function (event) { 3263 this.fireEvent(event.name, event); 3264 }, 3265 onPageScroll: function (event) { 3266 this.fireEvent(event.name, event); 3267 }, 3268 onTime: function (event) { 3269 this.fireEvent(event.name, event); 3270 }, 3271 initEventIdCookies: function (key) { 3272 var ids = {}; 3273 ids[key] = pys_generate_token(36) 3274 Cookies.set('pys_fb_event_id', JSON.stringify(ids)); 3275 }, 3276 updateEventId:function(key) { 3277 var cooData = Cookies.get("pys_fb_event_id") 3278 if(cooData === undefined) { 3279 this.initEventIdCookies(key); 3280 } else { 3281 var data = JSON.parse(cooData); 3282 data[key] = pys_generate_token(36); 3283 Cookies.set('pys_fb_event_id', JSON.stringify(data) ); 3284 } 3285 }, 3286 3287 getEventId:function (key) { 3288 var data = Cookies.get("pys_fb_event_id"); 3289 if(data === undefined) {
3290 this.initEventIdCookies(key); 3291 data = Cookies.get("pys_fb_event_id"); 3292 } 3293 return JSON.parse(data)[key]; 3294 }, 3295 3296 sendFacebookRestAPIRequest: function(allData, name, params) { 3297 if (typeof window.pysFacebookRest === 'undefined' || !window.pysFacebookRest.restApiUrl) { 3298 return; 3299 } 3300 3301 const restApiData = { 3302 event: name, 3303 data: JSON.stringify(params), 3304 ids: allData.pixelIds || options.facebook.pixelIds, 3305 eventID: allData.eventID, 3306 woo_order: allData.woo_order || 0, 3307 edd_order: allData.edd_order || 0, 3308 order_key: allData.order_key || '' 3309 }; 3310 3311 if (pageReferrer) { 3312 restApiData.referrer_url = pageReferrer; 3313 } 3314 3315 // Variant A: send the actual page URL where the event happened 3316 // so the server sets event_source_url to this page, not the 3317 // REST endpoint. PHP applies enable_remove_source_url_params. 3318 restApiData.event_source_url = window.location.href; 3319 3320 // Try sendBeacon first 3321 if (navigator.sendBeacon) { 3322 const formData = new FormData(); 3323 Object.keys(restApiData).forEach(key => { 3324 if (Array.isArray(restApiData[key])) { 3325 restApiData[key].forEach(value => { 3326 formData.append(key + '[]', value); 3327 }); 3328 } else { 3329 formData.append(key, restApiData[key]); 3330 } 3331 }); 3332 3333 if (navigator.sendBeacon(window.pysFacebookRest.restApiUrl, formData)) { 3334 return; 3335 } 3336 } 3337 3338 // Fallback to fetch 3339 fetch(window.pysFacebookRest.restApiUrl, { 3340 method: 'POST', 3341 headers: { 3342 'Content-Type': 'application/x-www-form-urlencoded', 3343 }, 3344 body: new URLSearchParams(restApiData) 3345 }).catch(() => { 3346 // Fallback to AJAX if fetch fails 3347 this.sendAjaxFallback(allData, name, params); 3348 }); 3349 }, 3350 3351 sendAjaxFallback: function(allData, name, params) { 3352 var json = { 3353 action: 'pys_api_event', 3354 pixel: 'facebook', 3355 event: name, 3356 data: params, 3357 ids: allData.pixelIds || options.facebook.pixelIds, 3358 eventID: allData.eventID, 3359 url: window.location.href, 3360 ajax_event: options.ajax_event 3361 }; 3362 if (allData.hasOwnProperty('woo_order')) { 3363 json['woo_order'] = allData.woo_order; 3364 } 3365 if (allData.hasOwnProperty('edd_order')) { 3366 json['edd_order'] = allData.edd_order; 3367 } 3368 if (allData.hasOwnProperty('order_key') && allData.order_key) { 3369 json['order_key'] = allData.order_key; 3370 } 3371 if (pageReferrer) { 3372 json.referrer_url = pageReferrer; 3373 } 3374 3375 Utils.sendServerAjaxRequest(options.ajaxUrl, json); 3376 }, 3377 }; 3378 3379 }(options); 3380 3381 var Analytics = function (options) { 3382 3383 var initialized = false; 3384 3385 /** 3386 * Fires event 3387 * 3388 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/sending-data 3389 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/events 3390 * @link: https://developers.google.com/gtagjs/reference/event 3391 * @link: https://developers.google.com/gtagjs/reference/parameter 3392 * 3393 * @link: https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dim
3393s-mets 3394 * 3395 * @param name 3396 * @param data 3397 */ 3398 function fireEvent(name, data) { 3399 3400 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name,'ga')) { 3401 return; 3402 } 3403 3404 var eventParams = Utils.copyProperties(data, {}); 3405 var _fireEvent = function (tracking_id,name,params) { 3406 3407 // Firing without a destination would park the hit inside gtag.js 3408 // instead of sending it, which Tag Assistant reports as a missing 3409 // config command. Skip, but keep the reason visible in debug mode. 3410 if (!Utils.hasSendToTarget(tracking_id)) { 3411 if (options.debug) { 3412 console.warn('[Google Analytics] ' + name + ' skipped: empty tracking ID', { 3413 trackingIds: options.ga.trackingIds 3414 }); 3415 } 3416 return; 3417 } 3418 3419 params['send_to'] = tracking_id; 3420 3421 if (options.debug) { 3422 console.log('[Google Analytics #' + tracking_id + '] ' + name, params); 3423 } 3424 3425 Utils.gtagPush('event', name, params); 3426 3427 }; 3428 options.ga.trackingIds.forEach(function (tracking_id) { 3429 var copyParams = Utils.copyProperties(eventParams, {}); // copy params because mapParamsTov4 can modify it 3430 var params = mapParamsTov4(tracking_id,name,copyParams) 3431 _fireEvent(tracking_id,name,params); 3432 }); 3433 3434 } 3435 function mapParamsTov4(tag,name,param) { 3436 //GA4 automatically collects a number of parameters for all events 3437 delete param.landing_page; 3438 // end 3439 if(isv4(tag)) { 3440 delete param.traffic_source; 3441 delete param.event_category; 3442 delete param.event_label; 3443 delete param.ecomm_prodid; 3444 delete param.ecomm_pagetype; 3445 delete param.ecomm_totalvalue; 3446 if(name === 'search') { 3447 param['search'] = param.search_term; 3448 delete param.search_term; 3449 delete param.dynx_itemid; 3450 delete param.dynx_pagetype; 3451 delete param.dynx_totalvalue; 3452 } 3453 } 3454 return param; 3455 } 3456 3457 function isv4(tag) { 3458 return tag.indexOf('G') === 0; 3459 } 3460 3461 /** 3462 * Public API 3463 */ 3464 return { 3465 tag: function() { 3466 return "ga"; 3467 }, 3468 isEnabled: function () { 3469 return options.hasOwnProperty('ga'); 3470 }, 3471 3472 disable: function () { 3473 initialized = false; 3474 }, 3475 3476 loadPixel: function () { 3477 3478 if (initialized || !this.isEnabled() || !Utils.consentGiven('analytics')) { 3479 return; 3480 } 3481 3482 Utils.loadGoogleTag(options.ga.trackingIds[0]); 3483 var config = {}; 3484 // Cross-Domain tracking 3485 if (options.ga.crossDomainEnabled) { 3486 config.linker = { 3487 accept_incoming: options.ga.crossDomainAcceptIncoming, 3488 domains: options.ga.crossDomainDomains 3489 }; 3490 } 3491 var ids = options.ga.trackingIds; 3492 // configure tracking ids 3493 ids.forEach(function (trackingId,index) { 3494 var obj = options.ga.isDebugEnabled; 3495 var searchValue = "index_"+index; 3496 var config_for_tag = Object.assign({}, options.config); 3497 config_for_tag.debug_mode = false;
3498 config_for_tag.send_page_view = !options.ga.custom_page_view_event; 3499 for (var key in obj) { 3500 if (obj.hasOwnProperty(key) && obj[key] === searchValue) { 3501 config_for_tag.debug_mode = true; 3502 break; 3503 } 3504 } 3505 if(!config_for_tag.debug_mode) 3506 { 3507 delete config_for_tag.debug_mode; 3508 } 3509 if(isv4(trackingId)) { 3510 if(options.ga.disableAdvertisingFeatures) { 3511 config_for_tag.allow_google_signals = false 3512 } 3513 if(options.ga.disableAdvertisingPersonalization) { 3514 config_for_tag.allow_ad_personalization_signals = false 3515 } 3516 } 3517 if(options.ga.hasOwnProperty('additionalConfig')){ 3518 if(options.ga.additionalConfig.hasOwnProperty(trackingId) && options.ga.additionalConfig[trackingId]){ 3519 config_for_tag.first_party_collection = options.ga.additionalConfig[trackingId].first_party_collection; 3520 } 3521 3522 } 3523 if(options.ga.hasOwnProperty('serverContainerUrls')){ 3524 if(options.ga.serverContainerUrls.hasOwnProperty(trackingId) && options.ga.serverContainerUrls[trackingId].enable_server_container != false){ 3525 if(options.ga.serverContainerUrls[trackingId].server_container_url != ''){ 3526 config_for_tag.server_container_url = options.ga.serverContainerUrls[trackingId].server_container_url; 3527 } 3528 if(options.ga.serverContainerUrls[trackingId].transport_url != ''){ 3529 config_for_tag.transport_url = options.ga.serverContainerUrls[trackingId].transport_url; 3530 } 3531 } 3532 } 3533 if (options.gdpr.cookiebot_integration_enabled && typeof Cookiebot !== 'undefined') { 3534 3535 var cookiebot_consent_category = options.gdpr['cookiebot_analytics_consent_category']; 3536 if (options.gdpr['analytics_prior_consent_enabled']) { 3537 if (Cookiebot.consented === true && Cookiebot.consent[cookiebot_consent_category]) { 3538 Utils.gtagPush('config', trackingId, config_for_tag); 3539 } 3540 } else { 3541 if (Cookiebot.consent[cookiebot_consent_category]) { 3542 Utils.gtagPush('config', trackingId, config_for_tag); 3543 } 3544 } 3545 } 3546 else 3547 { 3548 Utils.gtagPush('config', trackingId, config_for_tag); 3549 } 3550 }); 3551 3552 initialized = true; 3553 3554 Utils.fireStaticEvents('ga'); 3555 3556 }, 3557 3558 fireEvent: function (name, data) { 3559 3560 if (!initialized || !this.isEnabled()) { 3561 return false; 3562 } 3563 3564 data.delay = data.delay || 0; 3565 data.params = data.params || {}; 3566 data.params.eventID = Utils.generateUniqueId(data); 3567 if (data.delay === 0) { 3568 3569 fireEvent(name, data.params); 3570 3571 } else { 3572 3573 setTimeout(function (name, params) { 3574 fireEvent(name, params); 3575 }, data.delay * 1000, name, data.params); 3576 3577 } 3578 3579 return true; 3580 3581 }, 3582 3583 onCommentEvent: function (event) { 3584 this.fireEvent(event.name, event); 3585 }, 3586 3587 onDownloadEvent: function (event) { 3588 this.fireEvent(event.name, event); 3589 }, 3590 3591 onFormEvent: function (event) { 3592 this.fireEvent(event.name, event); 3593 }, 3594 3595 onWooAddToCartOnButtonEvent: function (product_id) { 3596 3597 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3598 return; 3599 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 3600 3601 if (window.pysWooProductData.hasOwnProperty(product_id)) { 3602 if (window.pysWooProductData[product_id].hasOwnProperty('ga')) { 3603 Utils.copyProperties(window.pysWooProductData[product_id]['ga'].params, event.params) 3604 this.fireEvent(event.name, event); 3605 } 3606 } 3607 3608 3609 }, 3610 3611 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, $form) { 3612 3613 window.pysWooProductData = window.pysWooProductData || []; 3614 3615 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3616 return; 3617 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 3618 3619 if (product_type === Utils.PRODUCT_VARIABLE && !options.ga.wooVariableAsSimple) { 3620 product_id = parseInt($form.find('input[name="variation_id"]').val()); 3621 } 3622 3623 if (window.pysWooProductData.hasOwnProperty(product_id)) { 3624 if (window.pysWooProductData[product_id].hasOwnProperty('ga')) { 3625 3626 Utils.copyProperties(window.pysWooProductData[product_id]['ga'].params, event.params); 3627 if(product_type === Utils.PRODUCT_GROUPED ) { 3628 var groupValue = 0; 3629 $form.find(".woocommerce-grouped-product-list .qty").each(function(index){ 3630 var childId = $(this).attr('name').replaceAll("quantity[","").replaceAll("]",""); 3631 var quantity = parseInt($(this).val()); 3632 if(isNaN(quantity)) { 3633 quantity = 0; 3634 } 3635 var childItem = window.pysWooProductData[product_id]['ga'].grouped[childId]; 3636 // update quantity
3637 event.params.items.forEach(function(el,index,array) { 3638 if(el.id == childItem.content_id) { 3639 if(quantity > 0){ 3640 el.quantity = quantity; 3641 } else { 3642 array.splice(index, 1); 3643 } 3644 } 3645 }); 3646 groupValue += childItem.price * quantity; 3647 }); 3648 if(options.woo.addToCartOnButtonValueEnabled && 3649 options.woo.addToCartOnButtonValueOption !== 'global' && 3650 event.params.hasOwnProperty('ecomm_totalvalue')) { 3651 event.params.ecomm_totalvalue = groupValue; 3652 } 3653 3654 if(groupValue == 0) return; // skip if no items selected 3655 } else { 3656 // update items qty param 3657 event.params.items[0].quantity = qty; 3658 } 3659 3660 // maybe customize value option 3661 if (options.woo.addToCartOnButtonValueEnabled && 3662 options.woo.addToCartOnButtonValueOption !== 'global' && 3663 product_type !== Utils.PRODUCT_GROUPED) 3664 { 3665 if(event.params.hasOwnProperty('ecomm_totalvalue')) { 3666 event.params.ecomm_totalvalue = event.params.items[0].price * qty; 3667 } 3668 3669 } 3670 3671 3672 this.fireEvent(event.name, event); 3673 } 3674 } 3675 3676 }, 3677 3678 onWooRemoveFromCartEvent: function (event) { 3679 3680 this.fireEvent(event.name, event); 3681 3682 }, 3683 3684 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 3685 3686 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3687 return; 3688 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 3689 3690 3691 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 3692 3693 var index; 3694 3695 if (price_index) { 3696 index = download_id + '_' + price_index; 3697 } else { 3698 index = download_id; 3699 } 3700 3701 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 3702 if (window.pysEddProductData[download_id][index].hasOwnProperty('ga')) { 3703 3704 Utils.copyProperties(window.pysEddProductData[download_id][index]['ga'].params, event.params); 3705 3706 // update items qty param 3707 event.params.items[0].quantity = qty; 3708 3709 this.fireEvent(event.name,event); 3710 3711 } 3712 } 3713 3714 } 3715 3716 }, 3717 3718 onEddRemoveFromCartEvent: function (event) { 3719 this.fireEvent(event.name, event); 3720 }, 3721 onPageScroll: function (event) { 3722 this.fireEvent(event.name, event); 3723 }, 3724 onTime: function (event) { 3725 this.fireEvent(event.name, event); 3726 }, 3727 3728 }; 3729 3730 }(options); 3731 3732 var GTM = function (options) { 3733 var initialized = false; 3734 3735 var datalayer_name = 'dataLayer'; 3736 /* 3737 * @param name 3738 * @param data 3739 */ 3740 function fireEvent(name, event) { 3741 if(typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name,'gtm')) { 3742 return; 3743 } 3744 3745 3746 var eventParams = event.params; 3747 var data = event.params; 3748 var valuesArray = Object.values(event.trackingIds); 3749 var ids = valuesArray;
3750 Utils.copyProperties(Utils.getRequestParams(), eventParams); 3751 var _fireEvent = function (tracking_id,name,params, event=null) { 3752 var eventData = {}; 3753 var ContainerCodeHasTag = !options.gtm.gtm_just_data_layer && tracking_id.length > 0; 3754 if(ContainerCodeHasTag) { 3755 params['send_to'] = tracking_id; 3756 } 3757 else if(!options.gtm.gtm_just_data_layer){ 3758 return 3759 } 3760 3761 if (params.hasOwnProperty('ecommerce')) { 3762 eventData.ecommerce = params.ecommerce; 3763 delete params.ecommerce; 3764 } 3765 3766 var automatedParams = { ...params }; 3767 [params['manualName'], 'manualName', 'triggerType'].forEach(key => delete automatedParams[key]); 3768 if(event && (!event.hasOwnProperty('hasAutoParam') || (event.hasOwnProperty('hasAutoParam') && event.hasAutoParam))) { 3769 eventData.automatedParameters = automatedParams; 3770 } 3771 3772// Move custom parameters to eventData 3773 3774 if (params.hasOwnProperty(params['manualName'])) { 3775 eventData[params['manualName']] = params[params['manualName']]; 3776 delete params[params['manualName']]; 3777 } 3778 3779 ['manualName','triggerType'].forEach(key => { 3780 if (params.hasOwnProperty(key)) { 3781 eventData[key] = params[key]; 3782 delete params[key]; 3783 } 3784 }); 3785 3786 eventData.manualDataLayer = Utils.resolveGtmDataLayerName(); 3787 3788 eventData.event = name; 3789 3790 if (options.debug) { 3791 if (ContainerCodeHasTag){ 3792 console.log('[Google GTM #' + tracking_id + '] ' + name, eventData); 3793 } 3794 else { 3795 console.log('[Google GTM push to "'+datalayer_name+'"] ' + name, eventData); 3796 } 3797 } 3798 window[datalayer_name].push(eventData); 3799 3800 }; 3801 3802 var copyParams = Utils.copyProperties(eventParams, {}); // copy params because mapParamsToGTM can modify it 3803 3804 var params = mapParamsToGTM(ids,name,copyParams) 3805 3806 params.event_id = Utils.generateUniqueId(event); 3807 3808 _fireEvent(ids, name, params, event); 3809 } 3810 3811 function normalizeEventName(eventName) { 3812 3813 var matches = { 3814 ViewContent: 'view_item', 3815 AddToCart: 'add_to_cart', 3816 AddToWishList: 'add_to_wishlist', 3817 InitiateCheckout: 'begin_checkout', 3818 Purchase: 'purchase', 3819 Lead: 'generate_lead', 3820 CompleteRegistration: 'sign_up', 3821 AddPaymentInfo: 'set_checkout_option' 3822 }; 3823 3824 return matches.hasOwnProperty(eventName) ? matches[eventName] : eventName; 3825 3826 } 3827 3828 function mapParamsToGTM(tag,name,param) { 3829 //GA4 automatically collects a number of parameters for all events 3830 var hasGTM = false; 3831 3832 // end 3833 if (Array.isArray(tag)) { 3834 hasGTM = tag.some(function (element) { 3835 return isGTM(element); 3836 }); 3837 } else if(isGTM(tag)) { 3838 // tag is a string and matches GTM 3839 hasGTM = true; 3840 } 3841 if(hasGTM) { 3842 delete param.event_category; 3843 delete param.event_label; 3844 3845 delete param.analytics_storage; 3846 delete param.ad_storage; 3847 delete param.ad_user_data; 3848 delete param.ad_personalization; 3849 3850 if(name === 'search') { 3851 param['search'] = param.search_term; 3852 delete param.search_term; 3853 delete param.dynx_itemid; 3854 delete param.dynx_pagetype; 3855 delete param.dynx_totalvalue; 3856 } 3857 } 3858 return param; 3859 } 3860 3861 function isGTM(tag) { 3862 return tag.indexOf('GTM') === 0; 3863 } 3864 /** 3865 * Public API 3866 */ 3867 return { 3868 tag: function() { 3869 return "gtm"; 3870 }, 3871 isEnabled: function () { 3872 return options.hasOwnProperty('gtm'); 3873 }, 3874 disable: function () { 3875 initialized = false;
3876 }, 3877 updateEnhancedConversionData : function () { 3878 if (!initialized || !this.isEnabled()) { 3879 return; 3880 } 3881 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnable) { 3882 var advanced = Utils.getAdvancedFormData(); 3883 if (Object.keys(advanced).length > 0) { 3884 window[datalayer_name].push({'user_data' : advanced}); 3885 } 3886 } 3887 }, 3888 loadPixel: function () { 3889 3890 if (initialized || !this.isEnabled() || !Utils.consentGiven('analytics')) { 3891 return; 3892 } 3893 datalayer_name = Utils.resolveGtmDataLayerName(); 3894 3895 for (var i = 0; i < options.gtm.trackingIds.length; i++) { 3896 var trackingId = options.gtm.trackingIds[i]; 3897 if (!options.gtm.gtm_just_data_layer) { 3898 console.log('[PYS] Google Tag Manager container code loaded'); 3899 Utils.loadGTMScript(trackingId); 3900 break; 3901 } 3902 } 3903 if(options.gtm.gtm_just_data_layer) { 3904 console.warn && console.warn("[PYS] Google Tag Manager container code placement set to OFF !!!"); 3905 console.warn && console.warn("[PYS] Data layer codes are active but GTM container must be loaded using custom coding !!!"); 3906 // Send the consent state even when GTM script loading is handled 3907 // externally: gtm_just_data_layer with tracking IDs configured skips 3908 // loadGTMScript() entirely. When trackingIds is empty, loadGTMScript() 3909 // below runs too and would reach this again â pushGTMConsentState() 3910 // is idempotent per page load, so the container still sees one command. 3911 Utils.pushGTMConsentState( datalayer_name ); 3912 if(options.gtm.trackingIds.length == 0){ 3913 Utils.loadGTMScript(); 3914 } 3915 } 3916 3917 if(options.hasOwnProperty("tracking_analytics") && options.tracking_analytics.hasOwnProperty("userDataEnable") && options.tracking_analytics.userDataEnable){ 3918 var advanced = Utils.getAdvancedFormData(); 3919 if(Object.keys(advanced).length > 0){ 3920 window[datalayer_name].push({'user_data' : advanced}); 3921 } 3922 } 3923 3924 3925 var config = {}; 3926 3927 if(options.user_id && options.user_id != 0) { 3928 config.user_id = options.user_id; 3929 } 3930 3931 // Cross-Domain tracking 3932 3933 var ids = options.gtm.trackingIds; 3934 3935 3936 initialized = true; 3937 3938 Utils.fireStaticEvents('gtm'); 3939 }, 3940 3941 fireEvent: function (name, data) { 3942 3943 if (!initialized || !this.isEnabled()) { 3944 return false; 3945 } 3946 3947 data.delay = data.delay || 0; 3948 data.params = data.params || {}; 3949 3950 if (data.delay === 0) { 3951 3952 fireEvent(name, data); 3953 3954 } else { 3955 3956 setTimeout(function (name, params) { 3957 fireEvent(name, params); 3958 }, data.delay * 1000, name, data); 3959 3960 } 3961 3962 return true; 3963 3964 }, 3965 3966 onCommentEvent: function (event) { 3967 this.fireEvent(event.name, event); 3968 }, 3969 3970 onDownloadEvent: function (event) { 3971 this.fireEvent(event.name, event); 3972 }, 3973 3974 onFormEvent: function (event) { 3975 this.fireEvent(event.name, event); 3976 }, 3977 3978 onWooAddToCartOnButtonEvent: function (product_id, prod_info = null) { 3979 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3980 return; 3981 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 3982 3983 if (window.pysWooProductData.hasOwnProperty(product_id)) { 3984 if (window.pysWooProductData[product_id].hasOwnProperty('gtm')) { 3985 Utils.copyProperties(window.pysWooProductData[product_id]['gtm'].params, event.params) 3986 this.fireEvent(event.name, event); 3987 } 3988 } 3989 3990 }, 3991 3992 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, $form) { 3993 window.pysWooProductData = window.pysWooProductData || []; 3994 3995 if(!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 3996 return; 3997 var event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 3998 3999 if (product_type === Utils.PRODUCT_VARIABLE && !options.gtm.wooVariableAsSimple) { 4000 product_id = parseInt($form.find('input[name="variation_id"]').val()); 4001 } 4002 4003 if (window.pysWooProductData.hasOwnProperty(product_id)) { 4004 if (window.pysWooProductData[product_id].hasOwnProperty('gtm')) { 4005 4006 Utils.copyProperties(window.pysWooProductData[product_id]['gtm'].params, event.params); 4007 4008 params = event.params.hasOwnProperty('ecommerce') ? event.params.ecommerce : event.params; 4009 4010 if(product_type === Utils.PRODUCT_GROUPED ) { 4011 var groupValue = 0; 4012 $form.find(".woocommerce-grouped-product-list .qty").each(function(index){ 4013 var childId = $(this).attr('name').replaceAll("quantity[","").replaceAll("]",""); 4014 var quantity = parseInt($(this).val()); 4015 if(isNaN(quantity)) { 4016 quantity = 0; 4017 } 4018 var childItem = window.pysWooProductData[product_id]['gtm'].grouped[childId];
4019 params.items.forEach(function(el,index,array) { 4020 if(el.id == childItem.content_id) { 4021 if(quantity > 0){ 4022 el.quantity = quantity; 4023 el.price = childItem.price; 4024 } else { 4025 array.splice(index, 1); 4026 } 4027 } 4028 }); 4029 groupValue += childItem.price * quantity; 4030 }); 4031 4032 if(options.woo.addToCartOnButtonValueEnabled && 4033 options.woo.addToCartOnButtonValueOption !== 'global' && 4034 params.hasOwnProperty('value')) { 4035 params.value = groupValue; 4036 } 4037 4038 if(groupValue == 0) return; // skip if no items selected 4039 } else { 4040 // update items qty param 4041 params.items[0].quantity = qty; 4042 } 4043 4044 // maybe customize value option 4045 if (options.woo.addToCartOnButtonValueEnabled && 4046 options.woo.addToCartOnButtonValueOption !== 'global' && 4047 product_type !== Utils.PRODUCT_GROUPED) 4048 { 4049 if(params.hasOwnProperty('value')) { 4050 params.value = params.items[0].price * qty; 4051 } 4052 4053 } 4054 4055 4056 this.fireEvent(event.name, event); 4057 } 4058 } 4059 4060 }, 4061 4062 onWooRemoveFromCartEvent: function (event) { 4063 4064 this.fireEvent(event.name, event); 4065 4066 }, 4067 4068 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 4069 if(!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) 4070 return; 4071 var event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 4072 4073 4074 if (window.pysEddProductData && window.pysEddProductData.hasOwnProperty(download_id)) { 4075 4076 var index; 4077 4078 if (price_index) { 4079 index = download_id + '_' + price_index; 4080 } else { 4081 index = download_id; 4082 } 4083 4084 if (window.pysEddProductData[download_id].hasOwnProperty(index)) { 4085 if (window.pysEddProductData[download_id][index].hasOwnProperty('gtm')) { 4086 4087 Utils.copyProperties(window.pysEddProductData[download_id][index]['gtm'].params, event.params); 4088 item = event.params.hasOwnProperty('ecommerce') ? event.params.ecommerce.items[0] : event.params.items[0]; 4089 // update items qty param 4090 item.quantity = qty; 4091 4092 this.fireEvent(event.name,event); 4093 4094 } 4095 } 4096 4097 } 4098 4099 }, 4100 4101 onEddRemoveFromCartEvent: function (event) { 4102 this.fireEvent(event.name, event); 4103 }, 4104 onPageScroll: function (event) { 4105 this.fireEvent(event.name, event); 4106 }, 4107 onTime: function (event) { 4108 this.fireEvent(event.name, event); 4109 }, 4110 }; 4111 4112 }(options); 4113 4114 4115 var OpenAI = function (options) { 4116 4117 let initialized = false, 4118 sentIdentity = null, 4119 identityWaitTimer = null, 4120 identityWaitExpired = false; 4121 4122 /** 4123 * Send the server copy of an event. 4124 * 4125 */ 4126 function sendOpenAIServerEvent(name, allData, data, eventId) { 4127
4128 if (!options.hasOwnProperty('openai') || !options.openai.serverApiEnabled) { 4129 return; 4130 } 4131 4132 let isApiDisabled = options.gdpr.all_disabled_by_api || 4133 options.gdpr.openai_disabled_by_api || 4134 options.gdpr.cookiebot_integration_enabled || 4135 options.gdpr.consent_magic_integration_enabled || 4136 options.gdpr.cookie_notice_integration_enabled || 4137 options.gdpr.cookie_law_info_integration_enabled; 4138 4139 if (!(options.ajaxForServerEvent || isApiDisabled)) { 4140 return; 4141 } 4142 4143 let isStatic = allData.type === 'static' || allData.type === undefined, 4144 delay = allData.delay || 0; 4145 4146 // PHP never collects a dynamic event, and it collects only delay == 0. 4147 let phpSkipped = !isStatic || delay > 0; 4148 4149 // A static event is PHP's, unless the site is set to send static 4150 // events over AJAX â which is exactly when PHP steps aside. 4151 if (!phpSkipped 4152 && !options.ajaxForServerStaticEvent 4153 && !(allData.hasOwnProperty('ajaxFire') && allData.ajaxFire)) { 4154 return; 4155 } 4156 4157 let params = {data: data}; 4158 4159 if (allData.params && allData.params.custom_event_name) { 4160 params.custom_event_name = allData.params.custom_event_name; 4161 } 4162 4163 let json = { 4164 action: 'pys_openai_api_event', 4165 pixel: 'openai', 4166 event: name, 4167 event_slug: allData.e_id || '', 4168 ids: allData.pixelIds || options.openai.pixelIds, 4169 data: params, 4170 url: window.location.href, 4171 eventID: eventId, 4172 ajax_event: options.ajax_event 4173 }; 4174 4175 if (allData.hasOwnProperty('woo_order')) { 4176 json['woo_order'] = allData.woo_order; 4177 } 4178 4179 if (allData.hasOwnProperty('edd_order')) { 4180 json['edd_order'] = allData.edd_order; 4181 } 4182 4183 if (allData.hasOwnProperty('order_key') && allData.order_key) { 4184 json['order_key'] = allData.order_key; 4185 } 4186 4187 let wait = name === 'page_viewed' ? 500 : 0; 4188 4189 setTimeout(function () { 4190 Utils.sendServerAjaxRequest(options.ajaxUrl, json); 4191 }, wait); 4192 } 4193 4194 /** 4195 * Is the pixel in OpenAI's restricted mode? 4196 * 4197 */ 4198 function optOutMode() { 4199 4200 if ( options.openai && options.openai.opt_out ) { 4201 return true; 4202 } 4203 4204 return !! ( options.gdpr 4205 && options.gdpr.consent_magic_integration_enabled 4206 && typeof CS_Data !== 'undefined' 4207 && Utils.csModeEnabled( CS_Data.cs_openai_opt_out_mode ) ); 4208 } 4209 4210 const SDK_DATA_FIELDS = { 4211 contents: ['type', 'amount', 'currency', 'contents'], 4212 custom: ['type', 'plan_id', 'amount', 'currency', 'contents'], 4213 customer_action: ['type', 'amount', 'currency'], 4214 plan_enrollment: ['type', 'plan_id', 'amount', 'currency', 'contents'] 4215 }; 4216 4217 const SDK_ITEM_FIELDS = ['id', 'name', 'content_type', 'quantity', 'amount', 'currency']; 4218 4219 function sdkInteger(value) { 4220 let number = typeof value === 'string' ? Number(value.trim()) : value; 4221 4222 return typeof number === 'number' && isFinite(number) ? Math.round(number) : null; 4223 } 4224 4225 function sdkCurrency(value) { 4226 return typeof value === 'string' && /^[A-Za-z]{3}$/.test(value) ? value : null; 4227 } 4228 4229 function sdkText(value) { 4230 if (typeof value === 'string') { 4231 return value === '' ? null : value; 4232 } 4233 4234 return typeof value === 'number' && isFinite(value) ? String(value) : null; 4235 } 4236 4237 /** 4238 * One contents[] item, with only the keys the SDK documents. 4239 * 4240 * @param item the item as configured or collected 4241 * @param eventHasCurrency true when data.currency is set: an item amount 4242 * needs a currency, its own or the event's 4243 */ 4244 function sdkSafeItem(item, eventHasCurrency) { 4245 4246 if (!item || typeof item !== 'object' || Array.isArray(item)) { 4247 return null; 4248 } 4249 4250 let safe = {}; 4251 4252 SDK_ITEM_FIELDS.forEach(function (field) { 4253 if (item[field] === undefined || item[field] === null || item[field] === '') { 4254 return; 4255 } 4256 4257 if (field === 'quantity' || field === 'amount') { 4258 let number = sdkInteger(item[field]); 4259 4260 if (number !== null) { 4261 safe[field] = number; 4262 } 4263 4264 return; 4265 } 4266 4267 if (field === 'currency') { 4268 let currency = sdkCurrency(item.currency); 4269 4270 if (currency !== null) { 4271 safe.currency = currency; 4272 } 4273 4274 return; 4275 } 4276 4277 let text = sdkText(item[field]); 4278 4279 if (text !== null) { 4280 safe[field] = text; 4281 } 4282 }); 4283 4284 if (safe.hasOwnProperty('amount') && !safe.hasOwnProperty('currency') && !eventHasCurrency) { 4285 delete safe.amount; 4286 } 4287 4288 return Object.keys(safe).length ? safe : null; 4289 } 4290 4291 /** 4292 * The event data, with only the fields the SDK documents for its shape. 4293 */ 4294 function sdkSafeData(data) { 4295 4296 if (!data || typeof data !== 'object') { 4297 return {}; 4298 } 4299 4300 let allowed = SDK_DATA_FIELDS[data.type]; 4301 4302 if (!allowed) { 4303 // An undocumented shape is the module's own bug, not the 4304 // payload's: hand it over untouched rather than empty it. 4305 return data; 4306 } 4307 4308 let safe = {}; 4309 4310 allowed.forEach(function (field) { 4311 if (data[field] !== undefined && data[field] !== null && data[field] !== '') { 4312 safe[field] = data[field]; 4313 } 4314 }); 4315 4316 if (safe.hasOwnProperty('plan_id')) { 4317 let planId = sdkText(safe.plan_id); 4318 4319 if (planId === null) { 4320 delete safe.plan_id; 4321 } else { 4322 safe.plan_id = planId; 4323 } 4324 } 4325 4326 if (safe.hasOwnProperty('currency')) { 4327 let currency = sdkCurrency(safe.currency); 4328 4329 if (currency === null) { 4330 delete safe.currency; 4331 } else { 4332 safe.currency = currency; 4333 } 4334 } 4335 4336 if (safe.hasOwnProperty('amount')) { 4337 let amount = sdkInteger(safe.amount); 4338 4339 // The SDK refuses an amount with no currency beside it. 4340 if (amount === null || !safe.hasOwnProperty('currency')) { 4341 delete safe.amount; 4342 } else { 4343 safe.amount = amount; 4344 } 4345 } 4346 4347 if (safe.hasOwnProperty('contents')) { 4348 let items = Array.isArray(safe.contents) 4349 ? safe.contents 4350 .map(function (item) { return sdkSafeItem(item, safe.hasOwnProperty('currency')); }) 4351 .filter(function (item) { return item !== null; }) 4352 : []; 4353 4354 if (items.length) { 4355 safe.contents = items; 4356 } else { 4357 delete safe.contents; 4358 } 4359 } 4360 4361 return safe; 4362 } 4363 4364 function fireEvent(name, allData) { 4365 4366 if (typeof window.pys_event_data_filter === "function" && window.pys_disable_event_filter(name, 'openai')) { 4367 return; 4368 } 4369 4370 if (typeof window.oaiq !== 'function') { 4371 return; 4372 } 4373 4374 let ids = (allData && allData.pixelIds && allData.pixelIds.length) 4375 ? allData.pixelIds 4376 : options.openai.pixelIds; 4377 4378 if (!ids || ids.length === 0) { 4379 return; 4380 } 4381 4382 let data = (allData.params && allData.params.data) ? Utils.clone(allData.params.data) : {}; 4383 4384 let eventOptions = {}, 4385 eventId = Utils.generateUniqueId(allData); 4386 4387 if (eventId) { 4388 eventOptions.event_id = eventId; 4389 } 4390 4391 if (allData.params && allData.params.custom_event_name) { 4392 eventOptions.custom_event_name = allData.params.custom_event_name; 4393 } 4394 4395 sendOpenAIServerEvent(name, allData, data, eventId); 4396 4397 let browserData = sdkSafeData(data); 4398 4399 if (optOutMode()) { 4400 eventOptions.opt_out = true; 4401 } 4402 4403 if (options.debug) { 4404 console.log('[OpenAI] ' + name, browserData, eventOptions, 'pixel_ids', ids); 4405 } 4406 4407 ids.forEach(function (pixelId) { 4408 oaiq('measureSingle', pixelId, name, browserData, eventOptions); 4409 }); 4410 4411 let customEvent = new CustomEvent('openai_event_sent', { 4412 detail: { 4413 eventName: name, 4414 data: data, 4415 options: eventOptions, 4416 pixel_ids: ids 4417 } 4418 }); 4419 window.dispatchEvent(customEvent); 4420 } 4421 4422 /** 4423 * True while the visitor's identity is still in flight. 4424 */ 4425 function identityPending() { 4426 return ! identityWaitExpired 4427 && ! options.dynamicDataApplied 4428 && !! options.dynamicDataUrl; 4429 } 4430 4431 const OPENAI_USER_FIELDS = [ 4432 'email_sha256', 4433 'phone_number_sha256', 4434 'external_id_sha256', 4435 'first_name_sha256', 4436 'last_name_sha256', 4437 'country', 4438 'city', 4439 'region', 4440 'postal_code' 4441 ]; 4442 4443 /** 4444 * OpenAI normalises more aggressively than the other pixels: the phone 4445 * is reduced to its national digit form (leading +, 00 and zeros 4446 * stripped, then length-validated), and names lose every punctuation 4447 * and whitespace character. Mirrors pys_openai_normalize_* in 4448 * modules/openai/function-helpers.php. 4449 */ 4450 function openaiCookieIdentity() { 4451 4452 let raw; 4453 4454 try { 4455 raw = Utils.getAdvancedFormData() || {}; 4456 } catch (e) { 4457 raw = {}; 4458 } 4459 4460 let phone = typeof raw.phone === 'string' 4461 ? raw.phone.trim().replace(/[\s().-]+/g, '') 4462 : ''; 4463 4464 if (!/^\+?[0-9]+$/.test(phone) || phone.indexOf('+0') === 0) { 4465 phone = ''; 4466 } else { 4467 let international = phone.indexOf('+') === 0 || phone.indexOf('00') === 0; 4468 let national = phone.replace(/^\+?0*/, ''); 4469 4470 if (international && national.indexOf('1') === 0 && national.length !== 11) { 4471 national = ''; 4472 } 4473 4474 phone = /^[1-9][0-9]{7,14}$/.test(national) ? national : ''; 4475 } 4476 4477 let normalizeName = function (value) { 4478 if (typeof value !== 'string' || value.length > 1024) { 4479 return ''; 4480 } 4481 let stripped = value.replace(/[!-\/:-@\[-`{-~\s]+/g, '').toLowerCase(); 4482 return /\D/u.test(stripped) ? stripped : ''; 4483 }; 4484 4485 return {
4486 email: Utils.AdvancedMatching.normalizeEmail(raw.email || ''), 4487 phone: phone, 4488 first_name: normalizeName(raw.first_name || ''), 4489 last_name: normalizeName(raw.last_name || '') 4490 }; 4491 } 4492 4493 /** 4494 * The identity to hand the SDK, built from whatever is known right now. 4495 */ 4496 function buildAdvancedMatchingUser() { 4497 4498 let user = {}; 4499 4500 if (options.openai.hasOwnProperty('advanced_matching')) { 4501 4502 let matching = options.openai.advanced_matching; 4503 4504 OPENAI_USER_FIELDS.forEach(function (field) { 4505 let value = matching[field]; 4506 4507 if (typeof value === 'string' && value.length > 0) { 4508 user[field] = value; 4509 } 4510 }); 4511 } 4512 4513 Utils.AdvancedMatching.merge(user, { 4514 email_sha256: { field: 'email', hash: true }, 4515 phone_number_sha256: { field: 'phone', hash: true }, 4516 first_name_sha256: { field: 'first_name', hash: true }, 4517 last_name_sha256: { field: 'last_name', hash: true } 4518 }, openaiCookieIdentity()); 4519 4520 let externalIdAllowed = options.send_external_id 4521 && ! ( options.gdpr && options.gdpr.externalID_disabled_by_api ) 4522 && ! ( options.cookie 4523 && ( options.cookie.disabled_all_cookie 4524 || options.cookie.externalID_disabled_by_api ) ); 4525 4526 if (!user.hasOwnProperty('external_id_sha256') 4527 && externalIdAllowed 4528 && typeof sha256 === 'function' 4529 && Cookies.get('pbid')) { 4530 user.external_id_sha256 = sha256(Cookies.get('pbid')); 4531 } 4532 4533 return user; 4534 } 4535 4536 /** 4537 * Public API 4538 */ 4539 return { 4540 tag: function () { 4541 return "openai"; 4542 }, 4543 isEnabled: function () { 4544 return options.hasOwnProperty('openai'); 4545 }, 4546 disable: function () { 4547 initialized = false; 4548 4549 if (typeof window.oaiq === 'function') { 4550 oaiq('consent', false); 4551 } 4552 }, 4553 4554 loadPixel: function () { 4555 4556 if (initialized || !this.isEnabled() || !Utils.consentGiven('openai')) { 4557 return; 4558 } 4559 4560 let ids = options.openai.pixelIds; 4561 4562 if (!ids || ids.length === 0) { 4563 return; 4564 } 4565 4566 if (identityPending()) { 4567 4568 if (identityWaitTimer === null) { 4569 identityWaitTimer = setTimeout(function () { 4570 identityWaitExpired = true; 4571 4572 if (window.pys && window.pys.OpenAI) { 4573 window.pys.OpenAI.loadPixel(); 4574 } 4575 }, 3000); 4576 } 4577 4578 return; 4579 } 4580 4581 if (identityWaitTimer !== null) { 4582 clearTimeout(identityWaitTimer); 4583 identityWaitTimer = null; 4584 } 4585 4586 !function (w, d, s, u) { 4587 if (w.oaiq) return; 4588 var q = function () { 4589 q.q.push(arguments); 4590 }; 4591 q.q = []; 4592 w.oaiq = q; 4593 var js = d.createElement(s); 4594 js.async = true; 4595 js.src = u; 4596 var f = d.getElementsByTagName(s)[0]; 4597 f.parentNode.insertBefore(js, f); 4598 }(window, document, 'script', 'https://bzrcdn.openai.com/sdk/oaiq.min.js'); 4599 4600 let user = buildAdvancedMatchingUser(); 4601 4602 if (Object.keys(user).length > 0) { 4603 sentIdentity = JSON.stringify(user); 4604 } 4605 4606 ids.forEach(function (pixelId) { 4607 let initOptions = { 4608 pixelId: pixelId 4609 }; 4610 4611 if (options.openai.debug) { 4612 initOptions.debug = true; 4613 } 4614 4615 if (Object.keys(user).length > 0) { 4616 initOptions.user = user; 4617 } 4618 4619 oaiq('init', initOptions); 4620 }); 4621 4622 oaiq('consent', true); 4623 4624 initialized = true; 4625 4626 Utils.fireStaticEvents('openai'); 4627 }, 4628 4629 /** 4630 * Hand the SDK an identity that arrived after the pixel was already initialised. 4631 */ 4632 refreshAdvancedMatching: function () { 4633 4634 if (!initialized || !this.isEnabled() || typeof window.oaiq !== 'function') { 4635 return; 4636 } 4637 4638 let user = buildAdvancedMatchingUser(); 4639 4640 if (Object.keys(user).length === 0) { 4641 return; 4642 } 4643 4644 let identity = JSON.stringify(user); 4645 4646 if (identity === sentIdentity) { 4647 return; 4648 } 4649 4650 sentIdentity = identity; 4651 4652 let ids = options.openai.pixelIds || []; 4653 4654 ids.forEach(function (pixelId) { 4655 oaiq('init', {pixelId: pixelId, user: user}); 4656 }); 4657 }, 4658 4659 fireEvent: function (name, data) { 4660 4661 if (!initialized || !this.isEnabled()) { 4662 return false;
4663 } 4664 4665 fireEvent(name, data); 4666 4667 return true; 4668 }, 4669 4670 /** 4671 * Add to cart from a listing button. The event is registered once with 4672 * a placeholder payload; the real one is substituted here from the 4673 * per-product data the server printed. 4674 */ 4675 onWooAddToCartOnButtonEvent: function (product_id) { 4676 4677 if (!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) { 4678 return; 4679 } 4680 if (!window.pysWooProductData || !window.pysWooProductData.hasOwnProperty(product_id)) { 4681 return; 4682 } 4683 if (!window.pysWooProductData[product_id].hasOwnProperty(this.tag())) { 4684 return; 4685 } 4686 4687 let productData = window.pysWooProductData[product_id][this.tag()]; 4688 let event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 4689 4690 Utils.copyProperties(productData['params'], event.params); 4691 4692 if (productData.hasOwnProperty('pixelIds')) { 4693 event.pixelIds = productData['pixelIds']; 4694 } 4695 4696 this.fireEvent(event.name, event); 4697 }, 4698 4699 /** 4700 * Add to cart from a single product page, where the visitor picks the 4701 * quantity. 4702 */ 4703 onWooAddToCartOnSingleEvent: function (product_id, qty, product_type, is_external, $form) { 4704 4705 if (!options.dynamicEvents.woo_add_to_cart_on_button_click.hasOwnProperty(this.tag())) { 4706 return; 4707 } 4708 if (!window.pysWooProductData || !window.pysWooProductData.hasOwnProperty(product_id)) { 4709 return; 4710 } 4711 if (!window.pysWooProductData[product_id].hasOwnProperty(this.tag())) { 4712 return; 4713 } 4714 4715 let event = Utils.clone(options.dynamicEvents.woo_add_to_cart_on_button_click[this.tag()]); 4716 4717 Utils.copyProperties(window.pysWooProductData[product_id][this.tag()]['params'], event.params); 4718 4719 if (qty && event.params && event.params.data && Array.isArray(event.params.data.contents)) { 4720 event.params.data.contents.forEach(function (item) { 4721 item.quantity = qty; 4722 }); 4723 } 4724 4725 this.fireEvent(event.name, event); 4726 }, 4727 4728 /** 4729 * Add to cart from an EDD button. Downloads with price variations are 4730 * keyed as "<download_id>_<price_index>". 4731 */ 4732 onEddAddToCartOnButtonEvent: function (download_id, price_index, qty) { 4733 4734 if (!options.dynamicEvents.edd_add_to_cart_on_button_click.hasOwnProperty(this.tag())) { 4735 return; 4736 } 4737 if (!window.pysEddProductData || !window.pysEddProductData.hasOwnProperty(download_id)) { 4738 return; 4739 } 4740 4741 let index = price_index ? download_id + '_' + price_index : download_id; 4742 4743 if (!window.pysEddProductData[download_id].hasOwnProperty(index)) { 4744 return; 4745 } 4746 if (!window.pysEddProductData[download_id][index].hasOwnProperty(this.tag())) { 4747 return; 4748 } 4749 4750 let event = Utils.clone(options.dynamicEvents.edd_add_to_cart_on_button_click[this.tag()]); 4751 4752 Utils.copyProperties(window.pysEddProductData[download_id][index][this.tag()].params, event.params); 4753 4754 if (qty && event.params && event.params.data && Array.isArray(event.params.data.contents)) { 4755 event.params.data.contents.forEach(function (item) { 4756 item.quantity = qty; 4757 }); 4758 } 4759 4760 this.fireEvent(event.name, event); 4761 }, 4762 }; 4763 4764 }(options); 4765 4766 var getPixelBySlag = function getPixelBySlag(slug) { 4767 switch (slug) { 4768 case "facebook": return window.pys.Facebook; 4769 case "ga": return window.pys.Analytics; 4770 case "gtm": return window.pys.GTM; 4771 case "bing": return window.pys.Bing; 4772 case "pinterest": return window.pys.Pinterest; 4773 case "reddit": return window.pys.Reddit; 4774 case "openai": return window.pys.OpenAI; 4775 } 4776 } 4777
4778 window.pys = window.pys || {}; 4779 window.pys.Facebook = Facebook; 4780 window.pys.Analytics = Analytics; 4781 window.pys.GTM = GTM; 4782 window.pys.OpenAI = OpenAI; 4783 window.pys.Utils = Utils; 4784 window.pys.getPixelBySlag = getPixelBySlag; 4785 4786 // Allow external scripts (e.g. generatePixelCode fragments) to refresh 4787 // the uniqueId cache for a given event key, so that each new add_to_cart 4788 // action gets a fresh shared ID across all pixels. 4789 window.pys.setEventUniqueId = function(idKey, newId) { 4790 uniqueId[idKey] = newId; 4791 }; 4792 4793 // Export getPixelBySlag globally for backward compatibility 4794 window.getPixelBySlag = getPixelBySlag; 4795 4796 4797 function pysNormalInit() { 4798 4799 if($("#pys_late_event").length > 0) { 4800 var dirAttr = $("#pys_late_event").attr("dir"); 4801 if (dirAttr) { 4802 try { 4803 var events = JSON.parse(dirAttr); 4804 } catch (e) { 4805 console.warn("Invalid JSON in pys_late_event dir attribute:", e); 4806 } 4807 } else { 4808 console.warn("pys_late_event dir attribute is undefined or empty"); 4809 } 4810 if (events) { 4811 for (var key in events) { 4812 var event = {}; 4813 event[events[key].e_id] = [events[key]]; 4814 if (options.staticEvents.hasOwnProperty(key)) { 4815 Object.assign(options.staticEvents[key], event); 4816 } else { 4817 options.staticEvents[key] = event; 4818 } 4819 } 4820 } 4821 } 4822 4823 var Pinterest = Utils.setupPinterestObject(); 4824 var Bing = Utils.setupBingObject(); 4825 var Reddit = Utils.setupRedditObject(); 4826 4827 if(options.hasOwnProperty('cookie')) 4828 { 4829 if(options.cookie.externalID_disabled_by_api || options.cookie.disabled_all_cookie) 4830 { 4831 Cookies.remove('pbid', { path: '/',domain: cookieDomain() }) 4832 } 4833 if(options.cookie.disabled_advanced_form_data_cookie || options.cookie.disabled_all_cookie) 4834 { 4835 // Removed twice on purpose: PHP writes this cookie host-only 4836 // until pys_cd is reported (or permanently, on a site whose 4837 // front end never reaches manageCookies()), while this script 4838 // writes/deletes it at cookieDomain(). A host-only cookie and 4839 // a domain-scoped one of the same name are separate jar 4840 // entries, and a delete only matches the scope it names â so 4841 // both scopes are cleared rather than guessed at. 4842 // `domain: ''` is deliberate: js-cookie 2.1.3 treats a falsy 4843 // Domain as "omit the attribute" (host-only delete), so it's 4844 // written explicitly rather than left off. 4845 // In Free this matters more than in Pro: there is no JS 4846 // writer for this cookie at all, so PHP is its only writer 4847 // and the host-only copy is the only copy a pending site 4848 // ever has. 4849 Cookies.remove('pys_advanced_form_data', { path: '/',domain: cookieDomain() }) 4850 Cookies.remove('pys_advanced_form_data', { path: '/',domain: '' }) 4851 } 4852 if(options.cookie.disabled_landing_page_cookie || options.cookie.disabled_all_cookie) 4853 { 4854 Cookies.remove('pys_landing_page', { path: '/',domain: cookieDomain() }) 4855 Cookies.remove('last_pys_landing_page', { path: '/',domain: cookieDomain() }) 4856 } 4857 if(options.cookie.disabled_trafficsource_cookie || options.cookie.disabled_all_cookie) 4858 { 4859 Cookies.remove('pysTrafficSource', { path: '/',domain: cookieDomain() }) 4860 Cookies.remove('last_pysTrafficSource', { path: '/',domain: cookieDomain() }) 4861 } 4862 if(options.cookie.disabled_first_visit_cookie || options.cookie.disabled_all_cookie) 4863 { 4864 Cookies.remove('pys_first_visit', { path: '/',domain: cookieDomain() }) 4865 4866 } 4867 if(options.cookie.disabled_utmTerms_cookie || options.cookie.disabled_all_cookie) 4868 { 4869 $.each(Utils.utmTerms, function (index, name) { 4870 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 4871 }); 4872 $.each(Utils.utmTerms, function (index, name) { 4873 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 4874 }); 4875 } 4876 if(options.cookie.disabled_utmId_cookie || options.cookie.disabled_all_cookie) 4877 { 4878 $.each(Utils.utmId,function(index,name) { 4879 Cookies.remove('pys_' + name, { path: '/',domain: cookieDomain() }) 4880 }) 4881 $.each(Utils.utmId,function(index,name) { 4882 Cookies.remove('last_pys_' + name, { path: '/',domain: cookieDomain() }) 4883 }); 4884 } 4885 } 4886 4887 4888 // Exactly one manageCookies() per init, and under a consent 4889 // integration only once consent is on record. The old fall-through 4890 // ran it a second time under CookieYes, racing two pbid requests, and 4891 // ran it before any consent when no CookieYes cookie existed yet. 4892 // external_id itself does not depend on this: the visitor key is 4893 // computed server-side without a cookie. 4894 var cookiesManaged = false;
4895 4896 if (options.gdpr.cookie_law_info_integration_enabled) { 4897 var cli_cookie = Cookies.get('cookieyes-consent') ?? Cookies.get('viewed_cookie_policy'); 4898 4899 if (typeof cli_cookie !== 'undefined') { 4900 if (cli_cookie === Cookies.get('cookieyes-consent') && getCookieYes('analytics') == 'yes') { 4901 Utils.manageCookies(); 4902 cookiesManaged = true; 4903 } else if (cli_cookie === Cookies.get('viewed_cookie_policy') && cli_cookie == 'yes') { 4904 Utils.manageCookies(); 4905 cookiesManaged = true; 4906 } 4907 } 4908 } 4909 4910 if ( ! cookiesManaged ) { 4911 if ( options.gdpr.consent_magic_integration_enabled && typeof CS_Data !== "undefined" ) { 4912 if ( CS_Data.cs_script_cat.pys == CS_Data.cs_necessary_cat_id || CS_Data.cs_script_cat.pys == 0 ) { 4913 Utils.manageCookies(); 4914 } else if ( Cookies.get( 'cs_enabled_cookie_term' + CS_Data.test_prefix + '_' + CS_Data.cs_script_cat.pys ) == 'yes' ) { 4915 Utils.manageCookies(); 4916 } 4917 } else if ( ! options.gdpr.cookie_law_info_integration_enabled ) { 4918 Utils.manageCookies(); 4919 } 4920 } 4921 4922 Utils.setupGdprCallbacks(); 4923 // page scroll event 4924 if ( options.dynamicEvents.hasOwnProperty("automatic_event_scroll") 4925 ) { 4926 4927 var singlePageScroll = function () { 4928 4929 4930 var docHeight = $(document).height() - $(window).height(); 4931 var isFired = false; 4932 4933 if (options.dynamicEvents.hasOwnProperty("automatic_event_scroll")) { 4934 var pixels = Object.keys(options.dynamicEvents.automatic_event_scroll); 4935 for(var i = 0;i<pixels.length;i++) { 4936 var event = Utils.clone(options.dynamicEvents.automatic_event_scroll[pixels[i]]); 4937 var scroll = Math.round(docHeight * event.scroll_percent / 100)// convert % to absolute positions 4938 4939 if(scroll < $(window).scrollTop()) { 4940 if ( pixels[i] !== 'reddit') { 4941 Utils.copyProperties( Utils.getRequestParam
4941s(), event.params ); 4942 } 4943 getPixelBySlag( pixels[ i ] ).onPageScroll( event ); 4944 isFired = true 4945 } 4946 } 4947 } 4948 if(isFired) { 4949 $(document).off("scroll",singlePageScroll); 4950 } 4951 } 4952 $(document).on("scroll",singlePageScroll); 4953 } 4954 4955 4956 if (options.dynamicEvents.hasOwnProperty("automatic_event_time_on_page")) { 4957 var pixels = Object.keys(options.dynamicEvents.automatic_event_time_on_page); 4958 var time = options.dynamicEvents.automatic_event_time_on_page[pixels[0]].time_on_page; // the same for all pixel 4959 setTimeout(function(){ 4960 for(var i = 0;i<pixels.length;i++) { 4961 var event = Utils.clone(options.dynamicEvents.automatic_event_time_on_page[pixels[i]]); 4962 4963 if ( pixels[i] !== 'reddit') { 4964 Utils.copyProperties(Utils.getRequestParams(), event.params); 4965 } 4966 getPixelBySlag(pixels[i]).onTime(event); 4967 } 4968 },time*1000); 4969 } 4970 4971 // setup Click Event 4972 if (options.dynamicEvents.hasOwnProperty("automatic_event_download")) { 4973 4974 $(document).onFirst('click', 'a, button, input[type="button"], input[type="submit"]', function (e) { 4975 4976 var $elem = $(this); 4977 4978 // Download 4979 if(options.dynamicEvents.hasOwnProperty("automatic_event_download") 4980 ) { 4981 var isFired = false; 4982 if ($elem.is('a')) { 4983 var href = $elem.attr('href'); 4984 if (typeof href !== "string") { 4985 return; 4986 } 4987 href = href.trim(); 4988 var extension = Utils.getLinkExtension(href); 4989 var track_download = false; 4990 4991 if (extension.length > 0) { 4992 4993 if(options.dynamicEvents.hasOwnProperty("automatic_event_download") ) { 4994 var pixels = Object.keys(options.dynamicEvents.automatic_event_download); 4995 for (var i = 0; i < pixels.length; i++) { 4996 var event = Utils.clone(options.dynamicEvents.automatic_event_download[pixels[i]]); 4997 var extensions = event.extensions; 4998 if (extensions.includes(extension)) { 4999 5000 if( pixels[i] == "tiktok" || pixels[i] == "reddit" ) { 5001 getPixelBySlag(pixels[i]).fireEvent(tikEvent.name, event); 5002 } else { 5003 if (options.enable_remove_download_url_param) { 5004 href = stripUrlQuery(href); 5005 } 5006 event.params.download_url = href; 5007 event.params.download_type = extension; 5008 event.params.download_name = Utils.getLinkFilename(href); 5009 getPixelBySlag(pixels[i]).onDownloadEvent(event); 5010 } 5011 5012 isFired = true; 5013 } 5014 } 5015 } 5016 } 5017 } 5018 if(isFired) { // prevent duplicate events on the same element 5019 return; 5020 } 5021 } 5022 }); 5023 } 5024 5025 5026 // setup Dynamic events 5027 $.each(options.triggerEventTypes, function (triggerType, events) { 5028 5029 $.each(events, function (eventId, triggers) { 5030 5031 switch (triggerType) { 5032 case 'url_click': 5033 //@see: Utils.setupURLClickEvents() 5034 break; 5035 5036 case 'css_click': 5037 Utils.setupCSSClickEvents(eventId, triggers); 5038 break; 5039 5040 case 'css_mouseover': 5041 Utils.setupMouseOverClickEvents(eventId, triggers); 5042 break; 5043 5044 case 'scroll_pos': 5045 Utils.setupScrollPosEvents(eventId, triggers); 5046 break; 5047 case 'comment': 5048 Utils.setupCommentEvents(eventId, triggers); 5049 break; 5050 } 5051 5052 }); 5053 5054 }); 5055 // setup WooCommerce events 5056 if (options.woo.enabled) { 5057 5058 // WooCommerce AddToCart 5059 if (options.dynamicEvents.hasOwnProperty("woo_add_to_cart_on_button_click") 5060 && options.woo.hasOwnProperty("addToCartCatchMethod") 5061 && options.woo.addToCartCatchMethod === "add_cart_js" 5062 ) { 5063 5064 // Loop, any kind of "simple" product, except external 5065 $('.add_to_cart_button:not(.product_type_variable,.product_type_bundle,.single_add_to_cart_button)').on("click",function (e) { 5066 5067 var product_id = $(this).data('product_id'); 5068 if (typeof product_id !== 'undefined') { 5069 if (options.dynamicEvents.hasOwnProperty("woo_add_to_cart_on_button_click")) { 5070 var tmpEventID = pys_generate_token(); 5071 $.each(options.dynamicEvents.woo_add_to_cart_on_button_click, function (i, tag) { 5072 tag.eventID = tmpEventID; 5073 // Also refresh the uniqueId cache so server-API path 5074 // (ajaxForServerStaticEvent) gets the same fresh ID. 5075 var idKey = tag.hasOwnProperty('custom_event_post_id') ? tag.custom_event_post_id : tag.e_id; 5076 uniqueId[idKey] = tmpEventID; 5077 }); 5078 } 5079 if (typeof product_id !== 'undefined') { 5080 Facebook.onWooAddToCartOnButtonEvent(product_id); 5081 Analytics.onWooAddToCartOnButtonEvent(product_id); 5082 GTM.onWooAddToCartOnButtonEvent(product_id); 5083 Pinterest.onWooAddToCartOnButtonEvent(product_id); 5084 Bing.onWooAddToCartOnButtonEvent(product_id); 5085 Reddit.onWooAddToCartOnButtonEvent(product_id); 5086 OpenAI.onWooAddToCartOnButtonEvent(product_id); 5087 } 5088 } 5089 }); 5090 5091 // Single Product 5092 // tap try to https://stackoverflow.com/questions/30990967/on-tap-click-event-firing-twice-how-to-avoid-it 5093 // $(document) not work 5094 $('body').onFirst('click','button.single_add_to_cart_button,.single_add_to_cart_button',function (e) { 5095 5096 var $button = $(this); 5097 5098 if ($button.hasClass('disabled')) { 5099 return; 5100 } 5101 5102 var $form = $button.closest('form'); 5103 5104 var product_type = Utils.PRODUCT_SIMPLE; 5105 5106 if ($form.length === 0) { 5107 return ; 5108 } else if ($form.hasClass('variations_form')) { 5109 product_type = Utils.PRODUCT_VARIABLE; 5110 } else if($form.hasClass('bundle_form')) { 5111 product_type = Utils.PRODUCT_BUNDLE; 5112 } else if($form.hasClass('grouped_form')) { 5113 product_type = Utils.PRODUCT_GROUPED; 5114 } 5115 5116 5117 5118 5119 var product_id; 5120 var qty; 5121 5122 if (product_type === Utils.PRODUCT_GROUPED) { 5123 qty = 1; 5124 product_id = parseInt($form.find('*[name="add-to-cart"]').val()); 5125 } else if (product_type === Utils.PRODUCT_VARIABLE) { 5126 product_id = parseInt($form.find('*[name="add-to-cart"]').val()); 5127 var qtyTag = $form.find('input[name="quantity"]'); 5128 if(qtyTag.length <= 0) { 5129 qtyTag = $form.find('select[name="quantity"]'); 5130 } 5131 qty = parseInt(qtyTag.val()); 5132 } else { 5133 product_id = parseInt($form.find('*[name="add-to-cart"]').val()); 5134 var qtyTag = $form.find('input[name="quantity"]'); 5135 if(qtyTag.length <= 0) { 5136 qtyTag = $form.find('select[name="quantity"]'); 5137 } 5138 qty = parseInt(qtyTag.val()); 5139 } 5140 5141 if(options.dynamicEvents.hasOwnProperty("woo_add_to_cart_on_button_click")){ 5142 var tmpEventID = pys_generate_token(); 5143 $.each(options.dynamicEvents.woo_add_to_cart_on_button_click, function (i, tag) { 5144 tag.eventID = tmpEventID; 5145 // Also refresh the uniqueId cache so server-API path 5146 // (ajaxForServerStaticEvent) gets the same fresh ID. 5147 var idKey = tag.hasOwnProperty('custom_event_post_id') ? tag.custom_event_post_id : tag.e_id; 5148 uniqueId[idKey] = tmpEventID; 5149 }); 5150 } 5151 5152 Facebook.onWooAddToCartOnSingleEvent(product_id, qty, product_type, $form); 5153 Analytics.onWooAddToCartOnSingleEvent(product_id, qty, product_type, $form); 5154 5155 GTM.onWooAddToCartOnSingleEvent(product_id, qty, product_type, $form); 5156 5157 Pinterest.onWooAddToCartOnSingleEvent(product_id, qty, product_type, false, $form); 5158 Bing.onWooAddToCartOnSingleEvent(product_id, qty, product_type, false, $form); 5159 Reddit.onWooAddToCartOnSingleEvent(product_id, qty, product_type, false, $form); 5160 OpenAI.onWooAddToCartOnSingleEvent(product_id, qty, product_type, false, $form); 5161 5162 }); 5163 5164 } 5165 5166 // WooCommerce RemoveFromCart 5167 if (options.dynamicEvents.hasOwnProperty("woo_remove_from_cart")) { 5168 5169 $('body').on('click', options.woo.removeFromCartSelector, function (e) { 5170 5171 var $a = $(e.currentTarget), 5172 href = $a.attr('href'); 5173 5174 // extract cart item hash from remove button URL 5175 var regex = new RegExp("[\\?&]remove_item=([^&#]*)"), 5176 results = regex.exec(href); 5177 5178 if (results !== null) { 5179 5180 var item_hash = results[1]; 5181 5182 if (options.dynamicEvents["woo_remove_from_cart"].hasOwnProperty(item_hash)) { 5183 var events = options.dynamicEvents["woo_remove_from_cart"][item_hash]; 5184 Utils.fireEventForAllPixel("onWooRemoveFromCartEvent",events) 5185 } 5186 5187 } 5188 5189 }); 5190 } 5191 } 5192 5193 // setup EDD events 5194 if (options.edd.enabled) { 5195 5196 // EDD AddToCart 5197 if (options.dynamicEvents.hasOwnProperty("edd_add_to_cart_on_button_click")) { 5198 5199 $('form.edd_download_purchase_form .edd-add-to-cart').on("click",function (e) { 5200 5201 var $button = $(this); 5202 var $form = $button.closest('form'); 5203 var variable_price = $button.data('variablePrice'); // yes/no 5204 var price_mode = $button.data('priceMode'); // single/multi 5205 var ids = []; 5206 var quantities = []; 5207 var qty; 5208 var id; 5209 5210 if (variable_price === 'yes' && price_mode === 'multi') { 5211 5212 id = $form.find('input[name="download_id"]').val(); 5213 5214 // get selected variants 5215 $.each($form.find('input[name="edd_options[price_id][]"]:checked'), function (i, el) { 5216 ids.push(id + '_' + $(el).val()); 5217 }); 5218 5219 // get qty for selected variants 5220 $.each(ids, function (i, variant_id) { 5221 5222 var variant_index = variant_id.split('_', 2); 5223 qty = $form.find('input[name="edd_download_quantity_' + variant_index[1] + '"]').val(); 5224 5225 if (typeof qty !== 'undefined') { 5226 quantities.push(qty); 5227 } else { 5228 quantities.push(1); 5229 } 5230 5231 }); 5232 5233 } else if (variable_price === 'yes' && price_mode === 'single') { 5234 5235 id = $form.find('input[name="download_id"]').val(); 5236 ids.push(id + '_' + $form.find('input[name="edd_options[price_id][]"]:checked').val()); 5237 5238 qty = $form.find('input[name="edd_download_quantity"]').val(); 5239 5240 if (typeof qty !== 'undefined') { 5241 quantities.push(qty); 5242 } else { 5243 quantities.push(1); 5244 } 5245 5246 } else { 5247 5248 ids.push($button.data('downloadId')); 5249 5250 qty = $form.find('input[name="edd_download_quantity"]').val(); 5251 5252 if (typeof qty !== 'undefined') { 5253 quantities.push(qty); 5254 } else { 5255 quantities.push(1); 5256 } 5257 } 5258 5259 // fire event for each download/variant 5260 $.each(ids, function (i, download_id) { 5261 5262 var q = parseInt(quantities[i]); 5263 var variant_index = download_id.toString().split('_', 2); 5264 var price_index; 5265 5266 if (variant_index.length === 2) { 5267 download_id = variant_index[0]; 5268 price_index = variant_index[1]; 5269 } 5270 5271 Facebook.onEddAddToCartOnButtonEvent(download_id, price_index, q); 5272 Analytics.onEddAddToCartOnButtonEvent(download_id, price_index, q); 5273 GTM.onEddAddToCartOnButtonEvent(download_id, price_index, q); 5274 Pinterest.onEddAddToCartOnButtonEvent(download_id, price_index, q); 5275 Bing.onEddAddToCartOnButtonEvent(download_id, price_index, q); 5276 Reddit.onEddAddToCartOnButtonEvent(download_id, price_index, q); 5277 OpenAI.onEddAddToCartOnButtonEvent(download_id, price_index, q); 5278 }); 5279 5280 }); 5281 5282 } 5283 5284 5285 // EDD RemoveFromCart 5286 if (options.dynamicEvents.hasOwnProperty("edd_remove_from_cart") ) { 5287 5288 $('form#edd_checkout_cart_form .edd_cart_remove_item_btn').on("click",function (e) { 5289 5290 var href = $(this).attr('href'); 5291 if(href) { 5292 var key = href.substring(href.indexOf('=') + 1).charAt(0); 5293 if (options.dynamicEvents.edd_remove_from_cart.hasOwnProperty(key)) { 5294 var events = options.dynamicEvents.edd_remove_from_cart[key]; 5295 Utils.fireEventForAllPixel("onEddRemoveFromCartEvent",events) 5296 } 5297 } 5298 }); 5299 5300 } 5301 5302 } 5303 5304 // setup Comment Event 5305 if (options.dynamicEvents.hasOwnProperty("automatic_event_c
5305omment") 5306 ) { 5307 5308 $('form.comment-form').on("submit",function () { 5309 if (options.dynamicEvents.hasOwnProperty("automatic_event_comment")) { 5310 var pixels = Object.keys(options.dynamicEvents.automatic_event_comment); 5311 for (var i = 0; i < pixels.length; i++) { 5312 var event = Utils.clone(options.dynamicEvents.automatic_event_comment[pixels[i]]); 5313 if ( pixels[i] !== 'reddit') { 5314 Utils.copyProperties(Utils.getRequestParams(), event.params); 5315 } 5316 getPixelBySlag(pixels[i]).onCommentEvent(event); 5317 } 5318 } 5319 }); 5320 5321 } 5322 5323 5324 // setup Form Event 5325 if ( options.dynamicEvents.hasOwnProperty("automatic_event_form")) { 5326 5327 $(document).onFirst('submit', 'form', function (e) { 5328 5329 var $form = $(this); 5330 5331 // exclude WP forms 5332 const formClass = $form.attr('class') || ''; 5333 const formRole = ($form.attr('role') || '').toLowerCase(); 5334 const formId = $form.attr('id'); 5335 5336 if ( 5337 $form.hasClass('comment-form') || 5338 $form.hasClass('search-form') || 5339 $form.hasClass('woocommerce-ordering') || 5340 formClass.toLowerCase().includes('search') || formClass.toLowerCase().includes('add-to-cart') || 5341 formRole === 'search' || 5342 formId === 'adminbarsearch' 5343 ) { 5344 return; 5345 } 5346 5347 // exclude Woo forms 5348 if ($form.hasClass('woocommerce-product-search') || $form.hasClass('cart') || $form.hasClass('woocommerce-cart-form') || 5349 $form.hasClass('woocommerce-shipping-calculator') || $form.hasClass('checkout') || $form.hasClass('checkout_coupon')) { 5350 return; 5351 } 5352 5353 // exclude EDD forms 5354 if ($form.hasClass('edd_form') || $form.hasClass('edd_download_purchase_form')) { 5355 return; 5356 } 5357// exclude CF7 forms 5358 if ($form.hasClass('wpcf7-form')) { 5359 return; 5360 } 5361 // exclude Forminator forms 5362 if ($form.hasClass('forminator-custom-form') || $form.hasClass('forminator_ajax')) { 5363 return; 5364 } 5365 // exclude WPforms forms 5366 if ($form.hasClass('wpforms-form') || $form.hasClass('wpforms-ajax-form')) { 5367 return; 5368 } 5369 // exclude Formidable forms 5370 /*if ($form.hasClass('frm-show-form')) { 5371 return; 5372 }*/ 5373 // exclude Ninja Forms forms 5374 if ($form.parent().hasClass('nf-form-layout')) { 5375 return; 5376 } 5377 // exclude Fluent forms 5378 if ($form.hasClass('frm-fluent-form')) { 5379 return; 5380 } 5381 if(!options.enable_success_send_form) { 5382 var params = { 5383 form_id: $form.attr('id'), 5384 form_class: $form.attr('class'), 5385 text: $form.find('[type="submit"]').is('input') ? 5386 $form.find('[type="submit"]').val() : $form.find('[type="submit"]').text() 5387 }; 5388 5389 if (options.dynamicEvents.hasOwnProperty("automatic_event_form")) { 5390 var pixels = Object.keys(options.dynamicEvents.automatic_event_form); 5391 for (var i = 0; i < pixels.length; i++) { 5392 var event = Utils.clone(options.dynamicEvents.automatic_event_form[pixels[i]]); 5393 5394 if (pixels[i] === "tiktok" || pixels[i] === "reddit") { 5395 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 5396 } else { 5397 Utils.copyProperties(params, event.params,) 5398 Utils.copyProperties(Utils.getRequestParam
5398s(), event.params); 5399 getPixelBySlag(pixels[i]).onFormEvent(event); 5400 } 5401 } 5402 } 5403 } 5404 }); 5405 5406 document.addEventListener( 'wpcf7mailsent', function( event ) { 5407 var form_id = event.detail.contactFormId; 5408 sendFormAction($(event.target), form_id); 5409 }, false ); 5410 //Forminator 5411 $(document).on( 'forminator:form:submit:success', function( event ){ 5412 var form_id = $(event.target).find('input[name="form_id"]').val(); 5413 sendFormAction($(event.target), form_id); 5414 }); 5415 5416 //WPForm 5417 $('form.wpforms-form').on('wpformsAjaxSubmitSuccess', (event) => { 5418 var form_id = $(event.target).attr('data-formid'); 5419 sendFormAction($(event.target), form_id); 5420 }) 5421 $(document).on( 'frmFormComplete', function( event, form, response ) { 5422 const form_id = $(form).find('input[name="form_id"]').val(); 5423 sendFormAction($(event.target), form_id); 5424 }); 5425 // Ninja Forms 5426 $(document).onFirst('nfFormSubmitResponse', function (event, data) { 5427 const form_id = data.response.data.form_id; 5428 sendFormAction($(event.target), form_id); 5429 }); 5430 5431 var fluentForms = $('form.frm-fluent-form'); 5432 fluentForms.each(function() { 5433 var $form = $(this); 5434 $form.on('fluentform_submission_success', function(event) { 5435 var $formItem = $(this); 5436 var form_id = $formItem.attr('data-form_id'); 5437 sendFormAction($(event.target), form_id); 5438 }); 5439 }); 5440 5441 } 5442 5443 5444 // load pixel APIs 5445 Utils.loadPixels(); 5446 // setup Enrich content 5447 if(Utils.isCheckoutPage()) { 5448 Utils.addCheckoutFields(); 5449 } 5450 } 5451 5452 $(document).ready(function () { 5453 if ( options.dynamicDataUrl ) { 5454 fetch( options.dynamicDataUrl, { 5455 method: 'GET', 5456 credentials: 'same-origin', 5457 cache: 'no-store', 5458 headers: { 'Accept': 'application/json' } 5459 } ) 5460 .then( function(r) { return r.ok ? r.json() : Promise.reject(r.status); } ) 5461 .then( function(dynamic) { 5462 options.ajax_event = dynamic.ajax_event; 5463 options.cache_bypass = dynamic.cache_bypass; 5464 options.tracking_analytics = dynamic.tracking_analytics; 5465 if ( dynamic.gdpr_dynamic && options.gdpr ) { 5466 Object.assign( options.gdpr, dynamic.gdpr_dynamic ); 5467 if ( dynamic.gdpr_dynamic.analytics_storage_value !== null ) 5468 options.gdpr.analytics_storage.value = dynamic.gdpr_dynamic.analytics_storage_value; 5469 if ( dynamic.gdpr_dynamic.ad_storage_value !== null ) 5470 options.gdpr.ad_storage.value = dynamic.gdpr_dynamic.ad_storage_value; 5471 if ( dynamic.gdpr_dynamic.ad_user_data_value !== null ) 5472 options.gdpr.ad_user_data.value = dynamic.gdpr_dynamic.ad_user_data_value; 5473 if ( dynamic.gdpr_dynamic.ad_personalization_value !== null ) 5474 options.gdpr.ad_personalization.value = dynamic.gdpr_dynamic.ad_personalization_value; 5475 } 5476 options.cookie = dynamic.cookie; 5477 5478 // Per-pixel user data (advanced matching) that was emptied out of 5479 // the cached HTML. An inline value that still holds something wins: 5480 // on pages we never strip (order received / EDD success) the HTML 5481 // carries the order's own data, which beats the account-level data 5482 // this endpoint can see. 5483 if ( dynamic.pixels ) { 5484 for ( var slug in dynamic.pixels ) { 5485 if ( ! dynamic.pixels.hasOwnProperty( slug ) || ! options[ slug ] ) continue; 5486 for ( var key in dynamic.pixels[ slug ] ) { 5487 if ( ! dynamic.pixels[ slug ].hasOwnProperty( key ) ) continue; 5488 var inline = options[ slug ][ key ]; 5489 if ( ! inline || Object.keys( inline ).length === 0 ) { 5490 options[ slug ][ key ] = dynamic.pixels[ slug ][ key ]; 5491 } 5492 } 5493 } 5494 } 5495 5496 options.dynamicDataApplied = true; 5497 5498 if ( window.pys && window.pys.OpenAI 5499 && typeof window.pys.OpenAI.refreshAdvancedMatching === 'function' ) { 5500 window.pys.OpenAI.refreshAdvancedMatching(); 5501 } 5502 5503 pysNormalInit(); 5504 } ) 5505 .catch( function(err) { 5506 if ( options.debug ) console.warn( '[PYS] dynamic-options fetch failed:', err ); 5507 options.dynamicDataApplied = true; 5508 pysNormalInit(); 5509 } ); 5510 } else { 5511 pysNormalInit(); 5512 } 5513 }); 5514 5515 var sendFormAction = function (form_target, formId){ 5516 var params = { 5517 form_id: formId, 5518 text: form_target.find('[type="submit"]').is('input') ? form_target.find('[type="submit"]').val() : 5519 form_target.find('.forminator-button-submit').text() != '' ? form_target.find('.forminator-button-submit').text() : 5520 form_target.find('[type="submit"]').text() 5521 }; 5522 5523 if (options.dynamicEvents.hasOwnProperty("automatic_event_form")) { 5524 var pixels = Object.keys(options.dynamicEvents.automatic_event_form); 5525 for (var i = 0; i < pixels.length; i++) { 5526 var event = options.dynamicEvents.automatic_event_form[pixels[i]]; 5527 if (pixels[i] === "tiktok" || pixels[i] === "reddit") { 5528 getPixelBySlag(pixels[i]).fireEvent(event.name, event); 5529 } else { 5530 Utils.copyProperties(params, event.params)
5531 Utils.copyProperties(Utils.getRequestParams(), event.params); 5532 getPixelBySlag(pixels[i]).onFormEvent(event); 5533 } 5534 } 5535 } 5536 } 5537 5538}(jQuery, pysOptions); 5539 5540function pys_generate_token() { 5541 return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 5542 (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 5543 ); 5544} 5545 5546function getBundlePriceOnSingleProduct(data) { 5547 var items_sum = 0; 5548 jQuery(".bundle_form .bundled_product").each(function(index){ 5549 var id = jQuery(this).find(".cart").data("bundled_item_id"); 5550 var item_price = data.prices[id]; 5551 var item_quantity = jQuery(this).find(".bundled_qty").val(); 5552 if(!jQuery(this).hasClass("bundled_item_optional") || 5553 jQuery(this).find(".bundled_product_optional_checkbox input").prop('checked')) { 5554 items_sum += item_price*item_quantity; 5555 } 5556 }); 5557 return items_sum; 5558} 5559function getUrlParameter(sParam) { 5560 var sPageURL = window.location.search.substring(1), 5561 sURLVariables = sPageURL.split('&'), 5562 sParameterName, 5563 i; 5564 5565 for (i = 0; i < sURLVariables.length; i++) { 5566 sParameterName = sURLVariables[i].split('='); 5567 5568 if (sParameterName[0] === sParam) { 5569 // Twin of ServerEventHelper::getUrlParameter(). A parameter with 5570 // no '=' has no value; skip it rather than inventing one, so 5571 // ?fbclid&fbclid=REAL still finds the real one. 5572 if (sParameterName[1] === undefined) { 5573 continue; 5574 } 5575 try { 5576 return decodeURIComponent(sParameterName[1]); 5577 } catch (e) { 5578 // decodeURIComponent throws on a malformed escape ("%", 5579 // "%zz", "a%2"). Return the raw value instead of throwing, 5580 // matching PHP's urldecode(), which is lenient about it. 5581 return sParameterName[1]; 5582 } 5583 } 5584 } 5585 return false; 5586}; 5587function getCookieYes(key) { 5588 const cookiesObj = document.cookie 5589 .split(";") 5590 .reduce((ac, cv) => { 5591 const [k, v] = cv.split("="); 5592 if (k && v) ac[k.trim()] = v; 5593 return ac; 5594 }, {}); 5595 const consentCookie = cookiesObj["cookieyes-consent"] || cookiesObj["wt_consent"]; 5596 if (!consentCookie) return undefined; 5597 const { [key]: value } = consentCookie 5598 .split(",") 5599 .reduce((obj, pair) => { 5600 const [k, v] = pair.split(":"); 5601 if (k && v) obj[k] = v; 5602 return obj; 5603 }, {}); 5604 return value; 5605} 5606function getRootDomain(useSubdomain = false) { 5607 const hostname = window.location.hostname; // Get the current hostname 5608 // Check if tldjs is defined before using it 5609 if (typeof tldjs === "undefined") { 5610 console.warn("tldjs is not defined"); 5611 return hostname; // Return hostname as a fallback 5612 } 5613 5614 const rootDomain = tldjs.getDomain(hostname); // Use tldjs to extract the root domain 5615 5616 // Return the root domain with or without a leading dot based on useSubdomain 5617 return rootDomain ? (useSubdomain ? '.' + rootDomain : rootDomain) : hostname; 5618}
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.