PageSourceSearch

https://www.vamoos.com/wp-content/plugins/pixelyoursite/dist/scripts/public.js

js vamoos.com collected 2026-10-01 14:15:48 UTC 238,975 bytes, 5,618 lines download raw bytes

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 += '&gtm_auth=' + gtm_auth + '&gtm_preview=' + gtm_preview + '&gtm_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.