PageSourceSearch

https://www.bangordailynews.com/wp-content/plugins/bdn-paywall/includes/client.js?ts=1790412636&ver=1790412636

js bangordailynews.com collected 2026-10-01 08:30:43 UTC 78,486 bytes, 1,698 lines download raw bytes

1//document.querySelectorAll(`svg,div:not([class*="background"]`)
2
3class BDN_Paywall {
4
5    debugOutput = false;
6    debugTrace = false;
7    metadata = { };
8    pixels = [ ];
9    uuid = "";
10    urlParams = new URLSearchParams(window.location.search);
11
12
13
14    failSafeTimer = 0;
15
16    // List of target classes to check for the Please dont subscribe twice notification
17    targetClasses = [
18        'pelcro-payment-container'
19    ];
20    viafouraToggle = true; // disabled for ads testing
21    isGamePage = false;
22
23    // STN video player (Voltax) — set false, or comment out the
24    // this.injectSTNPlayer() call in decision(), to disable during testing.
25    stnPlayerToggle = true;
26    stnPlayerFired = false;
27
28    constructor(metadata) {
29        
30        this.hasFiredAlert = false;
31
32        document.addEventListener('DOMContentLoaded', () => {
33            
34
35            const observer = new MutationObserver((mutations) => {
36                mutations.forEach((mutation) => {
37                    console.log("DUPE TEST", this.metadata.subscriberFlag, !this.urlParams.has('view','upgrade-flow'));
38                    if (
39                        (document.querySelector('.pelcro-payment-container') || 
40                        document.querySelector('.pelcro-subscription-create-view')) &&
41                        this.metadata.subscriberFlag &&
42                        !this.hasFiredAlert &&
43                        !this.urlParams.has('view','upgrade-flow')
44                    ) {
45                        console.log("Payment or subscription create modal detected, showing alert");
46                        this.showDupeAlert();
47                        this.hasFiredAlert = true; // Set flag to prevent repeats
48                        observer.disconnect(); // Stop observing
49                    }
50                });
51            });
52      
53            // Start observing the DOM
54            observer.observe(document.body, {
55                childList: true,
56                subtree: true,
57            });
58        
59            // Load Trinity Audio player (always, to ensure it's ready before paywall decision)
60            //this.loadTrinity();  
61
62            //this.loadGistAIWidget();
63        });
64
65        // Add smooth scroll CSS for Engagement Starter
66        const style = document.createElement('style');
67        style.textContent = `
68            html {
69                scroll-behavior: smooth;
70            }
71        `;
72        document.head.appendChild(style);
73        if (this.debugOutput) console.log('Injected smooth scroll CSS');
74        
75
76        const isPost = document.querySelector('article[id^="post-"]');
77        if (isPost && this.viafouraToggle) {
78            this.injectSingleCommentCountWidget();
79        }
80
81        //this.debugOutput = this.urlParams.has("bdnpaywalldebug");
82        this.debugOutput = true;
83        this.debugTrace = this.urlParams.has("bdnpaywalltrace");
84
85        this.metadata = metadata;
86        this.uuid = crypto.randomUUID().split("-")[0];
87
88
89        // Check serverUser for valid subscriber data
90        if (this.debugOutput) console.log("serverUser", this.metadata);
91        if (this.metadata.serverUser && Object.keys(this.metadata.serverUser).length > 0) {
92            if (this.debugOutput) console.log("Using server-provided user data:", this.metadata.serverUser);
93            this.metadata.userObj = this.metadata.serverUser;
94
95            // Check adlite entitlement immediately
96            this.checkAdliteEntitlement();
97            // Check for server-provided subscriber status
98            if (this.metadata.serverUser.isServerSubscriber) {
99                if (this.debugOutput) console.log("Server user is a subscriber, setting subscriberFlag");
100                this.metadata.subscriberFlag = true;
101                // Trigger decision immediately to skip paywall
102                this.decision("server-user");
103            }
104        }
105        
106
107        if(this.debugTrace) {
108            console.log("BDN Paywall Stack Trace:");
109            console.trace();
110        }
111
112        if(this.debugOutput) {
113            console.log("BDN Paywall Init!");
114            console.log(`Window reference set to: ${this.uuid}`);
115        }
116
117        //this line just stores the class instance in a variable in the window object
118        //that is obsure and not obvious so the garbage collector doesn't free it
119        //and moves around on the end user for obfuscation
120        window[this.uuid] = [ [ { "stash" : this } ] ];
121
122        //check to see if the paywall function has not run, if it has not run then stand up the listener event
123        if(this.metadata.paywallRunCheck == "0") {
124            //load the event listner of we need to add an event for PelcroSiteLoaded
125            document.addEventListener("PelcroSiteLoaded", this, false);
126            document.addEventListener("PelcroUserLoaded", this, false);
127            document.addEventListener("PelcroUserLoggedIn", this, false);
128        }
129
130        // Trigger Viafoura logout on Pelcro user logout
131        document.addEventListener("PelcroUserLogout", () => {
132            this.logout();
133            if (window.vf && window.vf.session && window.vf.session.logout) {
134                if (this.debugOutput) console.log('Triggering Viafoura logout on PelcroUserLogout');
135                let viafouraDivs = document.querySelectorAll('.viafoura');
136                viafouraDivs.forEach(div => {
137                    div.style.display = 'none';
138                    if (this.debugOutput) console.log('Hid Viafoura widget:', div);
139                });
140                window.vf.session.logout();
141            } else {
142                if (this.debugOutput) console.error('Cannot trigger Viafoura logout: window.vf.session.logout unavailable');
143            }
144        });
145
146        //failsave to make sure that the decision gets call if all else fails
147        this.failSafeTimer = setTimeout(() => {
148            if(this.debugOutput) console.log("Calling Decision From Failsafe Timer!");
149            this.failSafeTimer = 0;
150            this.decision("failsafe-timer");
151        }, 10000);
152    }
153
154    injectAdliteMetaTags() {
155
156        if (this.debugOutput) console.log('NMRAIL');
157        const target = document.querySelector('#top-posts-3');
158        if (target) {
159            const newDiv = document.createElement('div');
160            newDiv.id = 'nmrail';
161            target.parentNode.insertBefore(newDiv, target.nextSibling);
162        }
163
164
165
166        if (this.debugOutput) console.log("Injecting adlite meta tags");
167        const head = document.head || document.getElementsByTagName('head')[0];
168        
169        const existingMeta = document.querySelector('meta[name="adexperience"][content="lite"]');
170        if (existingMeta) {
171            if (this.debugOutput) console.log("Meta tag with name=adexperience, content=lite already exists, skipping injection");
172            return;
173        }
174
175        const meta1 = document.createElement('meta');
176        meta1.name = "adexperience";
177        meta1.content = "adlite";
178        //head.appendChild(meta1);
179        //if (this.debugOutput) console.log("Injected meta tag: name=adexperience, content=adlite");
180
181        const meta2 = document.createElement('meta');
182        meta2.name = "adexperience";
183        meta2.content = "lite";
184        head.appendChild(meta2);
185        if (this.debugOutput) console.log("Injected meta tag: name=adexperience, content=lite");
186
187        document.cookie = "bdn_subscriber=1; path=/; SameSite=Strict; Secure";
188    }
189
190    showDupeAlert() {
191        console.log("Dupe Event FIREDDDDDDDDDDXD!!!!!!!");
192        // Create overlay
193        const overlay = document.createElement('div');
194        overlay.id = 'bdn-paywall-overlay';
195        overlay.style.cssText = `
196            position: fixed;
197            top: 0;
198            left: 0;
199            width: 100%;
200            height: 100%;
201            background: rgba(0, 0, 0, 0.5); /* Semi-transparent gray */
202            z-index: 9998; /* Below modal, above everything else */
203        `;
204    
205        // Create modal
206        const modal = document.createElement('div');
207        modal.id = 'bdn-paywall-modal';
208        modal.style.cssText = `
209            position: fixed;
210            top: 45%;
211            left: 50%;
212            transform: translate(-50%, -50%);
213            width: 300px;
214            background: #fff;
215            padding: 20px;
216            border-radius: 8px;
217            box-shadow: 0 8px 38px rgba(0, 0, 0, 0.5);
218            z-index: 9999999999; /* Above overlay */
219        `;
220    
221        modal.innerHTML = `
222            <div class="test-modal">
223                <h2>Your current digital subscription is already active.</h2>
224                <h2> Thank you for supporting Bangor Daily News!</h2>
225                <div style="text-align: center; margin-top: 20px;">
226                    <button id="bdn-proceed-anyway" style="background: #28b45e; color: #fff; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 16px;">Proceed anyway</button>
227                </div>
228                <p><a href="https://www.bangordailynews.com/" style="color: #28b45e; text-decoration: underline;">Click Here</a> to return to the BDN homepage.</p>
229            </div>
230        `;
231    
232        // Append both to body
233        document.body.appendChild(overlay);
234        document.body.appendChild(modal);
235    
236        // Close our custom modal and overlay
237        const proceedButton = document.getElementById('bdn-proceed-anyway');
238        proceedButton.addEventListener('click', () => {
239            console.log("Proceed anyway clicked, closing custom modal");
240            modal.remove();
241            overlay.remove();
242        });
243    }
244
245    checkTargetClasses() {
246        console.log("check target class check")
247        for (const targetClass of this.targetClasses) {
248            // Check for buttons only (button element or elements with type="button")
249            const buttons = document.querySelectorAll(`button.${targetClass}, [type="button"].${targetClass}`);
250            
251            if (buttons.length >
251 0 && !this.urlParams.has('has','upgrade-flow')) {
252                console.log(`Pelcro Subscribe button found: ${targetClass}`);
253                this.showDupeAlert(); // Trigger the modal if any matching button is found
254                break; // Stop after the first match
255            }
256        }
257    }
258
259
260    handleEvent(event) {
261       
262        if(this.debugTrace) {
263            console.log("BDN Paywall Stack Trace:");
264            console.trace();
265        }
266
267        if(this.debugOutput) {
268            console.log("Event Data:");
269            console.log(event);
270        }
271
272        switch(event.type) {
273
274            case "PelcroSiteLoaded":
275
276                if(this.debugOutput) console.log("Pelcro Site Loaded Event: ");
277
278                const isAuthenticated = window.Pelcro.user.isAuthenticated();                 
279                const user = window.Pelcro.user.read(); 
280
281                if (!isAuthenticated || !user)  {
282                    if(this.debugOutput) console.log("===================>Pelcro is fully loaded, and the user is not authenticated."); 
283                    this.decision("site-loaded-notloggedin");
284                } else { 
285                    if(this.debugOutput) console.log("===================>User is authenticated:");
286                    if(this.debugOutput) console.log(user); 
287
288                    this.metadata.isAuthenticated = isAuthenticated;
289
290                    if(Object.keys(user).length > 0) {
291                        //user and site are ready
292                        if(this.debugOutput) console.log("Pelcro User Available On Site Load!");
293
294                        this.metadata.userObj = user;
295                        clearTimeout(this.failSafeTimer);
296                        this.failSafeTimer = 0;
297
298                        this.decision("site-loaded-loggedin");
299                    } else {
300                        //the user may not be null, but it isn't ready yet
301                        //additional functionality will be called on the PelcroUserLoaded event
302                        if(this.debugOutput) console.log("Pelcro User NOT Available On Site Load!");
303                    }
304                }
305                break;
306
307            case "PelcroUserLoaded":
308
309
310
311                if(this.debugOutput) console.log("Pelcro User Loaded Event: user/vf ");
312                this.metadata.userObj = event.detail.data;
313
314                this.checkAdliteEntitlement();
315                
316                // Inject Viafoura div, load script, and hide default comments
317                // VF init
318
319                if (window.Pelcro.user.auth && this.metadata.isCommenting !== 'closed' && this.viafouraToggle) {
320                    if (this.debugOutput) {
321                        console.log('user/vf check', window.Pelcro.user.auth);
322                        console.log('user/vf check', (window.Pelcro.user.auth && this.metadata.isCommenting !== 'closed'));
323                        console.log('Page URL:', window.location.href);
324                    }
325
326                    let postId = null;
327                    const postElement = document.querySelector('article[id^="post-"]');
328                    if (postElement && postElement.id) {
329                        postId = postElement.id.replace('post-', '');
330                    }
331                    if (!postId) {
332                        if (this.debugOutput) console.error('Could not find WordPress post ID for Viafoura container');
333                        postId = 'default';
334                    }
335
336                    if (postElement && postElement.id && postId !== 'default' && postElement.classList.contains('type-post') && this.metadata.isCommenting === 'open') {
337                        const mainContentDiv = document.querySelector('.main-content');
338                        if (mainContentDiv) {
339                            // Skip conversation widget injection 
340                            if (!document.querySelector(`#viafoura-comments-${postId}`)) {
341                                const viafouraDiv = document.createElement('div');
342                                viafouraDiv.className = 'viafoura';
343                                viafouraDiv.id = `viafoura-comments-${postId}`;
344                                viafouraDiv.innerHTML = `
345                                    <vf-livequestions vf-container-id="viafoura-comments-${postId}"></vf-livequestions>
346                                    <vf-conversations-count></vf-conversations-count>
347                                    <vf-tray-trigger floating="true"></vf-tray-trigger>
348                                    <vf-conversations vf-container-id="viafoura-comments-${postId}"></vf-conversations>
349                                `;
350                                mainContentDiv.appendChild(viafouraDiv);
351                                if (this.debugOutput) console.log(`Injected Viafoura conversation widget with vf-container-id="viafoura-comments-${postId}" into .main-content`);
352                            } else {
353                                if (this.debugOutput) console.log(`Skipping Viafoura conversation injection: already present with id="viafoura-comments-${postId}"`);
354                            }
355
356
357                            // Inject Vertical Trending Articles in Sidebar
358                            const sidebarBlock = document.querySelector('#block-18.widget.widget_block');
359
360                            if (sidebarBlock && postId !== 'default') {
361                                const sectionWrapper = document.createElement('section');
362                                sectionWrapper.id = 'bdn-trending-sidebar';
363                                sectionWrapper.className = 'widget widget_block bdn-sidebar-trending'; // Add custom classes for styling if needed
364
365                                const trendingDiv = document.createElement('div');
366                                trendingDiv.className = 'viafoura';
367                                trendingDiv.innerHTML = `
368                                    <vf-trending-articles
369                                        title="Join The Conversation"
370                                        limit="5"
371                                        days-published="7"
372                                        trend-window="1"
373                                        sort="comments"
374                                        view="full"
375                                        vf-container-id="viafoura-comments-${postId}"
376                                    ></vf-trending-articles>
377                                `;
378                                sectionWrapper.appendChild(trendingDiv);
379
380                                sidebarBlock.insertAdjacentElement('beforebegin', sectionWrapper);
381                                if (this.debugOutput) console.log(`Injected vertical Trending Articles section above #block-18 with vf-container-id="viafoura-comments-${postId}"`);
382                            } else {
383                                if (this.debugOutput) console.warn('Sidebar block (#block-18) not found or invalid post ID, skipping vertical Trending Articles injection');
384                            }
385
386
387                            // Check for og:image or vf:image meta tag
388                            const ogImage = document.querySelector('meta[property="og:image"]');
389                            const vfImage = document.querySelector('meta[name="vf:image"]');
390                            if (!ogImage && !vfImage) {
391                                if (this.debugOutput) console.warn('No og:image or vf:image meta tag found; Trending Conversations images may not display');
392                            }
393
394                            // Load Viafoura script if not already loaded
395                            if (!document.querySelector('script[src="//cdn.viafoura.net/entry/index.js"]')) {
396                                const script = document.createElement('script');
397                                script.src = '//cdn.viafoura.net/entry/index.js';
398                                script.async = true;
399                                script.onload = () => {
400                                    if (this.debugOutput) console.log('Viafoura script loaded successfully');
401                                    console.log('Post-load window.vfQ state:', window.vfQ);
402                                    console.log('Post-load window.viafoura state:', window.viafoura);
403                                    window.addEventListener('vfContentRecirculationAd', (event) => {
404                                        if (this.debugOutput) console.log('vfContentRecirculationAd event triggered:', event.detail);
405                                    });
406                                    window.addEventListener('requestConversationStarter', (event) => {
407                                        if (this.debugOutput) console.log('requestConversationStarter event triggered:', event.detail);
408                                    });
409                                };
410                                script.onerror = () => {
411                                    if (this.debugOutput) console.error('Viafoura script failed to load');
412                                };
413                                document.body.appendChild(script);
414                                if (this.debugOutput) console.log('Viafoura script injection initiated');
415                            }
416                        } else {
417                            if (this.debugOutput) console.error('Main content div (.main-content) not found');
418                        }
419
420                        // Inject notification tray and bell
421                        const body = document.body;
422                        if (body && postElement.classList.contains('type-post')) {
423                            const trayDiv = document.createElement('div');
424                            trayDiv.className = 'viafoura viafoura-tray';
425                            trayDiv.innerHTML = `<vf-notifications></vf-notifications>`;
426                            body.appendChild(trayDiv);
427                            if (this.debugOutput) console.log('Injected Viafoura notification tray');
428
429                            const bellDiv = document.createElement('div');
430                            bellDiv.className = 'viafoura viafoura-bell';
431                            bellDiv.innerHTML = `<vf-notification-bell></vf-notification-bell>`;
432                            body.appendChild(bellDiv);
433                            if (this.debugOutput) console.log('Injected Viafoura notification bell');
434                        } else {
435                            if (this.debugOutput) console.error('Document body not found for notification tray/bell injection');
436                        }
437
438                        // Hide default WordPress comment section
439                        const commentsDiv = document.querySelector('#comments.comments-area');
440                        if (commentsDiv) {
441                            commentsDiv.style.display = 'none';
442                            if (this.debugOutput) console.log('Hid default WordPress comment section (#comments.comments-area)');
443                        } else {
444                            if (this.debugOutput) console.warn('Default comment section (#comments.comments-area) not found');
445                        }
446
447                        // Trigger Viafoura login
448                        window.vfQ = window.vfQ || [];
449                        window.vfQ.push(() => {
450                            if (window.Pelcro.user.auth?.token) {
451                                const sessionToken = window.Pelcro.user.auth.token;
452                                if (this.debugOutput) console.log('Triggering Viafoura custom login with loggedInCookie_sid:', sessionToken.substring(0, 20) + '...');
453                                window.vf.session.login.cookie(sessionToken);
454                            } else {
455                                if (this.debugOutput) console.log('No session token available for Viafoura login');
456                            }
457                        });
458                    } else {
459                        if (this.debugOutput) console.log('Skipping Viafoura div injection: Not a post page or invalid post ID');
460                    }
461                }
462                // End Viafoura inject block
463                // End VF init
464
465                if(this.failSafeTimer > 0) {
466                    if(this.debugOutput) console.log(" - terminating wait timer: ");
467
468                    clearTimeout(this.failSafeTimer);
469                    this.failSafeTimer = 0;
470                }
471
472                const sessionToken = window.Pelcro.user.auth?.token;
473                document.cookie = `loggedInCookie_sid=${encodeURIComponent(sessionToken)}; Secure; SameSite=Strict; Path=/; Max-Age=86400`;
474                if (this.debugOutput) console.log(" - Set Viafoura session cookie:", sessionToken);
475
476                this.decision("user-loaded");
477                break;
478
479            case "PelcroUserLoggedIn":
480                if (this.debugOutput) console.log("Pelcro User Login Event: ");
481                
482                location.reload();
483                break;
484        }
485    }
486
487    displayWall() {
488        if (this.debugOutput) console.log(" - in display paywall!");
489        const selectorForPWBlur = '#bdn_paywall_content > p, #bdn_paywall_content > h1, #bdn_paywall_content > h2, #bdn_paywall_content > h3, #bdn_paywall_content > h4, #bdn_paywall_content >h5, #bdn_paywall_content > h6, #bdn_paywall_content > .wp-block-columns > .wp-block-column > p, .flourish-embed, .flourish-chart, .embed-documentcloud';
490        const selectorForPWMixup = '#bdn_paywall_content > p';
491        const pTags = document.querySelectorAll(selectorForPWBlur);
492        const crawlerTest = this.urlParams.has('crawler');
493        let cutoff = 200;
494
495        //Remove Everlit
496        const audioDiv = document.querySelector('.wp-block-custom-everlit-iframe-embed');
497        if (audioDiv) audioDiv.remove();
498    
499        // Inject Viafoura conversation widget for paywalled content
500        // VF init
501
502        let postId = this.metadata.postId || null;
503        if (!postId && this.viafouraToggle) {
504            const postElement = document.querySelector('article[id^="post-"]');
505            if (postElement && postElement.id) {
506                postId = postElement.id.replace('post-', '');
507            }
508            if (!postId) {
509                if (this.debugOutput) console.error('Could not find WordPress post ID for Viafoura container in paywall, using articleId');
510                postId = this.metadata.articleId || 'default';
511            }
512            this.metadata.postId = postId;
513        }
514        if (this.debugOutput) console.log('Using postId for paywall Viafoura:', postId);
515        
516        // End VF init
517
518        if (pTags.length > 0) {
519            if (pTags[0].textContent.length > cutoff) {
520                pTags[0].style.webkitMaskImage = 'linear-gradient(to bottom, black 10%, transparent 80%)';
521                pTags[0].style.maskImage = 'linear-gradient(to bottom, black 10%, transparent 80%)';
522            }
523
524            for (let i = 1; i < pTags.length; i++) {
525                pTags[i].style.filter = 'blur(10px)';
526                pTags[i].style.userSelect = 'none';
527                pTags[i].style.webkitUserSelect = 'none';
528                pTags[i].style.pointerEvents = 'none';
529            }
530
531            const copy = this.getPaywallCopy();
532            let html = this.metadata.inline_html;
533
534            if (copy) {
535                if (!document.getElementById('pw2026-styles')) {
536                    const pw2026Style = document.createElement('style');
537                    pw2026Style.id = 'pw2026-styles';
538                    pw2026Style.textContent = `#bdn-paywall-view{text-align:unset!important;background-color:transparent!important}#bdn-paywall{background:#c0c0c0;border:1px solid #ddd;max-width:680px;margin:32px auto;font-family:'Bebas Neue',sans-serif;box-shadow:0 2px 12px rgba(0,0,0,.10)}.pw2026-header{background-color:#00723e;display:flex;align-items:center;gap:14px;padding:10px 18px}.pw2026-header-logo{color:#fff;font-family:'Bebas Neue',sans-serif;font-size:28px;letter-spacing:1px;line-height:1;white-space:nowrap}.pw2026-header-cta{color:#fff;font-family:'Bebas Neue',sans-serif;font-size:22px;letter-spacing:.5px;line-height:1}.pw2026-subhead{text-align:center;padding:16px 24px 8px;font-family:'Bebas Neue',sans-serif;font-size:20px;letter-spacing:.3px;line-height:1.3;color:#222}.pw2026-offers{display:flex;gap:16px;padding:12px 20px 20px}.pw2026-card{flex:1;background:#fff;border:1px solid #ccc;display:flex;flex-direction:column;align-items:center;padding:14px 12px 16px;position:relative;text-align:center}.pw2026-badge{position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#e03030;color:#fff;font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:1px;padding:2px 12px;white-space:nowrap}.pw2026-card-img{width:100%;height:auto;display:block;margin:8px 0 10px}.pw2026-card-label{font-family:'Bebas Neue',sans-serif;font-size:20px;letter-spacing:.3px;color:#111;margin:0 0 4px;line-height:1.2}.pw2026-card-price{font-family:'Bebas Neue',sans-serif;font-size:16px;letter-spacing:.3px;color:#444;margin:0 0 12px;line-height:1.3}.pw2026-btn{display:block;width:100%;background:#e03030;color:#fff!important;font-family:'Bebas Neue',sans-serif;font-size:16px;letter-spacing:1px;text-align:center;padding:9px 0;text-decoration:none!important;border:none;cursor:pointer;margin-top:auto}.pw2026-btn:hover{background:#b52424;color:#fff!important;text-decoration:none!important}.pw2026-footer{background:#fff;text-align:center;padding:10px 20px 16px;border-top:1px solid #ddd}.pw2026-footer-tagline{font-family:'Bebas Neue',sans-serif;font-size:20px;letter-spacing:.3px;color:#111;margin:0 0 4px}.pw2026-footer-login{font-family:'Bebas Neue',sans-serif;font-size:16px;letter-spacing:.3px;color:#444;margin:0}.pw2026-footer-login a{color:#00723e;text-decoration:underline}@media(max-width:540px){.pw2026-offers{flex-direction:column}.pw2026-card{margin-top:16px}.pw2026-header-cta{font-size:17px}}`;
539                    document.head.appendChild(pw2026Style);
540                }
541                html = `
542                    <div id="bdn-paywall">
543                        <div class="bdn-root">
544                            <div id="bdn-paywall-view" class="bdn-text-center bdn-w-full bdn-mx-auto bdn-bg-[#00723]">
545                                ${copy.body}
546                            </div>
547                        </div>
548                        ${copy.custom_css ? `<style>${copy.custom_css}</style>` : ''}
549                    </div>
550                `;
551            }
552
553
554            pTags[0].insertAdjacentHTML('afterend', html);
555            if (this.debugOutput) console.log('Paywall HTML injected after first paragraph');
556
557            // Remove Trinity Audio player completely
558            //this.killTrinity();
559
560            // Hide all Everlit wrappers
561            document.querySelectorAll('.wp-block-custom-everlit-iframe-embed').forEach(el => el.style.display = 'none');
562            // Inject Viafoura conversation widget after #comments or at end of .main-content
563            // VF init 3
564
565            if (postId !== 'default' && this.viafouraToggle) {
566                const commentsDiv = document.querySelector('#comments.comments-area');
567                const mainContentDiv = document.querySelector('.main-content');
568                const viafouraDiv = document.createElement('div');
569                viafouraDiv.className = 'viafoura';
570                viafouraDiv.id = `bdn-paywall-viafoura-${postId}`;
571                viafouraDiv.innerHTML = `
572                    <vf-conversations-count></vf-conversations-count>
573                    <vf-conversations vf-container-id="viafoura-comments-${postId}"></vf-conversations>
574                `;
575                if (commentsDiv) {
576                    commentsDiv.insertAdjacentElement('afterend', viafouraDiv);
577                    if (this.debugOutput) console.log(`Injected Viafoura conversation widget after #comments with vf-container-id="viafoura-comments-${postId}"`);
578                } else if (mainContentDiv) {
579                    mainContentDiv.appendChild(viafouraDiv);
580                    if (this.debugOutput) console.log(`Injected Viafoura conversation widget at end of .main-content with vf-container-id="viafoura-comments-${postId}"`);
581                } else {
582                    if (this.debugOutput) console.error('Could not find #comments or .main-content for Viafoura injection in paywall');
583                }
584
585                // Load Viafoura script if not already loaded
586                if (!document.querySelector('script[src="//cdn.viafoura.net/entry/index.js"]')) {
587                    const script = document.createElement('script');
588                    script.src = '//cdn.viafoura.net/entry/index.js';
589                    script.async = true;
590                    script.onload = () => {
591                        if (this.debugOutput) console.log('Viafoura script loaded successfully for paywall comments');
592                        console.log('Post-load window.vfQ state:', window.vfQ);
593                        console.log('Post-load window.viafoura state:', window.viafoura);
594                    };
595                    script.onerror = () => {
596                        if (this.debugOutput) console.error('Viafoura script failed to load for paywall comments');
597                    };
598                    document.body.appendChild(script);
599                    if (this.debugOutput) console.log('Viafoura script injection initiated for paywall comments');
600                } else {
601                    if (this.debugOutput) console.log('Viafoura script already loaded, skipping script injection');
602                }
603            } else {
604                if (this.debugOutput) console.log('Skipping Viafoura injection in paywall: invalid postId');
605            }
606            // end VF init 3
607        } else {
608            if (this.debugOutput) console.error('No pTags found for paywall injection');
609        }
610
611    
612        if (!(this.isCrawler(this.debugOutput, this.debugTrace) || crawlerTest)) {
613            this.encode(selectorForPWMixup, 2, this.pixels);
614        }
615    
616        document.addEventListener("PelcroUserLoggedIn", this, false);
617    }
618
619
620    injectSingleCommentCountWidget() {
621        let postId = null;
622        const postElement = document.querySelector('article[id^="post-"]');
623        if (postElement && postElement.id) {
624            postId = postElement.id.replace('post-', '');
625        }
626        if (!postId) {
627            if (this.debugOutput) console.error('Could not find WordPress post ID for Viafoura comment count widget');
628            postId = this.metadata.articleId || 'default';
629        }
630
631        const entryMeta = document.querySelector('.entry-subhead .entry-meta');
632        if (entryMeta && postId !== 'default') {
633            const countDiv = document.createElement('div');
634            countDiv.className = 'viafoura viafoura-comment-count';
635            countDiv.style.minHeight = '0px';
636            countDiv.innerHTML = `
637                <a class="mr-3 flex hover:underline" href="#viafoura-comments-${postId}" title="Join the Conversation">
638                    <svg class="vf-comment-thing" stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" class="mr-1 mt-[3px] h-5 w-5" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
639                        <path d="M7 7h10v2H7zm0 4h7v2H7z"></path>
640                        <path d="M20 2H4c-1.103 0-2 .897-2 2v18l5.333-4H20c1.103 0 2-.897 2-2V4c0-1.103-.897-2-2-2zm0 14H6.
640667L4 18V4h16v12z"></path>
641                    </svg>
642                    <vf-conversations-count vf-container-id="viafoura-comments-${postId}"></vf-conversations-count>
643                </a>
644            `;
645            entryMeta.prepend(countDiv);
646            if (this.debugOutput) console.log(`Prepended Viafoura comment count widget with vf-container-id="viafoura-comments-${postId}" to .entry-meta`);
647        } else {
648            if (this.debugOutput) console.error('Entry meta (.entry-subhead .entry-meta) not found or invalid post ID, skipping Viafoura comment count injection');
649        }
650    }
651
652
653    injectCommentCountWidgets() {
654        const articleElements = document.querySelectorAll('article[data-post-id]');
655        if (this.debugOutput) console.log(`Found ${articleElements.length} article elements with data-post-id for comment count injection`);
656        articleElements.forEach(article => {
657            const articlePostId = article.getAttribute('data-post-id');
658            if (articlePostId) {
659                const countDiv = document.createElement('div');
660                countDiv.className = 'viafoura viafoura-comment-count';
661                countDiv.innerHTML = `<vf-conversations-count vf-container-id="viafoura-comments-${articlePostId}"></vf-conversations-count>`;
662                article.appendChild(countDiv);
663                if (this.debugOutput) console.log(`Injected Viafoura comment count widget for article with data-post-id="${articlePostId}"`);
664            } else {
665                if (this.debugOutput) console.warn('Skipping comment count injection: article missing data-post-id');
666            }
667        });
668    }
669
670    getPaywallCopy() {
671        const copyOptions = window.bdnPaywallData?.paywallCopy || [];
672        const currentUrl = window.location.href.toLowerCase();
673    
674        // Collect all matching active copies
675        const matches = [];
676        for (const copy of copyOptions) {
677            if (!copy.is_active) continue;
678            if (copy.url_regex) {
679                try {
680                    const regex = new RegExp(copy.url_regex, 'i');
681                    if (regex.test(currentUrl)) {
682                        matches.push(copy);
683                    }
684                } catch (e) {
685                    // Invalid regex, skip
686                    continue;
687                }
688            }
689        }
690    
691        if (matches.length === 0) {
692            return null;
693        }
694    
695        if (matches.length === 1) {
696            return matches[0];
697        }
698    
699        // Multiple matches: weighted random selection
700        const totalWeight = matches.reduce((sum, copy) => sum + copy.weight, 0);
701        let random = Math.random() * totalWeight;
702    
703        for (const copy of matches) {
704            random -= copy.weight;
705            if (random <= 0) {
706                return copy;
707            }
708        }
709    
710        // Fallback to last match if rounding error
711        return matches[matches.length - 1];
712    }
713
714    getUserArticleCount() {
715        // Placeholder: Replace with server-side tracking
716        const count = parseInt(localStorage.getItem('bdn_article_count') || '0');
717        localStorage.setItem('bdn_article_count', count + 1);
718        return count;
719    }
720
721    removeWall() {
722        if (this.debugOutput) console.log(" - in remove paywall!");
723
724        const selectorForPWBlur = '#bdn_paywall_content > p, #bdn_paywall_content > h1, #bdn_paywall_content > h2, #bdn_paywall_content > h3, #bdn_paywall_content > h4, #bdn_paywall_content >h5, #bdn_paywall_content > h6, #bdn_paywall_content > .wp-block-columns > .wp-block-column > p, .flourish-embed, .flourish-chart, .embed-documentcloud';
725        const selectorForPWMixup = '#bdn_paywall_content > p';
726        const pTags = document.querySelectorAll(selectorForPWBlur);
727
728        if (this.debugOutput) {
729            console.log(" - ptags for removal");
730            console.log(pTags);
731        }
732
733        if (pTags.length > 0) {
734            // Remove gradient mask from first paragraph
735            pTags[0].style.webkitMaskImage = '';
736            pTags[0].style.maskImage = '';
737
738            // Remove blur from subsequent elements
739            for (let i = 1; i < pTags.length; i++) {
740                pTags[i].style.filter = '';
741            }
742
743            // Remove inserted paywall HTML (assuming it’s the next sibling of pTags[0])
744            const nextSibling = pTags[0].nextElementSibling;
745            if (nextSibling && nextSibling.matches('.pelcro-container')) {
746                nextSibling.remove();
747            } else if (this.debugOutput) {
748                console.log(" - no paywall HTML found to remove");
749            }
750        }
751
752        const crawlerTest = this.urlParams.has('crawler');
753        if (!(isCrawler(this.debugOutput, this.debugTrace) || crawlerTest)) {
754            decode(selectorForPWMixup, 2);
755        }
756
757        // Remove PelcroUserLoggedIn event listener
758        document.removeEventListener("PelcroUserLoggedIn", this, false);
759    }     
760
761    isCrawler() {
762
763        if(this.debugTrace) {
764            console.log("BDN Paywall Stack Trace:");
765            console.trace();
766        }
767
768        if(this.debugOutput) console.log(" - in isCrawler logic!");
769
770        var botPattern = "(googlebot\\/|bot|Googlebot-Mobile|Googlebot-Image|Google favicon|Mediapartners-Google|bingbot|slurp|java|wget|curl|Commons-HttpClient|Python-urllib|libwww|httpunit|nutch|phpcrawl|msnbot|jyxobot|FAST-WebCrawler|FAST Enterprise Crawler|biglotron|teoma|convera|seekbot|gigablast|exabot|ngbot|ia_archiver|GingerCrawler|webmon |httrack|webcrawler|grub.org|UsineNouvelleCrawler|antibot|netresearchserver|speedy|fluffy|bibnum.bnf|findlink|msrbot|panscient|yacybot|AISearchBot|IOI|ips-agent|tagoobot|MJ12bot|dotbot|woriobot|yanga|buzzbot|mlbot|yandexbot|purebot|Linguee Bot|Voyager|CyberPatrol|voilabot|baiduspider|citeseerxbot|spbot|twengabot|postrank|turnitinbot|scribdbot|page2rss|sitebot|linkdex|Adidxbot|blekkobot|ezooms|dotbot|Mail.RU_Bot|discobot|heritrix|findthatfile|europarchive.org|NerdByNature.Bot|sistrix crawler|ahrefsbot|Aboundex|domaincrawler|wbsearchbot|summify|ccbot|edisterbot|seznambot|ec2linkfinder|gslfbot|aihitbot|intelium_bot|facebookexternalhit|yeti|RetrevoPageAnalyzer|lb-spider|sogou|lssbot|careerbot|wotbox|wocbot|ichiro|DuckDuckBot|lssrocketcrawler|drupact|webcompanycrawler|acoonbot|openindexspider|gnam gnam spider|web-archive-net.com.bot|backlinkcrawler|coccoc|integromedb|content crawler spider|toplistbot|seokicks-robot|it2media-domain-crawler|ip-web-crawler.com|siteexplorer.info|elisabot|proximic|changedetection|blexbot|arabot|WeSEE:Search|niki-bot|CrystalSemanticsBot|rogerbot|360Spider|psbot|InterfaxScanBot|Lipperhey SEO Service|CC Metadata Scaper|g00g1e.net|GrapeshotCrawler|urlappendbot|brainobot|fr-crawler|binlar|SimpleCrawler|Livelapbot|Twitterbot|cXensebot|smtbot|bnf.fr_bot|A6-Indexer|ADmantX|Facebot|Twitterbot|OrangeBot|memorybot|AdvBot|MegaIndex|SemanticScholarBot|ltx71|nerdybot|xovibot|BUbiNG|Qwantify|archive.org_bot|Applebot|TweetmemeBot|crawler4j|findxbot|SemrushBot|yoozBot|lipperhey|y!j-asr|Domain Re-Animator Bot|AddThis)";
771        var re = new RegExp(botPattern, 'i');
772        var userAgent = navigator.userAgent; 
773        var rVal = re.test(userAgent);
774        
775        if(this.debugOutput) {
776            console.log(" - is crawler: ");
777            console.log(rVal);
778        }
779
780        return rVal;
781    }
782
783    shuffle(array) {
784
785        for (let i = array.length - 1; i > 0; i--) {
786
787            const j = Math.floor(Math.random() * (i + 1));
788            [array[i], array[j]] = [array[j], array[i]]; 
789        }
790
791        return array;
792    }
793
794    encode(selector, startP) {
795        console.log('encode called with', selector, startP);
796
797        if (document.getElementById('coral_thread')) {
798            const hasProp = Object.hasOwn(document.getElementById('coral_thread'), 'style');
799            if (hasProp) {
800                document.getElementById('coral_thread').style.display = 'none';
801            }
802        }
803
804        // Verify wrapper exists
805        const wrapper = document.querySelector('#bdn_paywall_content');
806        //console.log('Wrapper element:', wrapper ? wrapper.outerHTML : 'Not found');
807
808        const pe = document.querySelectorAll(selector);
809        console.log('Selected <p> elements:', pe.length, Array.from(pe).map(el => el.outerHTML.slice(0, 50) + '...'));
810
811        this.pixels = new Array(pe.length);
812
813        if (pe.length === 0) {
814            console.warn('No <p> elements found for selector:', selector);
815            return;
816        }
817
818        if (startP - 1 >= pe.length) {
819            console.warn(`startP (${startP}) is too large; only ${pe.length} elements found`);
820            return;
821        }
822
823        for (let i = startP - 1; i < pe.length; i++) {
824            const element = pe[i];
825            console.log('Processing element', i, element.outerHTML.slice(0, 50) + '...');
826
827            // Collect text nodes and their content
828            const textNodes = [];
829            const walk = document.createTreeWalker(element, Node.TEXT_NODE, null, false);
830            let node;
831            while ((node = walk.nextNode())) {
832                const text = node.textContent.trim();
833                if (text) {
834                    textNodes.push({ node, text: text.split(/\s+/).filter(word => word) });
835                }
836            }
837            console.log('Text nodes:', textNodes.map(tn => tn.text));
838
839            // Flatten all words and shuffle them
840            const allWords = textNodes.flatMap(tn => tn.text);
841            console.log('Words to shuffle:', allWords);
842
843            if (allWords.length === 0) {
844                console.log('No words to shuffle for element', i);
845                this.pixels[i] = [];
846                continue;
847            }
848
849            const encodeVal = new Array(allWords.length);
850            for (let z = 0; z < encodeVal.length; z++) {
851                encodeVal[z] = z;
852            }
853            console.log('Shuffle indices before:', encodeVal.slice());
854            this.shuffle(encodeVal);
855            console.log('Shuffle indices after:', encodeVal.slice());
856            console.log('Is shuffled:', encodeVal.some((val, idx) => val !== idx));
857
858            // Distribute shuffled words back to text nodes
859            let wordIndex = 0;
860            for (const { node, text } of textNodes) {
861                const newText = [];
862                for (let j = 0; j < text.length && wordIndex < allWords.length; j++) {
863                    newText.push(allWords[encodeVal[wordIndex++]]);
864                }
865                node.textContent = newText.join(' ');
866                console.log('Updated text node:', node.textContent);
867            }
868
869            this.pixels[i] = encodeVal;
870            element.classList.add('bdn_paywall_prevent-select');
871        }
872    }
873
874    decode(selector, startP) {
875        console.log('decode called with', selector, startP);
876
877        if (document.getElementById('coral_thread')) {
878            document.getElementById('coral_thread').style.display = 'block';
879        }
880
881        const pe = document.querySelectorAll(selector);
882        console.log('Decode selected <p> elements:', pe.length);
883
884        for (let i = startP - 1; i < pe.length; i++) {
885            const element = pe[i];
886            console.log('Decoding element', i, element.outerHTML.slice(0, 50) + '...');
887            const encodeVal = this.pixels[i] || [];
888            console.log('Pixels:', encodeVal);
889
890            if (!encodeVal.length) {
891                console.log('No pixels for element', i);
892                continue;
893            }
894
895            // Collect text nodes and their content
896            const textNodes = [];
897            const walk = document.createTreeWalker(element, Node.TEXT_NODE, null, false);
898            let node;
899            while ((node = walk.nextNode())) {
900                const text = node.textContent.trim();
901                if (text) {
902                    textNodes.push({ node, text: text.split(/\s+/).filter(word => word) });
903                }
904            }
905            console.log('Decode text nodes:', textNodes.map(tn => tn.text));
906
907            // Flatten all words
908            const allWords = textNodes.flatMap(tn => tn.text);
909            console.log('Decode words:', allWords);
910
911            if (allWords.length === 0) {
912                console.log('No words to decode for element', i);
913                continue;
914            }
915
916            // Reconstruct original order
917            const origWords = new Array(allWords.length);
918            for (let y = 0; y < encodeVal.length; y++) {
919                if (encodeVal[y] < allWords.length) {
920                    origWords[encodeVal[y]] = allWords[y];
921                }
922            }
923            console.log('Restored words:', origWords);
924
925            // Distribute words back
926            let wordIndex = 0;
927            for (const { node, text } of textNodes) {
928                const newText = [];
929                for (let j = 0; j < text.length && wordIndex < origWords.length; j++) {
930                    newText.push(origWords[wordIndex++] || '');
931                }
932                node.textContent = newText.join(' ');
933                console.log('Decoded text node:', node.textContent);
934            }
935
936            element.classList.remove('bdn_paywall_prevent-select');
937        }
938    }
939
940    shuffle(array) {
941        for (let i = array.length - 1; i > 0; i--) {
942            const j = Math.floor(Math.random() * (i + 1));
943            [array[i], array[j]] = [array[j], array[i]];
944        }
945        return array;
946    }
947
948    nonLiteExperience() {
949
950        if(this.debugOutput) console.log("Non add lite experience called!");
951
952        /*opscobid.cmd.push(() => {
953            opscobid.newPage().then(() => {
954                opscobid.reloadSlot('bottom-anchor')
955            });
956        });*/
957
958        //Next Millenium inject
959        /*const nextMilleniumscript = document.createElement('script');
960        nextMilleniumscript.src = 'https://powerad.ai/268591500480275/script.
960js';
961        nextMilleniumscript.async = true;
962        nextMilleniumscript.type = 'text/javascript';
963        document.head.appendChild(nextMilleniumscript);*/
964
965        if (!document.getElementById('rev2exit')) {
966            const rev2exitScript = document.createElement('script');
967            rev2exitScript.id = 'rev2exit';
968            rev2exitScript.src = 'https://labs-cdn.revcontent.com/build/revexit.min.js?w=291897&p=74369&k=1fe26b5e39e0160e2384b62c7f5a97bd1ef37429&d=bangordailynews.com&x=both';
969            rev2exitScript.type = 'text/javascript';
970            document.body.appendChild(rev2exitScript);
971        }
972
973        if(typeof loadAd === 'function') {
974                
975            if(this.debugOutput) console.log("Pushing Yield Lift Ad To Bottom Anchor!");
976
977            loadAd("adhesionAdSlot", "/21682974628/bangordailynews/bottom-anchor", 1, 1);
978
979        } else {
980            if(this.debugOutput) console.log("loadad function is not defined.");
981        }
982    }
983
984    async logout() {
985
986        document.cookie = "bdn_subscriber=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Strict; Secure";
987        if (this.debugOutput) console.log("Entering logout method");
988
989        // Get the loggedInCookie_sid cookie
990        const getCookie = (name) => {
991            const value = `; ${document.cookie}`;
992            const parts = value.split(`; ${name}=`);
993            if (parts.length === 2) return parts.pop().split(';').shift();
994            return null;
995        };
996        const token = getCookie('loggedInCookie_sid');
997
998        if (!token) {
999            if (this.debugOutput) console.log("No loggedInCookie_sid cookie found");
1000            // Clear cookie anyway to ensure clean state
1001            document.cookie = "loggedInCookie_sid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
1002            // Refresh page to reset paywall state
1003            window.location.reload();
1004            return;
1005        }
1006
1007        try {
1008            // Call /logout endpoint
1009            const response = await fetch('https://apitest.bangordailynews.com/logout', {
1010                method: 'POST',
1011                headers: {
1012                    'Content-Type': 'application/json'
1013                },
1014                body: JSON.stringify({
1015                    key: '2e2d687e-90cb-4b11-8870-4a9f51e83226',
1016                    token: token
1017                })
1018            });
1019
1020            if (this.debugOutput) console.log("Logout API response status:", response.status);
1021
1022            if (response.ok) {
1023                // Delete the cookie
1024                document.cookie = "loggedInCookie_sid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
1025                if (this.debugOutput) console.log("loggedInCookie_sid cookie deleted");
1026                // Reset subscriberFlag
1027                this.metadata.subscriberFlag = false;
1028                this.metadata.userObj = {};
1029                // Refresh page to reset paywall state
1030                window.location.reload();
1031            } else {
1032                if (this.debugOutput) console.log("Logout API failed:", response.status, response.statusText);
1033                // Clear cookie anyway to ensure client-side logout
1034                document.cookie = "loggedInCookie_sid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
1035                window.location.reload();
1036            }
1037        } catch (error) {
1038            if (this.debugOutput) console.log("Logout API error:", error.message);
1039            // Clear cookie on error to ensure client-side logout
1040            document.cookie = "loggedInCookie_sid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
1041            window.location.reload();
1042        }
1043    }
1044
1045    async decision(mode) {
1046
1047        if(this.debugTrace) {    
1048            console.log("BDN Paywall Stack Trace:");
1049            console.trace();
1050        }
1051
1052        if(this.debugOutput) console.log("Entering Decision!");
1053
1054
1055
1056        if(this.metadata.paywallRunCheck == "0" && (this.metadata.goGoGadgetPaywall === "1")) {
1057
1058            if (this.metadata.subscriberFlag) {
1059                console.log("Skipping paywall: serverUser indicates active subscriber");
1060                demeter("pageview", { args: { article: this.isArticle || false, section: this.metadata.section || null } });
1061                this.injectSTNPlayer(true);
1062                return; // Exit early for subscribers
1063            }
1064
1065            if(this.debugOutput) console.log(" - decision mode: " + mode);
1066
1067            this.metadata.paywallRunCheck = "1";
1068
1069            if(this.debugOutput) {
1070                console.log("Metadata:");
1071                console.log(this.metadata);
1072            }
1073
1074            const isArticle = this.isArticle;
1075            if(this.debugOutput) console.log(" - isArticle: " + isArticle);
1076
1077            const siteFromUrl = this.siteFromUrl;
1078            if(this.debugOutput) console.log(" - siteFromUrl: " + siteFromUrl);
1079
1080            const isSubscriber = await this.isSubscriber();
1081            if(this.debugOutput) console.log(" - isSubscriber: " + isSubscriber);
1082
1083            const userType = this.userType;
1084            window.userState.userType = userType;
1085            window.userState.isSubscriber = isSubscriber;
1086
1087            if(!isSubscriber) this.nonLiteExperience();
1088            this.injectSTNPlayer(isSubscriber);
1089
1090            if(!isSubscriber) {
1091                
1092                console.log("GAME COPY STUFF",window.bdnPaywallData.gamePaths,window.bdnPaywallData.gameHeader,window.bdnPaywallData.gameCopy);
1093                const currentPath = window.location.pathname.toLowerCase();
1094                const gamePaths = window.bdnPaywallData.gamePaths;
1095                this.isGamePage = gamePaths.some(path => currentPath.startsWith(path));
1096
1097                if (this.isGamePage) {
1098                    if (this.debugOutput) console.log("Detected game page, triggering paywall.");
1099                    this.displayGameWall(); 
1100                    return; 
1101                }
1102            }
1103            //end game wall
1104
1105
1106
1107            if(isArticle === true) {
1108
1109                let argVal = { args: { article: isArticle, section: this.metadata.section } };
1110
1111                if(this.debugOutput) {
1112                    console.log(" - in, isArticle logic!");
1113                    console.log("Submitting Demeter (pageview) view data:");
1114                    console.log(argVal);
1115                }
1116                
1117                demeter("pageview", argVal);
1118
1119                if(this.debugOutput) console.log(" - called demeter pageview!");
1120
1121                if(!isSubscriber) {
1122
1123                    //this.nonLiteExperience();
1124
1125                    if(this.debugOutput) console.log(" - in, not(isSubscriber) logic!");
1126
1127                    if(this.debugOutput) {
1128                         console.log(" - userType: " + userType);
1129                         console.log(window.Pelcro);
1130                    }
1131
1132                    let isFreeContent = this.isFreeContent;
1133                    if(this.debugOutput) console.log(" - isFreeContent: " + isFreeContent);
1134
1135                    let isAlwaysWalledContent = this.isAlwaysWalledContent;
1136                    if(this.debugOutput) console.log(" - isAlwaysWalledContent: " + isAlwaysWalledContent);
1137
1138                    if(!isFreeContent) {
1139
1140                        if(this.debugOutput) console.log(" - in, !isFreContent logic!");
1141
1142                        if(isAlwaysWalledContent) {
1143
1144                            argVal = { args: "paywall" };
1145
1146                            if(this.debugOutput) {
1147                                 console.log(" - in, isAlwaysWalledContent logic!");
1148                                 console.log("Submitting Demeter (pageview) view data:");   
1149                            }
1150
1151                            this.displayWall();
1152                            demeter("wall", argVal );
1153                        } else {
1154
1155                            if(this.debugOutput) console.log(" - in, !isAlwaysWalledContent logic!");
1156                            if(this.debugOutput) console.log(" - calling demeter - getDecision");
1157
1158                            demeter("getDecision", {
1159
1160                                args: { visitor: userType },
1161                                onSuccess: (decision) => { 
1162
1163                                    let sophiDecision = (decision.outcome.wallVisibility === "always");
1164
1165                                    if(this.debugTrace) {
1166            
1167                                        console.log("BDN Paywall Stack Trace:")
1168                                        console.trace();
1169                                    }
1170
1171                                    if(this.debugOutput){
1172    
1173                                        console.log(" - decision returned!");
1174                                        console.log(sophiDecision);
1175                                        console.log(decision.outcome);
1176                                    }
1177                            
1178                                    if(this.urlParams.get('sophisaysyes') != null) {
1179                                        if(this.debugOutput) console.log(" - overriding sophi decision to yes because of query string!");
1180                                        sophiDecision = true;
1181                                    }
1182
1183                                    if(sophiDecision) {
1184
1185                                        //add final subscriber check to address race condition edge case
1186                                        // if not isSubscriber proceed 
1187                                        //if (!this.metadata.subscriberFlag) {
1188
1189                                            if(this.debugOutput) console.log(" - calling paywall!");
1190                                            this.displayWall();
1191
1192                                            if(this.debugOutput) console.log(" - demeter - wall!");
1193                                            demeter("wall", { args: "paywall" } );
1194                                            
1195                                            if(this.debugOutput) console.log(" - listener - wall!");
1196                                            (window._matherq = window._matherq || []).push(['paywallEvent', {
1197                                                type: 'paywall',
1198                                                category: 'block',
1199                                                action: 'stop',
1200                                            }]);
1201
1202                                        //} else {
1203                                        //    if (this.debugOutput) console.log(" - Sophi Ignored; user is subscribed. Race condition");
1204                                        //}
1205                                  
1206                                    }
1207
1208                                    //do nothing if sophi return false;
1209                                },
1210
1211                                onError: (err) => { 
1212                                    if(this.debugOutput){
1213                                        console.log("BDN Paywall Callstack:")
1214                                        console.trace();
1215                                        console.log(" - sophi error:");
1216                                        console.log(err);
1217                                    }
1218                                }
1219                            });
1220                        }
1221                    } else {
1222
1223                        if(this.debugOutput) console.log("Skipping because content is free!");
1224                    }
1225                } else {
1226
1227                    if(this.debugOutput) console.log("Skipping because user is signined in with valid subscription!");
1228                }
1229            } else {
1230
1231                const argVal = { args: { article: isArticle, section: this.metadata.section } };
1232
1233                if(this.debugOutput) { 
1234                    console.log("Skipping because is not article!");
1235                    console.log("Submitting Demeter (pageview) view data:");    
1236                    console.log(argVal);
1237                }
1238
1239                demeter("pageview", argVal);
1240            }
1241        }
1242        else {
1243
1244            if(this.debugOutput) console.log("Decision skipped, process has already run or paywall set not run!")
1245        }
1246    }
1247
1248    // Helper function to wait for user.subscriber
1249    async waitForProperty(obj, prop) {
1250        const maxWait = 20000; // 1 second timeout, since the delay is brief
1251        const interval = 50;  // Check every 50ms
1252        let elapsed = 0;
1253
1254        while (!Array.isArray(obj[prop]) && elapsed < maxWait) {
1255            await new Promise(resolve => setTimeout(resolve, interval));
1256            elapsed += interval;
1257        }
1258
1259        if (!Array.isArray(obj[prop])) {
1260            console.warn(`Property ${prop} did not load within ${maxWait}ms`);
1261        }
1262    }
1263
1264    get isFreeContent() {
1265        if (this.debugTrace) {
1266            console.log("BDN Paywall Stack Trace:");
1267            console.trace();
1268        }
1269
1270        if (this.debugOutput) console.log("Processing isFreeContent Logic!");
1271
1272        // Check if URL contains /bdn-maine/ or metadata.isFreeContent is true
1273        const isFree = Boolean(parseInt(this.metadata.isFreeContent)) || /\/bdn-maine\//.test(window.location.pathname) || /\/special-sections\//.test(window.location.pathname);
1274
1275        if (this.debugOutput) {
1276            console.log(" - isFree:", isFree);
1277            console.log(" - URL check:", /\/bdn-maine\//.test(window.location.pathname));
1278        }
1279
1280        return isFree;
1281    }
1282
1283    get isAlwaysWalledContent() {
1284
1285        if(this.debugTrace) {
1286            console.log("BDN Paywall Stack Trace:");
1287            console.trace();
1288        }
1289
1290        if(this.debugOutput) console.log("Processing isAlwaysWalledContent Logic!");
1291
1292        const isWalled = Boolean(parseInt(this.metadata.isAlwaysWalledContent));
1293
1294        if(this.debugOutput) console.log(isWalled);
1295        return isWalled;
1296    }
1297
1298    get userType() {
1299
1300        if(this.debugTrace) { 
1301            console.log("BDN Paywall Stack Trace:");
1302            console.trace();
1303        }
1304
1305        if(this.debugOutput) console.log("Processing userType Logic!");
1306
1307        let isUser = this.isUser;
1308        let rVal = "";
1309
1310        if(isUser)
1311            rVal =  "registered";
1312        else rVal = "anonymous";
1313
1314        if(this.debugOutput) console.log(` -  ${isUser} :: ${rVal}`);
1315        return rVal;
1316    }
1317
1318    async isIPAuthed() {
1319
1320        const ipAuthPromise = new Promise((resolve, reject) => {
1321  
1322            window.Pelcro.membership.getByIP((error, response) => {
1323   
1324                if(error != null)
1325                    resolve([]);
1326                else {
1327                    resolve(response);
1328                }
1329            });  
1330        });
1331        
1332        let ipAuthArray = await ipAuthPromise;
1333        window.ipAuthArray = ipAuthArray;
1334
1335        if(this.debugOutput) console.log(" - ip auth array recieved!");
1336        
1337        //console.log(ipAuthArray);
1338
1339        const rVal = ipAuthArray.some(s => s.status === "active");
1340
1341        //console.log(rVal);
1342        return rVal;
1343    }
1344
1345    async isSubscriber() {
1346        if (this.debugTrace) {
1347            console.log("BDN Paywall Stack Trace:");
1348            console.trace();
1349        }
1350    
1351        if (this.debugOutput) {
1352            console.log("Processing isSubscriber Logic!");
1353            console.log(" - reading metadata - user object:", this.metadata.userObj);
1354        }
1355    
1356        let rVal = false;
1357        let hasAdlite = false;
1358        const user = this.metadata.userObj;
1359        const currentSite = this.siteFromUrl.toLowerCase();
1360    
1361        if (this.debugOutput) console.log(" - current site from siteFromUrl:", currentSite);
1362    
1363        // Check serverUser or Pelcro user data
1364        if (user && Object.keys(user).length > 0) {
1365            if (user === this.metadata.serverUser && user.isServerSubscriber) {
1366                if (this.debugOutput) console.log(" - serverUser with isServerSubscriber true, performing entitlement check");
1367                if (Array.isArray(user.subscriptions)) {
1368                    rVal = user.subscriptions.some(subscription => {
1369                        if (subscription.status !== "active") return false;
1370                        const entitlements = (subscription.plan?.product?.entitlements || []).map(e => e.toLowerCase());
1371                        if (this.debugOutput) console.log(" - serverUser subscription entitlements:", entitlements);
1372                        if (entitlements.includes('adlite')) {
1373                            hasAdlite = true;
1374                            if (this.debugOutput) console.log(" - adlite entitlement found in serverUser subscriptions");
1375                        }
1376                        return entitlements.includes(currentSite);
1377                    });
1378                    if (this.debugOutput) console.log(" - serverUser subscriptions entitlement check result:", rVal);
1379                } else {
1380                    if (this.debugOutput) console.log(" - serverUser has no subscriptions, trusting isServerSubscriber");
1381                    rVal = true;
1382                }
1383            } else if (Array.isArray(user.subscriptions)) {
1384                if (this.debugOutput) console.log(" - user object contains subscriptions!");
1385                rVal = user.subscriptions.some(subscription => {
1386                    if (subscription.status !== "active") return false;
1387                    const entitlements = (subscription.plan?.product?.entitlements || []).map(e => e.toLowerCase());
1388                    if (this.debugOutput) console.log(" - subscription entitlements:", entitlements);
1389                    if (entitlements.includes('adlite')) {
1390                        hasAdlite = true;
1391                        if (this.debugOutput) console.log(" - adlite entitlement found in subscriptions");
1392                    }
1393                    return entitlements.includes(currentSite);
1394                });
1395                if (this.debugOutput) console.log(" - evaluated subscriptions:", rVal);
1396            } else {
1397                if (this.debugOutput) console.log(" - no subscriptions in user object");
1398            }
1399    
1400            if (!rVal && Array.isArray(user.memberships)) {
1401                if (this.debugOutput) console.log(" - user object contains memberships!");
1402                rVal = user.memberships.some(membership => {
1403                    if (membership.subscription?.status !== "active") return false;
1404                    const entitlements = (membership.subscription?.plan?.product?.entitlements || []).map(e => e.toLowerCase());
1405                    if (this.debugOutput) console.log(" - membership entitlements:", entitlements);
1406                    if (entitlements.includes('adlite')) {
1407                        hasAdlite = true;
1408                        if (this.debugOutput) console.log(" - adlite entitlement found in memberships");
1409                    }
1410                    return entitlements.includes(currentSite);
1411                });
1412                if (this.debugOutput) console.log(" - evaluated memberships:", rVal);
1413            } else {
1414                if (this.debugOutput) console.log(" - no memberships in user object");
1415            }
1416        } else {
1417            if (this.debugOutput) console.log(" - no user data, checking IP auth");
1418        }
1419    
1420        // Fallback to IP auth if no subscriber status
1421        if (!rVal) {
1422            const ipAuthed = await this.isIPAuthed();
1423            if (ipAuthed) {
1424                if (this.debugOutput) console.log(" - evaluated subscription via IP auth!");
1425                rVal = true;
1426                // Note: IP auth typically doesn't provide entitlement details, so we assume no adlite for IP-authenticated users
1427            } else {
1428                if (this.debugOutput) console.log(" - not IP authenticated");
1429            }
1430        }
1431    
1432        if (hasAdlite) {
1433            this.injectAdliteMetaTags();
1434        }
1435    
1436        if (rVal) {
1437            this.metadata.subscriberFlag = true;
1438        }
1439        if (this.debugOutput) console.log("FINAL SUBSCRIBER STATUS:", rVal, "Has adlite:", hasAdlite);
1440        return rVal;
1441    }
1442
1443    
1444    // Assuming waitForProperty remains unchanged
1445    async waitForProperty(obj, prop) {
1446        const maxWait = 1000; // 1 second timeout
1447        const interval = 50;  // Check every 50ms
1448        let elapsed = 0;
1449    
1450        while (!Array.isArray(obj[prop]) && elapsed < maxWait) {
1451            await new Promise(resolve => setTimeout(resolve, interval));
1452            elapsed += interval;
1453        }
1454    
1455        if (!Array.isArray(obj[prop])) {
1456            console.warn(`Property ${prop} did not load within ${maxWait}ms`);
1457        }
1458    }
1459
1460    async checkAdliteEntitlement() {
1461        const user = this.metadata.userObj;
1462        let hasAdlite = false;
1463        if (Array.isArray(user.subscriptions)) {
1464            hasAdlite = user.subscriptions.some(sub => sub.status === "active" && (sub.plan?.product?.entitlements || []).includes('adlite'));
1465        } else if (Array.isArray(user.memberships)) {
1466            hasAdlite = user.memberships.some(m => m.subscription?.status === "active" && (m.subscription.plan?.product?.entitlements || []).includes('adlite'));
1467        }
1468        if (hasAdlite) this.injectAdliteMetaTags();
1469    }
1470
1471    get isUser() {
1472
1473        if(this.debugTrace) {
1474            console.log("BDN Paywall Stack Trace:");
1475            console.trace();
1476        }
1477
1478        if(this.debugOutput) console.log("Entering isUser!");
1479
1480        let rVal = false;
1481        
1482        if(this.metadata.userObj != undefined)
1483            if (this.metadata.userObj.auth_token !== undefined)
1484                rVal = true;
1485        return rVal;
1486    }
1487
1488    /* ============================================================
1489     * STN VIDEO PLAYER (Voltax) — minimal live-test inject.
1490     * Replaces the old Ad Inserter script (embed.sendtonews.com/player3),
1491     * which manually forced s2nApi.float and is the suspected cause of
1492     * the Google Ad Manager "sticky video" policy restriction.
1493     * Deliberately does NOT touch float/sticky state — STN says the new
1494     * player self-manages the inline->sticky transition + dismiss control,
1495     * so we just mount it and get out of the way.
1496     * Shown to subscribers and non-subscribers alike (per team, 2026-09):
1497     * non-subscribers get the Floating variant, subscribers get the
1498     * Non-Floating variant. News only, no sports handling yet — smallest
1499     * surface to validate the fix before expanding scope.
1500     * Disable: stnPlayerToggle = false above, or comment out the
1501     * this.injectSTNPlayer(...) calls in decision(). Quick per-request
1502     * override: append ?stnoff to the URL.
1503     * ============================================================ */
1504    injectSTNPlayer(isSubscriber) {
1505
1506        if (!this.stnPlayerToggle) return;
1507        if (this.stnPlayerFired) return;
1508        if (this.urlParams.has('stnoff')) {
1509            if (this.debugOutput) console.log("[STN] suppressed via ?stnoff");
1510            return;
1511        }
1512
1513        const anchor = document.querySelector('#bdn_paywall_content');
1514        if (!anchor) {
1515            if (this.debugOutput) console.log("[STN] anchor #bdn_paywall_content not found, skipping");
1516            return;
1517        }
1518
1519        this.stnPlayerFired = true;
1520
1521        // mm-subId: 4 = News Floating (non-subscribers), 5 = News Non-Floating (subscribers)
1522        const subId = isSubscriber ? 5 : 4;
1523
1524        const playerDiv = document.createElement('div');
1525        playerDiv.className = 's2nPlayer';
1526        anchor.insertAdjacentElement('afterend', playerDiv);
1527
1528        const script = document.createElement('script');
1529        script.src = "https://players.voltaxservices.io/players/bangor-daily-news/bangor_daily_news/code?mm-subId=" + subId;
1530        document.body.appendChild(script);
1531
1532        if (this.debugOutput) console.log("[STN] player injected", { subId: subId, isSubscriber: isSubscriber });
1533    }
1534
1535    get isArticle() {
1536
1537        if(this.debugTrace) {
1538            console.log("BDN Paywall Stack Trace:");
1539            console.trace();
1540        }
1541
1542        if(this.debugOutput) console.log("Entering isArticle!");
1543
1544        return (this.metadata.pageType == "post");
1545    }
1546
1547    get siteFromUrl() {
1548        const fullUrl = window.location.href.toLowerCase();
1549        let site = "unknown";
1550    
1551        if (this.debugOutput) console.log("Processing siteFromUrl Logic!", fullUrl);
1552    
1553        // Define site mappings based on URL patterns
1554        if (fullUrl.includes("bangordailynews")) {
1555            site = "ebdn";
1556        } else if (fullUrl.includes("thecounty")) {
1557            site = "ecou";
1558        } else if (fullUrl.includes("observer-me")) {
1559            site = "etpo";
1560        } // Add more mappings as needed
1561    
1562        if (this.debugOutput) console.log(" - Detected site: " + site);
1563        
1564        if (this.debugTrace) {
1565            console.log("BDN Paywall Stack Trace:");
1566            console.trace();
1567        }
1568        
1569        return site;
1570    }
1571
1572    displayGameWall() {
1573        if (this.debugOutput) console.log("Displaying game paywall...");
1574
1575        // Check if paywall already exists to prevent duplicates
1576        if (document.querySelector('.bdn-game-paywall')) {
1577            if (this.debugOutput) console.log("Game paywall already present, skipping render.");
1578            return;
1579        }
1580
1581        // Find article and entry content
1582        const article = document.querySelector('article[id^="post-"]');
1583        const entryContent = document.querySelector('.entry-content');
1584
1585        // Hide the game wrapper (entry-content)
1586        if (entryContent) {
1587            entryContent.style.display = 'none';
1588            if (this.debugOutput) console.log('Entry content (game wrapper) hidden');
1589        }
1590
1591        // Basic paywall div
1592        const paywallDiv = document.createElement('div');
1593        paywallDiv.className = 'bdn-game-paywall';
1594        paywallDiv.style.cssText = `
1595            margin: 20px 0;
1596            padding: 15px;
1597            border: 1px solid #ccc;
1598            text-align: center;
1599        `;
1600
1601        const gameHeader = window.bdnPaywallData?.gameHeader || 'Subscribe to Play';
1602        const gameCopy = window.bdnPaywallData?.gameCopy || 'This puzzle is available only to subscribers. Please log in or subscribe to continue.';
1603        paywallDiv.innerHTML = `
1604            <h2>${gameHeader}</h2>
1605            <p>${gameCopy}</p>
1606            <button id="bdn-subscribe-btn" class="undefined !bdn-btn bdn-rounded-sm bdn-bg-white bdn-text-black bdn-uppercase hover:bdn-bg-black hover:bdn-text-white hover:bdn-border-white hover:bdn-border bdn-mb-6 bdn-w-1/4">Subscribe Now</button>
1607        `;
1608
1609        // Inject at the top of the article
1610        if (article) {
1611            article.insertBefore(paywallDiv, article.firstChild);
1612            if (this.debugOutput) console.log('Game paywall div injected at the top of article');
1613        } else {
1614            document.body.appendChild(paywallDiv);
1615            if (this.debugOutput) console.warn('No article found, game paywall div appended to body');
1616        }
1617
1618        // Optional: Add click handler for subscribe button
1619        document.getElementById('bdn-subscribe-btn')?.addEventListener('click', () => {
1620            if (this.debugOutput) console.log("Subscribe button clicked.");
1621            window.location.href = "https://www.bangordailynews.com/subscribe/";
1622        });
1623    }
1624
1625    loadGistAIWidget() {
1626        const paywallContent = document.getElementById('bdn_paywall_content');
1627        if (!paywallContent) {
1628            console.warn('Element with id="bdn_paywall_content" not found. Cannot load Gist AI widget.');
1629            return;
1630        }
1631    
1632        // Check if widget already exists to prevent duplicates
1633        if (document.querySelector('gist-chat-widget')) {
1634            console.log('Gist AI widget already loaded. Skipping.');
1635            return;
1636        }
1637    
1638        // Load the Gist AI script in the head (type="module" is required)
1639        const existingScript = document.querySelector('script[src="https://cdn.gist.ai/chatWidget.js"]');
1640        if (!existingScript) {
1641            const script = document.createElement('script');
1642            script.type = 'module';
1643            script.src = 'https://cdn.gist.ai/chatWidget.js'; // Production URL; for sandbox, use 'https://cdn.sandbox.gist.ai/chatWidget.js'
1644            script.async = true;
1645            script.onload = () => {
1646                console.log('Gist AI widget script loaded successfully.');
1647            };
1648            script.onerror = () => {
1649                console.error('Failed to load Gist AI widget script.');
1650            };
1651            document.head.appendChild(script);
1652            console.log('Gist AI script injected into <head>.');
1653        } else {
1654            console.log('Gist AI script already loaded, skipping script injection.');
1655        }
1656    
1657        // Create the widget container div for inline styling
1658        const widgetDiv = document.createElement('div');
1659        widgetDiv.id = 'gist-ai-widget';
1660        widgetDiv.style.cssText = `
1661            border-radius: 8px;
1662            text-align: center;
1663            max-width: 100%;
1664            box-sizing: border-box;
1665        `; // Inline-friendly styling: centered, bordered container
1666    
1667        // Create the custom element for the widget (omit 'floating' for inline behavior)
1668        const widgetElement = document.createElement('gist-chat-widget');
1669        widgetElement.setAttribute('api-key', 'pk_b5f65225378ae6f24e9908c7d9f1e683f1950f5d03e35e99acd5d0427ae354c4'); // Replace with your actual API key
1670        widgetElement.setAttribute('user-id', 'bangordailynews.com'); // Replace with your domain or organization ID, e.g., 'bangordailynews.com'
1671        // Removed 'floating' attribute to enable inline placement
1672        // Optionally add 'sticky-prompt' if you want the prompt sticky on results page
1673        widgetElement.setAttribute('sticky-prompt', ''); 
1674        widgetElement.setAttribute('default-placeholder', 'Search with AI…'); // Optional: custom placeholder
1675    
1676        // Style the widget element itself for better inline fit (adjust as needed)
1677        widgetElement.style.cssText = `
1678            display: block;
1679            margin: 0 auto;
1680            max-width: 600px; /* Limit width for article readability */
1681        `;
1682    
1683        // Append the widget element to the container div
1684        widgetDiv.appendChild(widgetElement);
1685    
1686        // Append the container div after the paywall content
1687        paywallContent.insertAdjacentElement('beforebegin', widgetDiv);
1688        console.log('Gist AI widget appended inline after #bdn_paywall_content (floating disabled).');
1689    }
1690}
1691
1692
1693
1694window.xxx = new BDN_Paywall(bdn_paywall_metadata);
1695window.userState = {
1696    userType: null,
1697    isSubscriber: null
1698};

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.