PageSourceSearch

https://kcnawatch.us/wp-content/plugins/nk-paywall/assets/js/nk-paywall.js?ver=1.4.0

js kcnawatch.us collected 2026-09-25 20:42:51 UTC 102,967 bytes, 2,688 lines download raw bytes

1/**
2 * NK Paywall — Alpine.js frontend.
3 *
4 * @package NKPaywall
5 */
6
7/**
8 * Matomo event tracking helper.
9 */
10const nkTrack = (category, action, name = '', value = 0) => {
11    if (typeof _paq !== 'undefined') {
12        _paq.push(['trackEvent', category, action, name, value]);
13    }
14};
15
16/**
17 * Shared email validation (signup, signin, etc.).
18 *
19 * @param {string} email Raw email string.
20 * @return {boolean} Whether the string looks like a valid email.
21 */
22function nkPaywallValidateEmail(email) {
23    return /^(([^<>()[\]\\.,;:\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,}))$/.test(
24        String(email || ''),
25    );
26}
27
28/** Once per page: avoid double AJAX when Alpine inits the component more than once. */
29let nkPaywallCheckDone = false;
30
31/** Once per page: the gate component must wire up (watchers, close handlers, URL
32 *  popup dispatch) on a single canonical instance even if Alpine inits it twice. */
33let nkPaywallGateInited = false;
34
35/**
36 * Cloudflare Turnstile invokes this global as soon as its (deferred) api.js loads.
37 * It MUST exist before that script runs, so it is defined here at module top level
38 * (this file is enqueued in the head with no defer, so it executes during HTML parse,
39 * before any deferred script). Defining it inside registerNkPaywallAlpineComponents()
40 * is too late: that runs on `alpine:init`, which only fires on DOMContentLoaded —
41 * after every deferred script (including Turnstile's api.js) has already run, so the
42 * callback would be undefined when Turnstile looks for it and it would time out.
43 */
44window.__nkPaywallTurnstileLoaded = false;
45window.turnstileLoaded = function () {
46    window.__nkPaywallTurnstileLoaded = true;
47    if (typeof window.nkPaywallInitPopupTurnstile === 'function') {
48        window.nkPaywallInitPopupTurnstile();
49    }
50};
51
52function registerNkPaywallAlpineComponents() {
53    if (typeof Alpine === 'undefined' || window.__nkPaywallAlpineRegistered) {
54        return;
55    }
56
57    window.__nkPaywallAlpineRegistered = true;
58
59    // Single ref so only one listener handles show-popup (avoids duplicate AJAX when gate inits multiple times).
60    let gateInstance = null;
61
62    const turnstileForms = [
63        {
64            formId: 'signin_form',
65            containerId: 'signin_turnstile',
66        },
67        {
68            formId: 'lostpasswordform',
69            containerId: 'forget_turnstile',
70        },
71        {
72            formId: 'pro_inquiry_form',
73            containerId: 'pro_inquiry_turnstile',
74            onVerified(form) {
75                form.dispatchEvent(
76                    new CustomEvent('nk-paywall-nkpro-pro-inquiry', {
77                        bubbles: true,
78                    }),
79                );
80            },
81        },
82        {
83            formId: 'fellowship_submit_form',
84            containerId: 'fellowship_turnstile',
85            onVerified(form) {
86                form.dispatchEvent(
87                    new CustomEvent('nk-paywall-nkpro-fellowship', {
88                        bubbles: true,
89                    }),
90                );
91            },
92        },
93    ];
94
95    const TURNSTILE_UNAVAILABLE_MSG =
96        'Security check is still loading. Please wait a moment and try again.';
97
98    const turnstileWidgets = new WeakMap();
99    const turnstilePending = new WeakMap();
100
101    function isTurnstileEnabled() {
102        return (
103            typeof nkPaywall !== 'undefined' &&
104            !!nkPaywall.turnstileEnabled &&
105            !!nkPaywall.turnstileSiteKey
106        );
107    }
108
109    function nativeSubmit(form) {
110        form.dataset.nkPaywallTurnstileBypass = '1';
111        HTMLFormElement.prototype.submit.call(form);
112    }
113
114    function setSubmitButtonsDisabled(form, disabled) {
115        if (!form) {
116            return;
117        }
118        const buttons = form.querySelectorAll(
119            'button[type="submit"], input[type="submit"]',
120        );
121        for (let i = 0; i < buttons.length; i++) {
122            buttons[i].disabled = disabled;
123        }
124    }
125
126    function isTurnstileReady() {
127        return (
128            typeof turnstile !== 'undefined' &&
129            typeof turnstile.render === 'function' &&
130            typeof turnstile.execute === 'function'
131        );
132    }
133
134    function setTurnstileToken(form, token) {
135        if (!form) {
136            return;
137        }
138        let field = form.querySelector('[name="cf-turnstile-response"]');
139        if (!field) {
140            field = document.createElement('input');
141            field.type = 'hidden';
142            field.name = 'cf-turnstile-response';
143            form.appendChild(field);
144        }
145        field.value = token || '';
146    }
147
148    function clearTurnstileToken(form) {
149        if (!form) {
150            return;
151        }
152        const field = form.querySelector('[name="cf-turnstile-response"]');
153        if (field) {
154            field.value = '';
155        }
156    }
157
158    function showTurnstileUnavailableError(form) {
159        if (!form) {
160            return;
161        }
162        if (form.id === 'signin_form') {
163            const errEl = document.getElementById('nk-paywall-signin-error');
164            if (errEl) {
165                errEl.textContent = TURNSTILE_UNAVAILABLE_MSG;
166                errEl.style.display = '';
167            }
168            return;
169        }
170        form.dispatchEvent(
171            new CustomEvent('nk-paywall-turnstile-unavailable', {
172                bubbles: true,
173                detail: { message: TURNSTILE_UNAVAILABLE_MSG },
174            }),
175        );
176    }
177
178    function renderTurnstileWidget(form, containerId, root) {
179        if (!form || turnstileWidgets.has(form) || !isTurnstileReady()) {
180            return;
181        }
182
183        const container = root.querySelector('#' + containerId);
184        if (!container) {
185            return;
186        }
187
188        const widgetId = turnstile.render(container, {
189            sitekey: nkPaywall.turnstileSiteKey,
190            execution: 'execute',
191            appearance: 'interaction-only',
192            callback(token) {
193                setTurnstileToken(form, token);
194                const pending = turnstilePending.get(form);
195                if (pending) {
196                    pending.resolve(token);
197                    turnstilePending.delete(form);
198                }
199            },
200            'error-callback'() {
201                const pending = turnstilePending.get(form);
202                if (pending) {
203                    pending.reject(new Error('turnstile-error'));
204                    turnstilePending.delete(form);
205                }
206            },
207            'expired-callback'() {
208                clearTurnstileToken(form);
209                // If a submit is waiting on this token, settle it so the handler's
210                // .catch re-arms the form. Otherwise a native submit would sit with its
211                // button disabled and inflight stuck at '1', unable to retry.
212                const pending = turnstilePending.get(form);
213                if (pending) {
214                    pending.reject(new Error('turnstile-expired'));
215                    turnstilePending.delete(form);
216                }
217                const widget = turnstileWidgets.get(form);
218                if (widget !== undefined) {
219                    turnstile.reset(widget);
220                }
221            },
222        });
223
224        turnstileWidgets.set(form, widgetId);
225    }
226
227    // How long a submit waits for Cloudflare's deferred api.js to finish loading and
228    // render the widget before giving up and showing the "still loading" message.
229    const TURNSTILE_READY_TIMEOUT_MS = 5000;
230    const TURNSTILE_READY_POLL_MS = 100;
231
232    // Resolve with the form's widget id once Turnstile is ready and its widget is
233    // rendered, polling until then. A fast submit can land before the deferred api.js
234    // has loaded; rather than failing instantly, wait briefly — turnstileLoaded →
235    // initPopupTurnstile renders the widget as soon as the script arrives, and this
236    // resolves right after. Rejects only if it never becomes ready within the timeout.
237    function waitForTurnstileWidget(form) {
238        const widgetReady = () =>
239            isTurnstileReady() && turnstileWidgets.get(form) !== undefined;
240        if (widgetReady()) {
241            return Promise.resolve(turnstileWidgets.get(form));
242        }
243        return new Promise((resolve, reject) => {
244            const start = Date.now();
245            const timer = setInterval(() => {
246                if (widgetReady()) {
247                    clearInterval(timer);
248                    resolve(turnstileWidgets.get(form));
249                    return;
250                }
251                if (Date.now() - start >= TURNSTILE_READY_TIMEOUT_MS) {
252                    clearInterval(timer);
253                    reject(new Error('turnstile-unavailable'));
254                }
255            }, TURNSTILE_READY_POLL_MS);
256        });
257    }
258
259    function executeTurnstile(form) {
260        if (!isTurnstileEnabled()) {
261            return Promise.reject(new Error('turnstile-unavailable'));
262        }
263
264        return waitForTurnstileWidget(form).then(
265            (widgetId) =>
266                new Promise((resolve, reject) => {
267                    turnstilePending.set(form, { resolve, reject });
268                    turnstile.execute(widgetId);
269                }),
270        );
271    }
272
273    function bindTurnstileForm(
274        { formId, containerId, onVerified },
275        root = document,
276    ) {
277        const form = root.querySelector('#' + formId);
278        if (!form) {
279            return;
280        }
281
282        // Rendering is idempotent (guarded by the turnstileWidgets WeakMap) and must be
283        // retried whenever Turnstile becomes ready. A form can be bound while
284        // Cloudflare's deferred api.js is still loading: renderTurnstileWidget then
285        // no-ops, and if we early-returned on the bound flag the widget would never get
286        // rendered — every submit would reject as "still loading" forever. So attempt
287        // the render on EVERY call, before the bound early-return, so the later
288        // turnstileLoaded → initPopupTurnstile pass renders it once the script arrives.
289        renderTurnstileWidget(form, containerId, root);
290
291        if (form.dataset.nkPaywallTurnstileBound === '1') {
292            return;
293        }
294        form.dataset.nkPaywallTurnstileBound = '1';
295
296        // Native-submit forms (sign-in, forgot-password) navigate the page on
297        // submit; the AJAX forms (pro inquiry, fellowship) manage their own
298        // in-flight state and re-enable their buttons downstream.
299        const isNativeSubmit = typeof onVerified !== 'function';
300        const verified = isNativeSubmit
301            ? () => nativeSubmit(form)
302            : () => onVerified(form);
303
304        form.addEventListener('submit', (event) => {
305            if (form.dataset.nkPaywallTurnstileBypass === '1') {
306                form.dataset.nkPaywallTurnstileBypass = '';
307                return;
308            }
309
310            if (!isTurnstileEnabled()) {
311                event.preventDefault();
312                verified();
313                return;
314            }
315
316            if (!form.checkValidity()) {
317                return;
318            }
319
320            event.preventDefault();
321
322            // Collapse rapid repeat submits (double-click, Enter held down).
323            // There is an async Turnstile round-trip between this click and the
324            // real nativeSubmit, and each submit reuses the ONE token cached in
325            // the widget. A second POST therefore lands as `timeout-or-duplicate`
326            // server-side and bounces the (already logged-in) user to the
327            // security-check error. Ignore submits while one is already in flight.
328            // Collapse rapid repeat submits for EVERY form: there is now an async
329            // wait+execute round-trip before the real submit, and each submit reuses
330            // the one token cached in the widget, so a second in-flight submit lands
331            // server-side as `timeout-or-duplicate`. Guard all forms (not just native
332            // ones) so the widened wait window can't spawn concurrent executes.
333            if (form.dataset.nkPaywallTurnstileInflight === '1') {
334                return;
335            }
336            form.dataset.nkPaywallTurnstileInflight = '1';
337            if (isNativeSubmit) {
338                setSubmitButtonsDisabled(form, true);
339            }
340
341            executeTurnstile(form)
342                .then(() => {
343                    verified();
344                    // AJAX forms don't navigate; re-arm once their handler has fired so
345                    // a later retry can submit. Native forms navigate on success — leave
346                    // inflight set so a pre-navigation double-click can't fire a dupe POST.
347                    if (!isNativeSubmit) {
348                        form.dataset.nkPaywallTurnstileInflight = '';
349                    }
350                })
351                .catch(() => {
352                    form.dataset.nkPaywallTurnstileInflight = '';
353                    if (isNativeSubmit) {
354                        setSubmitButtonsDisabled(form, false);
355                    }
356                    showTurnstileUnavailableError(form);
357                });
358        });
359    }
360
361    function initPopupTurnstile(root = document) {
362        turnstileForms.forEach((config) => bindTurnstileForm(config, root));
363    }
364
365    window.nkPaywallInitPopupTurnstile = initPopupTurnstile;
366
367    window.nkPaywallResetFormTurnstile = function (form) {
368        clearTurnstileToken(form);
369        const widgetId = turnstileWidgets.get(form);
370        if (widgetId !== undefined && isTurnstileReady()) {
371            turnstile.reset(widgetId);
372        }
373    };
374
375    // `window.turnstileLoaded` is defined at module top level so it exists before
376    // Cloudflare's deferred api.js runs. If Turnstile already loaded before Alpine
377    // initialised this component (the normal case with deferred scripts), bind the
378    // popup forms now — the top-level callback couldn't yet, as this function had
379    // not defined window.nkPaywallInitPopupTurnstile when it fired.
380    if (window.__nkPaywallTurnstileLoaded) {
381        initPopupTurnstile();
382    }
383
384    const SIGNIN_ERROR_MESSAGES = {
385        failed: 'The email or password you entered is incorrect!',
386        recaptcha: 'Your sign in request was rejected by the security check.',
387        locked: 'Sorry, we have locked your account to prevent malicious access. Please check your email for instructions on how to unlock it!',
388        auth_code:
389            'SECURITY ALERT: To ensure your account remains secure, please enter the token code we just emailed you while logging in.',
390        auth_error: 'Your security token code is wrong.',
391    };
392    const FORGOT_ERROR_MESSAGES = {
393        empty: 'Enter a valid email address...',
394        recaptcha:
395            'Your password reset request was rejected by the security check.',
396        generic: 'Unable to process your request. Please try again.',
397    };
398
399    function fixPopupFormUrls(root) {
400        if (!root) return;
401
402        var loc = window.location;
403        var pagePath = loc.pathname;
404
405        var signinForm = root.querySelector('#signin_form');
406        if (signinForm) {
407            signinForm.action = loc.origin + pagePath + '?popup=signin';
408        }
409
410        var forgotForm = root.querySelector('#lostpasswordform');
411        if (forgotForm) {
412            forgotForm.action =
413                loc.origin + pagePath + '?popup=forget-password-form';
414        }
415
416        var refererInputs = root.querySelectorAll(
417            'input[name="_wp_http_referer"]',
418        );
419        for (var i = 0; i < refererInputs.length; i++) {
420            refererInputs[i].value = pagePath;
421        }
422    }
423
424    function initResetPasswordForm(root) {
425        if (!root) return;
426        var form = root.querySelector('#resetpw_form');
427        if (!form) return;
428
429        form.addEventListener('submit', function (e) {
430            e.preventDefault();
431
432            var errorEl = root.querySelector('#nk-paywall-resetpw-error');
433            var successEl = root.querySelector('#nk-paywall-resetpw-success');
434            var submitBtn = form.querySelector('#nk-paywall-resetpw-submit');
435
436            if (errorEl) {
437                errorEl.style.display = 'none';
438                errorEl.textContent = '';
439            }
440            if (successEl) {
441                successEl.style.display = 'none';
442                successEl.textContent = '';
443            }
444
445            var password = form.querySelector('input[name="user_password"]');
446            var confirm = form.querySelector('input[name="confirm_password"]');
447            if (!password || !password.value || !confirm || !confirm.value) {
448                if (errorEl) {
449                    errorEl.textContent =
450                        "Sorry, we don't accept empty passwords.";
451                    errorEl.style.display = '';
452                }
453                return;
454            }
455
456            if (submitBtn) {
457                submitBtn.disabled = true;
458                submitBtn.textContent = 'Resetting\u2026';
459            }
460
461            var body = new FormData();
462            body.append('action', 'nk_paywall_reset_password');
463            body.append('nonce', nkPaywall.nonce);
464            body.append(
465                'rp_key',
466                form.querySelector('input[name="rp_key"]').value,
467            );
468            body.append(
469                'rp_login',
470                form.querySelector('input[name="rp_login"]').value,
471            );
472            body.append('user_password', password.value);
473            body.append('confirm_password', confirm.value);
474
475            fetch(nkPaywall.ajaxUrl, { method: 'POST', body: body })
476                .then(function (r) {
477                    return r.json();
478                })
479                .then(function (res) {
480                    if (res.success) {
481                        if (successEl) {
482                            successEl.textContent = res.data.message;
483                            successEl.style.display = '';
484                        }
485                        form.style.display = 'none';
486                        var subtitle = root.querySelector(
487                            '#nk-paywall-resetpw-subtitle',
488                        );
489                        if (subtitle) subtitle.style.display = 'none';
490                    } else {
491                        var msg =
492                            (res.data && res.data.message) ||
493                            'An error occurred. Please try again.';
494                        if (errorEl) {
495                            errorEl.textContent = msg;
496                            errorEl.style.display = '';
497                        }
498                    }
499                })
500                .catch(function () {
501                    if (errorEl) {
502                        errorEl.textContent =
503                            'Network error. Please try again.';
504                        errorEl.style.display = '';
505                    }
506                })
507                .finally(function () {
508                    if (submitBtn) {
509                        submitBtn.disabled = false;
510                        submitBtn.textContent = 'Reset Password';
511                    }
512                });
513        });
514    }
515
516    if (typeof nkPaywall !== 'undefined' && nkPaywall.showDailySubscribe) {
517        Alpine.data('nkPaywallDailySubscribe', () => ({
518            step: 'email',
519            email: '',
520            firstName: '',
521            loading: false,
522            errorMessage: '',
523
524            init() {
525                // Only the widget on the page should trigger the popup; skip when we're inside the gate modal (injected popup content)
526                if (this.$el && this.$el.closest('#nk-paywall-app')) return;
527
528                if (nkPaywall.loggedIn === '1') return;
529
530                setTimeout(() => {
531                    if (
532                        typeof canShowNkPaywallPopup !== 'function' ||
533                        !canShowNkPaywallPopup()
534                    )
535                        return;
536
537                    const key = 'subscribe-popup';
538                    const match = document.cookie.match(
539                        new RegExp(key + '=([^;]+)'),
540                    );
541                    const currentCount = match
542                        ? parseInt(match[1], 10) || 0
543                        : 0;
544                    const newCount = currentCount + 1;
545                    document.cookie =
546                        key + '=' + newCount + ';expires=0;secure;path=/';
547                    if (currentCount % 5 === 0) {
548                        showPopup('daily-subscribe');
549                    }
550                }, 5000);
551            },
552
553            goToName() {
554                const e = (this.email || '').trim();
555                if (!e || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)) return;
556                this.step = 'name';
557            },
558
559            submit() {
560                if (!this.email.trim() || !this.firstName.trim()) return;
561                this.loading = true;
562                this.errorMessage = '';
563                const body = new FormData();
564                body.append('action', 'nk_paywall_daily_subscribe');
565                body.append(
566                    'nonce',
567                    typeof nkPaywall !== 'undefined' ? nkPaywall.nonce : '',
568                );
569                body.append('email', this.email);
570                body.append('fname', this.firstName);
571                fetch(
572                    typeof nkPaywall !== 'undefined' ? nkPaywall.ajaxUrl : '',
573                    {
574                        method: 'POST',
575                        body,
576                    },
577                )
578                    .then((r) => r.json())
579                    .then((res) => {
580                        this.loading = false;
581                        if (res.success) this.step = 'success';
582                        else {
583                            this.step = 'error';
584                            this.errorMessage =
585                                res.data && (res.data.message || res.data.msg)
586                                    ? res.data.message || res.data.msg
587                                    : 'Something went wrong. Please try again.';
588                        }
589                    })
590                    .catch(() => {
591                        this.loading = false;
592                        this.step = 'error';
593                        this.errorMessage = 'Network error. Please try again.';
594                    });
595            },
596        }));
597    }
598
599    function isNkproSite() {
600        return typeof nkPaywall !== 'undefined' && nkPaywall.site === 'nkpro';
601    }
602
603    // The $1 trial is an NK News offer only (see SiteConfig / AccessController).
604    function isNkNewsSite() {
605        return typeof nkPaywall !== 'undefined' && nkPaywall.site === 'nknews';
606    }
607
608    function resolvePopupSlug(slug) {
609        if (isNkproSite() && (slug === 'signin-form' || slug === 'signup')) {
610            return 'nkpro-members-only';
611        }
612        return slug;
613    }
614
615    function showPopup(slug, locked, fullscreen, trial) {
616        const resolvedSlug = resolvePopupSlug(slug);
617        if (gateInstance.popup && gateInstance.popup !== resolvedSlug) {
618            gateInstance.closePopup();
619        }
620        if (!gateInstance.popup) {
621            gateInstance.showPopup(
622                resolvedSlug,
623                locked,
624                fullscreen,
625                undefined,
626                trial,
627            );
628        }
629    }
630
631    // Only one NK Paywall modal at a time: do not open daily-subscribe if another popup is already open.
632    function canShowNkPaywallPopup() {
633        return gateInstance && !gateInstance.popup;
634    }
635
636    const TRIAL_LANDING_ORIGIN = 'https://signup.nknews.org';
637    const TRIAL_LANDING_PATH = '/nknews/1-dollar-trial/';
638
639    // postMessage needs an exact target origin - '*' would broadcast to whoever
640    // happens to be framing us - and the landing page is shared by production and
641    // staging, so it can't hardcode one. Tell it which origin we are; it checks
642    // that against its own allowlist before posting anything back.
643    function trialLandingUrl() {
644        return (
645            TRIAL_LANDING_ORIGIN +
646            TRIAL_LANDING_PATH +
647            '?parent=' +
648            encodeURIComponent(window.location.origin)
649        );
650    }
651
652    // Agreed payload for the landing page's in-page CTAs; see the `message`
653    // listener below.
654    const TRIAL_LANDING_CTA_MESSAGE = 'nk-paywall:trial-cta';
655
656    // Non-null only while our own trial lightbox is open, so a stray message can
657    // never start the trial flow or close somebody else's modal.
658    let trialLightboxSession = null;
659
660    // $1-trial CTA, logged-out visitor: show the marketing landing page
661    // (signup.nknews.org) in a bs5-lightbox first; once the visitor closes it,
662    // `onClose` opens the real signup-form popup. bs5-lightbox reads its
663    // config off data-* attributes on the element passed to its constructor,
664    // so a detached <a> is enough - it never gets inserted into the DOM.
665    // Bootstrap's modal events bubble to `document`, so a one-time listener
666    // there catches the close without needing bs5-lightbox's internal modal
667    // node.
668    function openTrialLandingLightbox(onClose) {
669        if (typeof Lightbox === 'undefined') {
670            // Library failed to load: don't block the trial flow on it.
671            onClose();
672            return;
673        }
674
675        const trigger = document.createElement('a');
676        trigger.href = trialLandingUrl();
677        trigger.setAttribute('data-type', 'url');
678
679        // Runs exactly once per open, whichever ends it: the visitor dismissing
680        // the lightbox, or a CTA click relayed from inside the landing page.
681        const session = { lightbox: null, done: false };
682        session.finish = () => {
683            if (session.done) {
684                return;
685            }
686            session.done = true;
687            if (trialLightboxSession === session) {
688                trialLightboxSession = null;
689            }
690            onClose();
691        };
692
693        // Only arm the session (and the close handler) once the lightbox itself
694        // exists, so a constructor failure can't leave a stale one behind.
695        session.lightbox = new Lightbox(trigger, { size: 'xl' });
696        trialLightboxSession = session;
697
698        document.addEventListener('hidden.bs.modal', session.finish, {
699            once: true,
700        });
701
702        session.lightbox.show();
703    }
704
705    // The message must come from the iframe we opened, not from some other frame
706    // that happens to share the landing page's origin.
707    function isTrialLightboxFrame(session, source) {
708        const modalEl = session.lightbox && session.lightbox.modalElement;
709        if (!modalEl || !source) {
710            return false;
711        }
712        return Array.prototype.some.call(
713            modalEl.querySelectorAll('iframe'),
714            (iframe) => iframe.contentWindow === source,
715        );
716    }
717
718    // bs5-lightbox wraps a real bootstrap Modal, so hide() emits
719    // `hidden.bs.modal` and the flow continues through session.finish() exactly
720    // as it does when the visitor closes the lightbox themselves.
721    function closeTrialLandingLightbox(session) {
722        if (session.lightbox && typeof session.lightbox.hide === 'function') {
723            session.lightbox.hide();
724            return;
725        }
726        session.finish();
727    }
728
729    // The landing page renders in a cross-origin iframe, so the host page's own
730    // click handlers can't see its in-page "start trial" CTAs. Those CTAs signal
731    // us instead with window.parent.postMessage('nk-paywall:trial-cta', origin).
732    // Any window can post to any other, so the signal is only honoured when all
733    // of these hold: our trial lightbox is currently open, the message came from
734    // the landing page's origin, it came from that lightbox's own frame, and it
735    // carries the exact agreed payload. Even then it only closes the lightbox —
736    // the resulting `hidden.bs.modal` runs the same onClose a manual dismissal
737    // would, so a forged message could do nothing the visitor can't already do.
738    window.addEventListener('message', (event) => {
739        const session = trialLightboxSession;
740        if (!session || session.done) {
741            return;
742        }
743        if (event.origin !== TRIAL_LANDING_ORIGIN) {
744            return;
745        }
746        const data = event.data;
747        const type = typeof data === 'string' ? data : data && data.type;
748        if (TRIAL_LANDING_CTA_MESSAGE !== type) {
749            return;
750        }
751        if (!isTrialLightboxFrame(session, event.source)) {
752            return;
753        }
754        closeTrialLandingLightbox(session);
755    });
756
757    // Capture: .nk-paywall-modal uses @click.stop so bubble-phase listeners on document never run for in-modal clicks.
758    document.addEventListener(
759        'click',
760        (e) => {
761            const popupAttribute = 'data-nk-paywall-popup';
762            const trialAttribute = 'data-nk-paywall-trial';
763            const btn = e.target.closest(`[${popupAttribute}]`);
764            // return first
765
766            if (!btn || !btn.getAttribute(popupAttribute)) {
767                return;
768            }
769
770            e.preventDefault();
771            const slug = btn.getAttribute(popupAttribute);
772            const trial =
773                'signup-form' === slug &&
774                'true' === btn.getAttribute(trialAttribute);
775
776            if (trial && gateInstance && !gateInstance.loggedIn) {
777                openTrialLandingLightbox(() => {
778                    showPopup(slug, undefined, undefined, trial);
779                });
780                return;
781            }
782
783            showPopup(slug, undefined, undefined, trial);
784        },
785        true,
786    );
787
788    // Intent-based prefetch: warm the popup cache the moment the user shows intent
789    // (hover or keyboard focus on a trigger), so the eventual click opens instantly.
790    // Deduped per cache key in _fetchPopupHtml, so repeated events cost nothing.
791    const prefetchFromIntent = (e) => {
792        if (
793            !gateInstance ||
794            !e.target ||
795            typeof e.target.closest !== 'function'
796        ) {
797            return;
798        }
799        const btn = e.target.closest('[data-nk-paywall-popup]');
800        const slug = btn && btn.getAttribute('data-nk-paywall-popup');
801        if (slug) {
802            const trial =
803                'signup-form' === slug &&
804                'true' === btn.getAttribute('data-nk-paywall-trial');
805            gateInstance.prefetchPopup(slug, trial);
806        }
807    };
808    document.addEventListener('pointerover', prefetchFromIntent, true);
809    document.addEventListener('focusin', prefetchFromIntent, true);
810
811    window.addEventListener('nk-paywall:open-popup', (event) => {
812        const slug = event.detail && event.detail.slug;
813        if (!slug) {
814            return;
815        }
816        const locked =
817            typeof event.detail.locked === 'boolean'
818                ? event.detail.locked
819                : undefined;
820        const fullscreen =
821            typeof event.detail.fullscreen === 'boolean'
822                ? event.detail.fullscreen
823                : undefined;
824        showPopup(slug, locked, fullscreen);
825    });
826
827    window.addEventListener('nk-paywall-close', () => {
828        if (gateInstance && !gateInstance.popupLocked) {
829            gateInstance.closePopup();
830        }
831    });
832
833    /**
834     * Main paywall gate controller.
835     * Fires AJAX access check on init, handles content reveal / popup display.
836     */
837    Alpine.data('nkPaywallGate', () => ({
838        granted: false,
839        popup: null,
840        popupHtml: '',
841        popupLocked: false,
842        popupFullscreen: false,
843        ctaHtml: '',
844        remaining: -1,
845        limit: 0,
846        loggedIn:
847            typeof nkPaywall !== 'undefined' &&
848            (nkPaywall.loggedIn === true || nkPaywall.loggedIn === '1'),
849        nkproPopupState: '',
850        loading: true,
851
852        init() {
853            const el = this.$el;
854            el.style.display = '';
855
856            // Alpine can initialise this component more than once per page
857            // (deferred/delayed script execution, DOM re-walks). Everything below —
858            // registering watchers, binding close handlers, and opening popups from
859            // URL params — must happen exactly once on a single canonical instance.
860            // A second init() would, on the signin-error reload path, re-run
861            // this.showPopup('signin-form') and re-wire popupHtml rendering, leaving a
862            // duplicate form and a stuck backdrop blur after close. Bail out for any
863            // subsequent invocation.
864            if (nkPaywallGateInited) {
865                return;
866            }
867            nkPaywallGateInited = true;
868            gateInstance = this;
869
870            this._bindModalCloseHandlers(el);
871
872            this.$watch('popupHtml', (html) => {
873                this.$nextTick(() => this._renderPopup(html));
874            });
875
876            // Lock page scroll whenever any paywall popup is open, not just locked ones.
877            this.$watch('popup', (slug) => {
878                document.body.style.overflow = slug ? 'hidden' : '';
879            });
880
881            let skipCheckAccess = false;
882
883            var sp = new URLSearchParams(window.location.search);
884            var loginErr = sp.get('login');
885            if (
886                sp.get('popup') === 'resetpw' &&
887                sp.get('key') &&
888                sp.get('login')
889            ) {
890                this._resetPwKey = sp.get('key');
891                this._resetPwLogin = sp.get('login');
892                window.history.replaceState(null, '', window.location.pathname);
893                this.showPopup('reset-password-form');
894                skipCheckAccess = true;
895            } else if (
896                sp.get('popup') === 'forget-password-form' &&
897                (sp.get('forgot') === 'sent' || sp.get('forgot') === 'failed')
898            ) {
899                var forgotStatus = sp.get('forgot');
900                var forgotCode = sp.get('forgot_code') || 'generic';
901                window.history.replaceState(null, '', window.location.pathname);
902                this.showPopup('forget-password-form');
903                skipCheckAccess = true;
904
905                var forgotPollTimer = setInterval(function () {
906                    var errorEl = document.getElementById(
907                        'nk-paywall-forget-error',
908                    );
909                    var successEl = document.getElementById(
910                        'nk-paywall-forget-success',
911                    );
912                    var titleEl = document.getElementById(
913                        'nk-paywall-forget-title',
914                    );
915                    var iconEl = document.getElementById(
916                        'nk-paywall-forget-mail-icon',
917                    );
918                    var subtitleEl = document.getElementById(
919                        'nk-paywall-forget-subtitle',
920                    );
921                    var formEl = document.getElementById('lostpasswordform');
922
923                    if (!errorEl || !successEl) {
924                        return;
925                    }
926
927                    clearInterval(forgotPollTimer);
928
929                    if (forgotStatus === 'sent') {
930                        var forgetRoot =
931                            document.getElementById('forget-password');
932                        if (forgetRoot) {
933                            forgetRoot.classList.add(
934                                'nk-paywall-forget--email-sent',
935                            );
936                        }
937                        successEl.style.display = 'none';
938                        if (iconEl) iconEl.style.display = 'block';
939                        if (titleEl) titleEl.textContent = 'Email Sent';
940                        if (subtitleEl) {
941                            subtitleEl.textContent =
942                                'If an account is linked to the email address you provided, a password reset link will arrive to that inbox shortly.';
943                            subtitleEl.style.display = '';
944                        }
945                        if (formEl) formEl.style.display = 'none';
946                    } else {
947                        if (iconEl) iconEl.style.display = 'none';
948                        errorEl.textContent =
949                            FORGOT_ERROR_MESSAGES[forgotCode] ||
950                            FORGOT_ERROR_MESSAGES.generic;
951                        errorEl.style.display = '';
952                    }
953                }, 150);
954
955                setTimeout(function () {
956                    clearInterval(forgotPollTimer);
957                }, 10000);
958            } else if (
959                sp.get('popup') === 'signin' &&
960                loginErr &&
961                SIGNIN_ERROR_MESSAGES[loginErr]
962            ) {
963                this._signinError = loginErr;
964                window.history.replaceState(null, '', window.location.pathname);
965                this.showPopup('signin-form');
966                skipCheckAccess = true;
967
968                var errorMsg = SIGNIN_ERROR_MESSAGES[loginErr];
969                var isAuthCode =
970                    loginErr === 'auth_code' || loginErr === 'auth_error';
971                var pollTimer = setInterval(function () {
972                    var el = document.getElementById('nk-paywall-signin-error');
973                    if (el) {
974                        clearInterval(pollTimer);
975                        el.textContent = errorMsg;
976                        el.style.display = '';
977                        if (isAuthCode) {
978                            window.dispatchEvent(
979                                new CustomEvent('set-auth-mode'),
980                            );
981                        }
982                    }
983                }, 150);
984                setTimeout(function () {
985                    clearInterval(pollTimer);
986                }, 10000);
987            } else if (sp.get('popup') === 'signin' && !loginErr) {
988                window.history.replaceState(null, '', window.location.pathname);
989                this.showPopup('signin-form');
990                skipCheckAccess = true;
991            } else if (sp.get('popup') === 'signup' && isNkproSite()) {
992                this.nkproPopupState = this.loggedIn
993                    ? 'logged_in_upgrade'
994                    : 'logged_out_signin';
995                window.history.replaceState(null, '', window.location.pathname);
996                if (
997                    this.loggedIn &&
998                    typeof nkPaywall !== 'undefined' &&
999                    nkPaywall.postId > 0
1000                ) {
1001                    this._urlPopupPending = 'nkpro-members-only';
1002                } else {
1003                    skipCheckAccess = true;
1004                    this.showPopup('nkpro-members-only', false, false, true);
1005                }
1006            } else if (
1007                sp.get('popup') === 'signup' &&
1008                (nkPaywall.site === 'nknews' || nkPaywall.site === 'koreapro')
1009            ) {
1010                window.history.replaceState(null, '', window.location.pathname);
1011                this.showPopup('signup-form');
1012                skipCheckAccess = true;
1013            } else if (sp.get('popup') === 'trial' && isNkNewsSite()) {
1014                // Trial signup form with $1 pricing, no marketing lightbox: a
1015                // visitor arriving on this URL has already come from the landing
1016                // page (or from a link that stands in for it), so re-showing it
1017                // would just put a step back in front of them.
1018                window.history.replaceState(null, '', window.location.pathname);
1019                showPopup('signup-form', undefined, undefined, true);
1020                skipCheckAccess = true;
1021            } else if (sp.get('popup') === 'free-member-form') {
1022                window.history.replaceState(null, '', window.location.pathname);
1023                this.showPopup('free-member-form');
1024                skipCheckAccess = true;
1025            } else if (
1026                window.__nkPaywallTossReturnState &&
1027                !window.__nkPaywallTossGateHandled
1028            ) {
1029                window.__nkPaywallTossGateHandled = true;
1030                this.showPopup('signup-form');
1031                skipCheckAccess = true;
1032            } else if (
1033                !window.__nkPaywallTossGateHandled &&
1034                this._detectTossFailFromUrl(sp)
1035            ) {
1036                window.__nkPaywallTossGateHandled = true;
1037                this.showPopup('signup-form');
1038                skipCheckAccess = true;
1039            }
1040
1041            if (nkPaywallCheckDone) {
1042                return;
1043            }
1044            nkPaywallCheckDone = true;
1045
1046            // KCNA homepage: never auto-open a gating popup. Explicit popups from
1047            // query params (e.g. redirect ?popup=, signin errors) are handled above.
1048            if (typeof nkPaywall !== 'undefined' && nkPaywall.isKcnaHome) {
1049                skipCheckAccess = true;
1050            }
1051
1052            if (!skipCheckAccess) {
1053                this.checkAccess();
1054            }
1055        },
1056
1057        _detectTossFailFromUrl(sp) {
1058            var tossCode = sp.get('code');
1059            if (!tossCode) return false;
1060            var tossPending = false;
1061            try {
1062                tossPending =
1063                    sessionStorage.getItem('nk_paywall_toss_pending') === '1';
1064            } catch (_) {
1065                /* ignore */
1066            }
1067            if (!tossPending) return false;
1068            try {
1069                sessionStorage.removeItem('nk_paywall_toss_pending');
1070            } catch (_) {
1071                /* ignore */
1072            }
1073            var msg =
1074                sp.get('message') ||
1075                tossCode
1076                    .replace(/_/g, ' ')
1077                    .toLowerCase()
1078                    .replace(/^\w/, function (c) {
1079                        return c.toUpperCase();
1080                    });
1081            window.__nkPaywallTossReturnState = {
1082                type: 'fail',
1083                message: msg,
1084            };
1085            window.history.replaceState(null, '', window.location.pathname);
1086            return true;
1087        },
1088
1089        _renderPopup(html) {
1090            const container = this.$refs.popupBody;
1091            if (!container) return;
1092            container.innerHTML = html || '';
1093            if (typeof Alpine !== 'undefined' && Alpine.initTree) {
1094                Alpine.initTree(container);
1095            }
1096            fixPopupFormUrls(container);
1097            initPopupTurnstile(container);
1098
1099            if (this._resetPwKey && this._resetPwLogin) {
1100                var keyInput = container.querySelector('input[name="rp_key"]');
1101                var loginInput = container.querySelector(
1102                    'input[name="rp_login"]',
1103                );
1104                if (keyInput) keyInput.value = this._resetPwKey;
1105                if (loginInput) loginInput.value = this._resetPwLogin;
1106            }
1107
1108            initResetPasswordForm(container);
1109        },
1110
1111        checkAccess() {
1112            const body = new FormData();
1113            body.append('action', 'nk_paywall_check');
1114            body.append('post_id', nkPaywall.postId);
1115            body.append('referrer', document.referrer || '');
1116
1117            const urlParams = new URLSearchParams(window.location.search);
1118            const shareToken = urlParams.get('share');
1119            if (shareToken) {
1120                body.append('share', shareToken);
1121            }
1122
1123            fetch(nkPaywall.ajaxUrl, { method: 'POST', body })
1124                .then((r) => r.json())
1125                .then((res) => {
1126                    this.loading = false;
1127
1128                    if (!res.success) return;
1129
1130                    const d = res.data;
1131                    this.granted = d.granted;
1132
1133                    // Korea-based visitors: point header KCNA link at the proxy domain.
1134                    if (typeof d.is_kor !== 'undefined' && d.is_kor) {
1135                        document.querySelectorAll('.kcna_url').forEach((el) => {
1136                            el.setAttribute('href', 'https://' + d.is_kor);
1137                        });
1138                    }
1139                    const popupLocked = !!d.popup_locked;
1140                    const popupFullscreen =
1141                        typeof d.popup_fullscreen === 'boolean'
1142                            ? d.popup_fullscreen
1143                            : popupLocked;
1144
1145                    if (!this.popupLocked) {
1146                        this.popupLocked = !d.granted && popupLocked;
1147                    }
1148                    if (!this.popupFullscreen) {
1149                        this.popupFullscreen = !d.granted && popupFullscreen;
1150                    }
1151                    this.loggedIn = d.logged_in;
1152                    this.nkproPopupState =
1153                        typeof d.nkpro_popup_state === 'string'
1154                            ? d.nkpro_popup_state
1155                            : '';
1156                    this.remaining = d.remaining ?? -1;
1157                    this.limit = d.limit ?? 0;
1158
1159                    // CTA area: spinner until AJAX returns; then CTA HTML or hide when granted.
1160                    const ctaContainer =
1161                        document.querySelector('#cta-container');
1162                    if (ctaContainer) {
1163                        if (d.granted) {
1164                            ctaContainer.style.display = 'none';
1165                            ctaContainer.innerHTML = '';
1166                            ctaContainer.classList.remove(
1167                                'nk-paywall-cta-loading',
1168                            );
1169                        } else {
1170                            ctaContainer.style.display = '';
1171                            if (d.cta_html) {
1172                                ctaContainer.innerHTML = d.cta_html;
1173                                ctaContainer.classList.remove(
1174                                    'nk-paywall-cta-loading',
1175                                );
1176                            }
1177                        }
1178                    }
1179
1180                    if (d.granted && d.content) {
1181                        this.revealContent(d.content);
1182                    }
1183
1184                    if (
1185                        d.granted &&
1186                        typeof jQuery !== 'undefined' &&
1187                        typeof jQuery.fn.bxSlider === 'function'
1188                    ) {
1189                        const articleEl =
1190                            document.querySelector('.editor-content') ||
1191                            document.querySelector('.news-full-desc');
1192                        if (articleEl) {
1193                            const $unslid = jQuery(articleEl)
1194                                .find('.gallery')
1195                                .filter(function () {
1196                                    return !jQuery(this).closest('.bx-wrapper')
1197                                        .length;
1198                                });
1199                            if ($unslid.length) {
1200                                $unslid.bxSlider({
1201                                    slideSelector: '.gallery-item',
1202                                    mode: 'fade',
1203                                    speed: 0,
1204                                    keyboardEnabled: true,
1205                                    controls: true,
1206                                });
1207                                $unslid
1208                                    .find('.gallery-item a')
1209                                    .on('click', function (e) {
1210                                        e.preventDefault();
1211                                    });
1212                            }
1213                        }
1214                    }
1215
1216                    if (
1217                        d.granted &&
1218                        typeof window.trigger_main === 'function'
1219                    ) {
1220                        try {
1221                            window.trigger_main();
1222                        } catch (_) {
1223                            /* legacy Vue 2 helper; may fail on Vue 3 tools */
1224                        }
1225                    }
1226
1227                    var toolsApp = document.querySelector(
1228                        'body.research-tools #app',
1229                    );
1230                    if (toolsApp) {
1231                        toolsApp.style.visibility = d.granted
1232                            ? 'visible'
1233                            : 'hidden';
1234                    }
1235
1236                    if (d.cta_html) {
1237                        this.ctaHtml = d.cta_html;
1238                        if (d.granted) {
1239                            this.injectCta();
1240                        }
1241                        nkTrack('Paywall', 'CTA Shown', d.cta);
1242                    }
1243
1244                    if (
1245                        d.popup_html &&
1246                        !d.granted &&
1247                        !this.popup &&
1248                        !this._urlPopupPending
1249                    ) {
1250                        this.popupHtml = d.popup_html;
1251                        this.popup = d.popup;
1252                        nkTrack('Paywall', 'Popup Shown', d.popup);
1253                    }
1254                    this.updateSidebar();
1255                    this._flushUrlPopupPending();
1256
1257                    window.dispatchEvent(
1258                        new CustomEvent('nk-paywall:access-resolved', {
1259                            detail: {
1260                                granted: d.granted,
1261                                loggedIn: d.logged_in,
1262                            },
1263                        }),
1264                    );
1265                })
1266                .catch(() => {
1267                    this.loading = false;
1268                    this._flushUrlPopupPending();
1269                });
1270        },
1271
1272        _flushUrlPopupPending() {
1273            if (!this._urlPopupPending) {
1274                return;
1275            }
1276            const slug = this._urlPopupPending;
1277            this._urlPopupPending = null;
1278            this.showPopup(slug, false, false, true);
1279        },
1280
1281        revealContent(html) {
1282            // Prefer plugin-owned structure: full content comes only from AJAX, inject into empty div.
1283            const pluginFull = document.querySelector('.nk-paywall-full');
1284            const pluginExcerpt = document.querySelector('.nk-paywall-excerpt');
1285            const pluginWrap = document.querySelector(
1286                '.nk-paywall-content-wrap',
1287            );
1288
1289            if (pluginFull && pluginExcerpt) {
1290                if (html) {
1291                    pluginFull.innerHTML = html;
1292                }
1293                pluginExcerpt.style.display = 'none';
1294                pluginFull.style.display = '';
1295                if (pluginWrap) {
1296                    pluginWrap.style.display = '';
1297                }
1298                this.processEmbeds(pluginFull);
1299                return;
1300            }
1301
1302            // Legacy: theme outputs #fullContent / #excerptContent.
1303            const full = document.getElementById('fullContent');
1304            const excerpt = document.getElementById('excerptContent');
1305            const wrapper = document.getElementById('content-wrapper');
1306
1307            if (full) {
1308                full.innerHTML = html;
1309                full.style.display = '';
1310                full.classList.remove('d-none');
1311            }
1312
1313            if (excerpt) {
1314                excerpt.style.display = 'none';
1315            }
1316
1317            if (wrapper) {
1318                wrapper.classList.remove('limited');
1319            }
1320
1321            this.processEmbeds(full || wrapper);
1322
1323            // NK Pro: theme uses .news-full-desc.limited with custom excerpt.
1324            const nkproDesc = document.querySelector('.news-full-desc.limited');
1325            if (nkproDesc) {
1326                if (html) {
1327                    nkproDesc.innerHTML = html;
1328                }
1329                nkproDesc.classList.remove('limited');
1330                this.processEmbeds(nkproDesc);
1331            }
1332        },
1333
1334        processEmbeds(container) {
1335            if (!container) return;
1336
1337            container.querySelectorAll('iframe').forEach((iframe) => {
1338                if (iframe.dataset.src && !iframe.src) {
1339                    iframe.src = iframe.dataset.src;
1340                }
1341            });
1342
1343            container.querySelectorAll('img[data-lazy-src]').forEach((img) => {
1344                img.src = img.dataset.lazySrc;
1345            });
1346
1347            // --- Article post-processing (parity with leaky-paywall-ajax script_for_article;
1348            //    #fullContent / #print_area → scoped to this container, plugin uses #nk-paywall-content > .nk-paywall-full) ---
1349
1350            // Prevent caption wrapper links from navigating (image captions link to attachment page by default).
1351            container.querySelectorAll('.wp-caption > a').forEach((a) => {
1352                a.addEventListener('click', (e) => e.preventDefault());
1353            });
1354
1355            // Open caption-text and image-credit links in a new tab.
1356            container
1357                .querySelectorAll('.wp-caption-text > a, .image-credit a')
1358                .forEach((a) => a.setAttribute('target', '_blank'));
1359
1360            // Image lightbox (lity): same targets as legacy — wp-image-* and gallery thumbs only (not every <img>).
1361            // Capture phase so bxSlider viewport/touch handlers do not consume the event on bubble.
1362            const prevLityCap = container._nkPaywallLityCapture;
1363            if (prevLityCap) {
1364                container.removeEventListener('click', prevLityCap, true);
1365            }
1366            container._nkPaywallLityCapture = function (e) {
1367                if (typeof lity !== 'function') {
1368                    return;
1369                }
1370                const t = e.target;
1371                if (!t || !t.closest) {
1372                    return;
1373                }
1374                let img = t.closest('img:not(.pdf-download)');
1375                if (!img && t.matches && t.matches('a')) {
1376                    img = t.querySelector('img:not(.pdf-download)');
1377                }
1378                if (!img || !container.contains(img)) {
1379                    return;
1380                }
1381                const inGalleryItem = img.closest('.gallery .gallery-item');
1382                const isWpMedia =
1383                    typeof img.className === 'string' &&
1384                    img.className.indexOf('wp-image-') !== -1;
1385                if (!inGalleryItem && !isWpMedia) {
1386                    return;
1387                }
1388                e.preventDefault();
1389                e.stopPropagation();
1390                lity(img.src);
1391            };
1392            container.addEventListener(
1393                'click',
1394                container._nkPaywallLityCapture,
1395                true,
1396            );
1397
1398            // Prevent drag and right-click on captioned images (skip .gallery: mousedown preventDefault kills click → breaks lity/bxSlider).
1399            container.querySelectorAll('.wp-caption img').forEach((img) => {
1400                if (img.closest('.gallery')) {
1401                    return;
1402                }
1403                img.addEventListener('mousedown', (e) => e.preventDefault());
1404                img.addEventListener('contextmenu', () => false);
1405            });
1406
1407            // Wrap bare tables in Bootstrap responsive classes.
1408            container
1409                .querySelectorAll('table:not(.table-bordered)')
1410                .forEach((table) => {
1411                    table.classList.add('table', 'table-bordered');
1412                    const wrapper = document.createElement('div');
1413                    wrapper.className = 'table-responsive';
1414                    table.parentNode.insertBefore(wrapper, table);
1415                    wrapper.appendChild(table);
1416                });
1417
1418            // Wrap YouTube iframes in a 16:9 ratio container (skip Datawrapper embeds).
1419            container.querySelectorAll('iframe').forEach((iframe) => {
1420                const src = iframe.getAttribute('src') || '';
1421                if (
1422                    !src.includes('youtu') ||
1423                    (iframe.id && iframe.id.startsWith('datawrapper')) ||
1424                    iframe.classList.contains('embed-responsive-item')
1425                ) {
1426                    return;
1427                }
1428                iframe.classList.add('embed-responsive-item');
1429                const wrapper = document.createElement('div');
1430                wrapper.className = 'ratio ratio-16x9';
1431                iframe.parentNode.insertBefore(wrapper, iframe);
1432                wrapper.appendChild(iframe);
1433            });
1434
1435            // Gallery slider via bxSlider (jQuery plugin enqueued by themes on singles).
1436            if (
1437                typeof jQuery !== 'undefined' &&
1438                typeof jQuery.fn.bxSlider === 'function'
1439            ) {
1440                const $galleries = jQuery(container).find('.gallery');
1441                if ($galleries.length) {
1442                    $galleries.bxSlider({
1443                        slideSelector: '.gallery-item',
1444                        mode: 'fade',
1445                        speed: 0,
1446                        keyboardEnabled: true,
1447                        controls: true,
1448                    });
1449                }
1450            }
1451
1452            // Prevent gallery item links from navigating (slider handles display).
1453            container
1454                .querySelectorAll('.gallery .gallery-item a')
1455                .forEach((a) => {
1456                    a.addEventListener('click', (e) => e.preventDefault());
1457                });
1458
1459            // TwentyTwenty image comparison re-init after AJAX content injection.
1460            if (
1461                typeof jQuery !== 'undefined' &&
1462                typeof jQuery.fn.twentytwenty === 'function'
1463            ) {
1464                const $t20 = jQuery(container).find('.twentytwenty-container');
1465                if ($t20.length) {
1466                    setTimeout(() => $t20.twentytwenty(), 1000);
1467                }
1468            }
1469        },
1470
1471        injectCta() {
1472            if (!this.ctaHtml) return;
1473
1474            const target =
1475                document.querySelector('.nk-paywall-full') ||
1476                document.getElementById('fullContent') ||
1477                document.getElementById('content-wrapper') ||
1478                document.querySelector('.news-full-desc');
1479
1480            if (target && target.parentNode) {
1481                let wrapper = target.nextElementSibling;
1482
1483                if (
1484                    !wrapper ||
1485                    !wrapper.classList.contains('nk-paywall-cta-wrapper')
1486                ) {
1487                    wrapper = document.createElement('div');
1488                    wrapper.className = 'nk-paywall-cta-wrapper';
1489                    target.parentNode.insertBefore(wrapper, target.nextSibling);
1490                }
1491
1492                wrapper.innerHTML = this.ctaHtml;
1493            }
1494        },
1495
1496        updateSidebar() {
1497            const widgets = document.querySelectorAll(
1498                '[data-nk-paywall-sidebar]',
1499            );
1500            if (!widgets.length || this.remaining < 0) return;
1501
1502            widgets.forEach((widget) => {
1503                widget.style.display = '';
1504                const counter = widget.querySelector('[data-remaining]');
1505                if (counter) {
1506                    counter.textContent = this.remaining + '/' + this.limit;
1507                }
1508            });
1509        },
1510
1511        // State that varies the rendered popup HTML for a given slug (only nkpro
1512        // members-only carries an extra state). Used to build the cache key.
1513        _popupState(slug) {
1514            return slug === 'nkpro-members-only' &&
1515                typeof this.nkproPopupState === 'string' &&
1516                this.nkproPopupState
1517                ? this.nkproPopupState
1518                : '';
1519        },
1520
1521        // Cache key for a resolved slug. remaining/limit are included because some
1522        // templates interpolate the article counter, so a different count = different HTML.
1523        _popupCacheKey(slug, state, trial) {
1524            return [
1525                slug,
1526                state || '',
1527                this.remaining,
1528                this.limit,
1529                trial ? '1' : '0',
1530            ].join('|');
1531        },
1532
1533        // Fetch (and cache) the rendered popup HTML for a resolved slug.
1534        // Returns a Promise resolving to the HTML string, or null on failure.
1535        // Concurrent calls for the same key share a single in-flight request, so a
1536        // hover-prefetch followed by a click never fires two server requests.
1537        _fetchPopupHtml(slug, state, trial) {
1538            this._popupCache = this._popupCache || {};
1539            const key = this._popupCacheKey(slug, state, trial);
1540            if (this._popupCache[key]) {
1541                return this._popupCache[key];
1542            }
1543
1544            const body = new FormData();
1545            body.append('action', 'nk_paywall_get_popup');
1546            body.append('popup', slug);
1547            body.append('remaining', this.remaining);
1548            body.append('limit', this.limit);
1549            if (slug === 'nkpro-members-only' && state) {
1550                body.append('nkpro_popup_state', state);
1551            }
1552            if (slug === 'signup-form' && trial) {
1553                body.append('trial', '1');
1554            }
1555
1556            const promise = fetch(nkPaywall.ajaxUrl, { method: 'POST', body })
1557                .then((r) => r.json())
1558                .then((res) => {
1559                    if (res && res.success && res.data && res.data.html) {
1560                        return res.data.html;
1561                    }
1562                    throw new Error('nk-paywall: empty popup response');
1563                })
1564                .catch(() => {
1565                    // Don't cache failures — allow a retry on the next trigger.
1566                    delete this._popupCache[key];
1567                    return null;
1568                });
1569
1570            this._popupCache[key] = promise;
1571            return promise;
1572        },
1573
1574        // Warm the cache on user intent (hover / keyboard focus) before the click.
1575        // Harmless if it's never opened; the result is just held for the eventual open.
1576        prefetchPopup(rawSlug, trial) {
1577            if (!rawSlug) {
1578                return;
1579            }
1580            const slug = resolvePopupSlug(rawSlug);
1581            this._fetchPopupHtml(slug, this._popupState(slug), trial);
1582        },
1583
1584        showPopup(slug, locked, fullscreen, force, trial) {
1585            const forceOpen = force === true;
1586            if (this.popup && !forceOpen) {
1587                return;
1588            }
1589            if (this.popup && forceOpen) {
1590                if (this._popupHtmlClearTimer) {
1591                    clearTimeout(this._popupHtmlClearTimer);
1592                    this._popupHtmlClearTimer = null;
1593                }
1594                this.popup = null;
1595                this.popupLocked = false;
1596                this.popupFullscreen = false;
1597                this.popupHtml = '';
1598            }
1599            slug = resolvePopupSlug(slug);
1600            const popupLocked = typeof locked === 'boolean' ? locked : false;
1601            const popupFullscreen =
1602                typeof fullscreen === 'boolean' ? fullscreen : popupLocked;
1603            nkTrack('Paywall', 'Popup Opened', slug);
1604
1605            this._fetchPopupHtml(slug, this._popupState(slug), trial).then(
1606                (html) => {
1607                    if (html && (forceOpen || !this.popup)) {
1608                        if (this._popupHtmlClearTimer) {
1609                            clearTimeout(this._popupHtmlClearTimer);
1610                            this._popupHtmlClearTimer = null;
1611                        }
1612                        this.popupHtml = html;
1613                        this.popup = slug;
1614                        this.popupLocked = popupLocked;
1615                        this.popupFullscreen = popupFullscreen;
1616                    }
1617                },
1618            );
1619        },
1620
1621        _bindModalCloseHandlers(root) {
1622            if (!root || root.dataset.nkPaywallCloseBound === '1') {
1623                return;
1624            }
1625
1626            root.dataset.nkPaywallCloseBound = '1';
1627
1628            const overlay = root.querySelector('.nk-paywall-overlay');
1629            const modal = root.querySelector('.nk-paywall-modal');
1630            const closeBtn = root.querySelector('#nk-paywall-modal__close');
1631
1632            if (modal) {
1633                modal.addEventListener('click', (event) => {
1634                    event.stopPropagation();
1635                });
1636            }
1637
1638            if (overlay) {
1639                overlay.addEventListener('click', (event) => {
1640                    if (event.target === overlay && !this.popupLocked) {
1641                        this.closePopup();
1642                    }
1643                });
1644            }
1645
1646            if (closeBtn) {
1647                closeBtn.addEventListener('click', () => {
1648                    if (!this.popupLocked) {
1649                        this.closePopup();
1650                    }
1651                });
1652            }
1653        },
1654
1655        closePopup() {
1656            if (this.popupLocked) {
1657                return;
1658            }
1659            this.popup = null;
1660            this.popupLocked = false;
1661            this.popupFullscreen = false;
1662            if (this._popupHtmlClearTimer) {
1663                clearTimeout(this._popupHtmlClearTimer);
1664                this._popupHtmlClearTimer = null;
1665            }
1666            // Clear content after leave transition (modal 0.3s + backdrop 0.2s) so slide-out is visible
1667            this._popupHtmlClearTimer = setTimeout(() => {
1668                this._popupHtmlClearTimer = null;
1669                if (!this.popup) {
1670                    this.popupHtml = '';
1671                }
1672            }, 500);
1673        },
1674    }));
1675
1676    /**
1677     * Multi-step signup form component (account → plan → payment).
1678     */
1679    Alpine.data('nkPaywallSignup', () => ({
1680        isLoggedIn:
1681            typeof nkPaywall !== 'undefined' && nkPaywall.loggedIn === '1',
1682        isTrial: false,
1683        isNknews:
1684            typeof nkPaywall !== 'undefined' && nkPaywall.site === 'nknews',
1685        step: 1,
1686        plans: [],
1687        plansLoading: false,
1688        signupGateway: 'stripe',
1689        selectedPlan: 0,
1690        signupNonce: '',
1691        stripe: null,
1692        cardElement: null,
1693        cardMounted: false,
1694        needsProfile: false,
1695        exUser: false,
1696        discount: 0,
1697        sp: false,
1698        couponError: { error: true, msg: '' },
1699        tossPayment: null,
1700        tossCustomerKey: '',
1701        supportEmail:
1702            typeof nkPaywall !== 'undefined' && nkPaywall.signupSupportEmail
1703                ? nkPaywall.signupSupportEmail
1704                : '[email protected]',
1705        successCheckIcon: (() => {
1706            const color =
1707                (typeof nkPaywall !== 'undefined' && nkPaywall.primaryColor) ||
1708                '#ce2e2c';
1709            const encoded = encodeURIComponent(color);
1710            return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='53' height='53' viewBox='0 0 53 53'%3E%3Cpath fill='${encoded}' d='M26.5 0C11.865 0 0 11.865 0 26.5S11.865 53 26.5 53 53 41.135 53 26.5 41.136 0 26.5 0zm-4.861 40.113L9.97 28.445l3.89-3.89 7.779 7.78L39.14 14.831l3.89 3.89L21.639 40.113z'/%3E%3C/svg%3E`;
1711        })(),
1712        details: {
1713            preferred_name: '',
1714            full_name: '',
1715            email: '',
1716            password: '',
1717            name_on_card: '',
1718            agreement: false,
1719            privacy: false,
1720            conf: false,
1721            profession:
1722                typeof nkPaywall !== 'undefined' && nkPaywall.profession
1723                    ? nkPaywall.profession
1724                    : '',
1725            country:
1726                typeof nkPaywall !== 'undefined' && nkPaywall.country
1727                    ? nkPaywall.country
1728                    : '',
1729            coupon_code: '',
1730            stripeToken: '',
1731            livemode: false,
1732            plan: 0,
1733        },
1734        confirmPassword: '',
1735        errors: [],
1736        paymentError: '',
1737        submitting: false,
1738        ajaxUrl: '/wp-admin/admin-ajax.php',
1739        ngoDocumentFile: null,
1740        ngoDocumentLabel: '',
1741        ngoDocumentOnFile:
1742            typeof nkPaywall !== 'undefined' && !!nkPaywall.ngoDocumentOnFile,
1743
1744        formatPrice(amount) {
1745            const n = Number(String(amount).replace(/,/g, ''));
1746            if (this.signupGateway === 'toss') {
1747                return Math.round(n).toLocaleString('en-US', {
1748                    maximumFractionDigits: 0,
1749                    minimumFractionDigits: 0,
1750                });
1751            }
1752            return (Math.round(n * 100) / 100).toFixed(2);
1753        },
1754
1755        init() {
1756            if (typeof nkPaywall !== 'undefined' && nkPaywall.ajaxUrl) {
1757                this.ajaxUrl = nkPaywall.ajaxUrl;
1758            }
1759
1760            this.isTrial = this.$el.dataset.trial === '1';
1761
1762            var tossState = window.__nkPaywallTossReturnState || null;
1763
1764            if (tossState) {
1765                if (tossState.type === 'fail') {
1766                    this.paymentError =
1767                        tossState.message ||
1768                        'Payment failed. Please try again.';
1769                    this.$nextTick(() => {
1770                        window.__nkPaywallTossReturnState = null;
1771                    });
1772                    return;
1773                }
1774
1775                if (tossState.type === 'success') {
1776                    this.step = 4;
1777                    if (!window.__nkPaywallTossFinalizing) {
1778                        window.__nkPaywallTossFinalizing = true;
1779                        var tossReturn =
1780                            typeof window !== 'undefined'
1781                                ? window.nkPaywallTossReturn
1782                                : null;
1783                        this.$nextTick(() => {
1784                            window.__nkPaywallTossReturnState = null;
1785                            this.finalizeTossReturn(tossState, tossReturn);
1786                        });
1787                    }
1788                    return;
1789                }
1790            }
1791
1792            if (this.isLoggedIn) {
1793                const userData =
1794                    typeof nkPaywall !== 'undefined' && nkPaywall.userData
1795                        ? nkPaywall.userData
1796                        : null;
1797                if (userData) {
1798                    this.details.email = userData.email || this.details.email;
1799                    this.details.preferred_name =
1800                        userData.preferred_name || this.details.preferred_name;
1801                    this.details.full_name =
1802                        userData.full_name || this.details.full_name;
1803                    this.details.profession =
1804                        userData.profession || this.details.profession;
1805                    this.details.country =
1806                        userData.country || this.details.country;
1807                }
1808                this.details.agreement = true;
1809                this.details.privacy = true;
1810
1811                if (!this.details.profession || !this.details.country) {
1812                    this.needsProfile = true;
1813                } else {
1814                    this.errors = false;
1815                    this.step = 2;
1816                    if (this.isTrial) {
1817                        this.loadPlans().then(() => this.autoSelectTrialPlan());
1818                    } else {
1819                        this.loadPlans();
1820                    }
1821                }
1822            }
1823        },
1824
1825        async finalizeTossReturn(tossState, tossReturn) {
1826            try {
1827                const result = await tossReturn.finalizeSignup(
1828                    tossState.customerKey,
1829                    tossState.authKey,
1830                );
1831
1832                if (!result.ok) {
1833                    this.step = 3;
1834                    this.paymentError =
1835                        result.message || 'Could not complete subscription.';
1836                    return;
1837                }
1838
1839                this.onPaymentSuccess({
1840                    redirect:
1841                        result.redirectUrl ||
1842                        tossReturn.redirectUrl ||
1843                        (typeof nkPaywall !== 'undefined' && nkPaywall.homeUrl
1844                            ? nkPaywall.homeUrl + 'profile'
1845                            : '/profile'),
1846                });
1847            } catch (_) {
1848                this.step = 3;
1849                this.paymentError = 'Could not complete subscription.';
1850            }
1851        },
1852
1853        ngoSignupNeedsDocument() {
1854            if (this.details.profession !== 'ngo') {
1855                return false;
1856            }
1857            // Trial signups check out at $1 today; don't gate them on NGO verification.
1858            if (this.isTrial) {
1859                return false;
1860            }
1861            // NGO notice/upload UI is hidden for nknews; don't require what the user has no way to provide.
1862            if (this.isNknews) {
1863                return false;
1864            }
1865            if (this.isLoggedIn && this.ngoDocumentOnFile) {
1866                return false;
1867            }
1868            return true;
1869        },
1870
1871        pickNgoDocument() {
1872            this.$refs.ngoDocumentInput?.click();
1873        },
1874
1875        onNgoDocumentSelected(event) {
1876            const input = event.target;
1877            const f = (input.files && input.files[0]) || null;
1878            input.value = '';
1879            this.ngoDocumentFile = f;
1880            this.ngoDocumentLabel = f ? f.name : '';
1881        },
1882
1883        completeSignupUsesNgoMultipart() {
1884            return (
1885                this.details.profession === 'ngo' &&
1886                this.ngoSignupNeedsDocument() &&
1887                !!this.ngoDocumentFile
1888            );
1889        },
1890
1891        postCompleteSignup(success) {
1892            const payload = JSON.parse(JSON.stringify(this.details));
1893            if (this.completeSignupUsesNgoMultipart()) {
1894                const fd = new FormData();
1895                fd.append('action', 'complete_signup');
1896                fd.append('data', JSON.stringify(payload));
1897                fd.append('ngo_document', this.ngoDocumentFile);
1898                jQuery.ajax({
1899                    url: this.ajaxUrl,
1900                    type: 'POST',
1901                    data: fd,
1902                    processData: false,
1903                    contentType: false,
1904                    dataType: 'json',
1905                    success,
1906                });
1907                return;
1908            }
1909            jQuery.ajax({
1910                url: this.ajaxUrl,
1911                type: 'post',
1912                dataType: 'json',
1913                data: {
1914                    action: 'complete_signup',
1915                    data: payload,
1916                },
1917                success,
1918            });
1919        },
1920
1921        get isToss() {
1922            const key =
1923                typeof nkPaywall !== 'undefined' ? nkPaywall.tossClientKey : '';
1924            return !!(key && this.details.country === 'South Korea');
1925        },
1926
1927        ensureTossCustomerKey() {
1928            if (!this.tossCustomerKey) {
1929                this.tossCustomerKey =
1930                    'nk_' +
1931                    Math.random().toString(36).slice(2) +
1932                    Date.now().toString(36);
1933            }
1934        },
1935
1936        loadTossSdk() {
1937            if (typeof window.TossPayments !== 'undefined') {
1938                return Promise.resolve();
1939            }
1940            return new Promise((resolve, reject) => {
1941                const s = document.createElement('script');
1942                s.src = 'https://js.tosspayments.com/v2/standard';
1943                s.async = true;
1944                s.onload = () => resolve();
1945                s.onerror = reject;
1946                document.head.appendChild(s);
1947            });
1948        },
1949
1950        ensureTossPayment() {
1951            const clientKey =
1952                typeof nkPaywall !== 'undefined' ? nkPaywall.tossClientKey : '';
1953            if (
1954                !clientKey ||
1955                !this.tossCustomerKey ||
1956                typeof window.TossPayments === 'undefined'
1957            ) {
1958                return;
1959            }
1960            const toss = window.TossPayments(clientKey);
1961            this.tossPayment = toss.payment({
1962                customerKey: this.tossCustomerKey,
1963            });
1964        },
1965
1966        unmountCard() {
1967            if (this.cardElement && this.cardMounted) {
1968                try {
1969                    this.cardElement.unmount();
1970                } catch (e) {
1971                    /* ignore */
1972                }
1973                this.cardMounted = false;
1974            }
1975        },
1976
1977        async loadPlans() {
1978            this.plansLoading = true;
1979            const formData = new FormData();
1980            formData.append('action', 'load_signup_plans');
1981            formData.append('nonce', nkPaywall.nonce);
1982            formData.append('is_trial', this.isTrial);
1983
1984            if (!this.isLoggedIn) {
1985                formData.append('email', this.details.email);
1986                formData.append(
1987                    'name',
1988                    this.details.preferred_name || this.details.full_name,
1989                );
1990            }
1991            if (!this.isLoggedIn || this.needsProfile) {
1992                formData.append('profession', this.details.profession);
1993                formData.append('country', this.details.country);
1994            }
1995
1996            return fetch(this.ajaxUrl, {
1997                method: 'POST',
1998                body: formData,
1999            })
2000                .then((r) => r.json())
2001                .then((res) => {
2002                    if (res.ex_user) {
2003                        this.exUser = true;
2004                        return;
2005                    }
2006                    this.exUser = false;
2007                    this.plans = res.plans || [];
2008                    this.signupGateway =
2009                        String(res.type || 'stripe').toLowerCase() === 'toss'
2010                            ? 'toss'
2011                            : 'stripe';
2012                    this.sp = !!res.sp;
2013                    this.signupNonce = res.signup_nonce;
2014                    return this.initStripe(res.published_key);
2015                })
2016                .finally(() => {
2017                    this.plansLoading = false;
2018                });
2019        },
2020
2021        async initStripe(key) {
2022            if (!key) return;
2023
2024            if (!document.querySelector('script[src*="js.stripe.com"]')) {
2025                await new Promise((resolve, reject) => {
2026                    const s = document.createElement('script');
2027                    s.src = 'https://js.stripe.com/v3/';
2028                    s.onload = resolve;
2029                    s.onerror = reject;
2030                    document.head.appendChild(s);
2031                });
2032            } else {
2033                await new Promise((resolve) => {
2034                    const poll = setInterval(() => {
2035                        if (typeof window.Stripe !== 'undefined') {
2036                            clearInterval(poll);
2037                            resolve();
2038                        }
2039                    }, 100);
2040                    setTimeout(() => {
2041                        clearInterval(poll);
2042                        resolve();
2043                    }, 10000);
2044                });
2045            }
2046
2047            this.stripe = window.Stripe(key);
2048
2049            const fontCss =
2050                typeof nkPaywall !== 'undefined' &&
2051                nkPaywall.signupStripeFontCss
2052                    ? nkPaywall.signupStripeFontCss
2053                    : '';
2054            const fonts = fontCss ? [{ cssSrc: fontCss }] : [];
2055            const accent =
2056                typeof nkPaywall !== 'undefined' && nkPaywall.primaryColor
2057                    ? nkPaywall.primaryColor
2058                    : '#ce2e2c';
2059            const elements = this.stripe.elements({ fonts });
2060            this.cardElement = elements.create('card', {
2061                style: {
2062                    base: {
2063                        fontSize: '14px',
2064                        fontFamily:
2065                            '"CircularStd", "Helvetica Neue", Helvetica, Arial, sans-serif',
2066                    },
2067                    invalid: {
2068                        color: accent,
2069                        iconColor: accent,
2070                        ':-webkit-autofill': { color: accent },
2071                    },
2072                },
2073            });
2074        },
2075
2076        mountCard() {
2077            if (this.isToss || !this.cardElement || this.cardMounted) return;
2078            const el = document.getElementById('nk-paywall-stripe-card');
2079            if (el) {
2080                this.cardElement.mount(el);
2081                this.cardMounted = true;
2082            }
2083        },
2084
2085        currentPlan() {
2086            return this.selectedPlan > 0
2087                ? this.plans[this.selectedPlan - 1]
2088                : null;
2089        },
2090
2091        isStudentStripePlan() {
2092            const p = this.currentPlan();
2093            return p && p.stripeplan && p.stripeplan.indexOf('student') > -1;
2094        },
2095
2096        choosePlan(index) {
2097            this.unmountCard();
2098            this.selectedPlan = index + 1;
2099            this.details.plan = this.currentPlan();
2100            this.resetCoupon();
2101            this.trackGoal(10);
2102            this.trackPlanGoal('select');
2103            this.step = 3;
2104            this.$nextTick(() => this.mountCard());
2105        },
2106
2107        // $1-trial CTA: the trial is yearly-only, so skip the "Choose your plan"
2108        // step entirely (it's hidden in the step nav whenever isTrial), lock in
2109        // the yearly plan, and land straight on Payment. Applies whether the
2110        // trial was started logged-in (profile complete) or logged-out (right
2111        // after Account Details) — both paths call this once plans have loaded.
2112        autoSelectTrialPlan() {
2113            if (!this.isTrial || this.exUser) {
2114                return;
2115            }
2116            const yearlyIndex = this.plans.findIndex(
2117                (p) => (p.interval || '').toLowerCase() === 'year',
2118            );
2119            if (yearlyIndex > -1) {
2120                this.choosePlan(yearlyIndex);
2121            }
2122        },
2123
2124        canGoTo(target) {
2125            if (target === this.step) return false;
2126            if (target === 1) return !this.isLoggedIn || this.needsProfile;
2127            if (target === 2) return this.errors === false;
2128            if (target === 3)
2129                return (
2130                    this.errors === false &&
2131                    this.selectedPlan > 0 &&
2132                    this.selectedPlan <= this.plans.length
2133                );
2134            return false;
2135        },
2136
2137        goTo(target) {
2138            if (target === this.step) return;
2139            if (target === 1 && this.isLoggedIn && !this.needsProfile) return;
2140            if (target === 2 && this.step === 1) {
2141                this.validateFields();
2142                return;
2143            }
2144            if (this.canGoTo(target)) {
2145                if (target !== 3 && this.step === 3) {
2146                    this.unmountCard();
2147                }
2148                this.step = target;
2149                if (target === 3) {
2150                    this.$nextTick(() => this.mountCard());
2151                }
2152            }
2153        },
2154
2155        resetCoupon() {
2156            this.couponError = { error: true, msg: '' };
2157            this.discount = 0;
2158            this.details.coupon_code = '';
2159        },
2160
2161        handleCoupon() {
2162            if (!this.details.coupon_code.length) {
2163                this.couponError.msg = '';
2164                this.discount = 0;
2165                return;
2166            }
2167            jQuery.ajax({
2168                url: this.ajaxUrl,
2169                type: 'post',
2170                dataType: 'json',
2171                data: {
2172                    action: 'validate_coupon',
2173                    coupon_code: this.details.coupon_code,
2174                    plan_id: this.currentPlan().plan_id,
2175                },
2176                success: (result) => {
2177                    this.couponError = result;
2178                    if (result.error) {
2179                        this.discount = 0;
2180                        return;
2181                    }
2182                    this.discount =
2183                        result.amount_type === 'percent'
2184                            ? (this.currentPlan().price * result.amount) / 100
2185                            : result.amount;
2186                },
2187            });
2188        },
2189
2190        hasError(field) {
2191            return (
2192                Array.isArray(this.errors) && this.errors.indexOf(field) > -1
2193            );
2194        },
2195
2196        pwRulePass(rule) {
2197            return (
2198                !Array.isArray(this.errors) || this.errors.indexOf(rule) === -1
2199            );
2200        },
2201
2202        validatePassword() {
2203            this.errors = [];
2204            const pw = this.details.password;
2205            if (pw.length < 8) this.errors.push('minimum');
2206            if (!/[A-Z]/.test(pw)) this.errors.push('upper');
2207            if (!/[a-z]/.test(pw)) this.errors.push('lower');
2208            if (!/[0-9]/.test(pw)) this.errors.push('number');
2209            if (!/[^A-Za-z0-9]/.test(pw)) this.errors.push('special');
2210        },
2211
2212        validateFields() {
2213            this.errors = [];
2214
2215            if (!this.details.preferred_name.length)
2216                this.errors.push('preferred_name');
2217
2218            if (!this.needsProfile) {
2219                if (
2220                    !this.details.email.length ||
2221                    !nkPaywallValidateEmail(this.details.email)
2222                )
2223                    this.errors.push('email');
2224
2225                const pw = this.details.password;
2226                if (pw.length < 8) this.errors.push('minimum');
2227                if (!/[A-Z]/.test(pw)) this.errors.push('upper');
2228                if (!/[a-z]/.test(pw)) this.errors.push('lower');
2229                if (!/[0-9]/.test(pw)) this.errors.push('number');
2230                if (!/[^A-Za-z0-9]/.test(pw)) this.errors.push('special');
2231
2232                if (pw !== this.confirmPassword) this.errors.push('cpassword');
2233
2234                if (!this.details.agreement) this.errors.push('agreement');
2235                if (!this.details.privacy) this.errors.push('privacy');
2236            }
2237
2238            if (!this.details.profession) this.errors.push('profession');
2239            if (!this.details.country) this.errors.push('country');
2240
2241            this.$nextTick(() => {
2242                if (this.errors.length) return;
2243
2244                this.loadPlans().then(() => {
2245                    if (!this.exUser) {
2246                        this.errors = false;
2247                        this.step = 2;
2248                        this.autoSelectTrialPlan();
2249                    }
2250                });
2251            });
2252        },
2253
2254        onSubmit() {
2255            if (this.step !== 3 || this.submitting) return;
2256            this.errors = [];
2257            if (this.ngoSignupNeedsDocument() && !this.ngoDocumentFile) {
2258                this.errors.push('ngo_document');
2259            }
2260            if (this.isToss) {
2261                if (this.isStudentStripePlan() && !this.details.conf) {
2262                    this.errors.push('conf');
2263                }
2264                if (this.errors.length) return;
2265                this.errors = false;
2266                this.submitting = true;
2267                this.trackPlanGoal('payment');
2268                this.submitToss();
2269                return;
2270            }
2271            if (!this.details.name_on_card.trim().length) {
2272                this.errors.push('name_on_card');
2273            }
2274            if (this.isStudentStripePlan() && !this.details.conf) {
2275                this.errors.push('conf');
2276            }
2277            if (this.errors.length) return;
2278            this.errors = false;
2279            this.submitting = true;
2280
2281            this.trackPlanGoal('payment');
2282
2283            this.stripe
2284                .createPaymentMethod('card', this.cardElement)
2285                .then((result) => {
2286                    if (result.error) {
2287                        this.submitting = false;
2288                        this.step = 3;
2289                        this.$nextTick(() => {
2290                            const el = this.$el.querySelector(
2291                                '#nk-paywall-card-errors',
2292                            );
2293                            if (el)
2294                                el.innerHTML =
2295                                    '<div>' + result.error.message + '</div>';
2296                        });
2297                    } else {
2298                        this.processPayment(result.paymentMethod);
2299                    }
2300                });
2301        },
2302
2303        async submitToss() {
2304            const errEl = this.$el.querySelector('#nk-paywall-card-errors');
2305            if (errEl) errEl.innerHTML = '';
2306            try {
2307                await this.loadTossSdk();
2308                this.ensureTossCustomerKey();
2309                this.ensureTossPayment();
2310                if (!this.tossPayment) {
2311                    throw new Error('Toss Payments could not be initialized.');
2312                }
2313                const plan = this.currentPlan();
2314                const plain = JSON.parse(JSON.stringify(this.details));
2315                plain.plan = plan;
2316                const customerData = {
2317                    email: plain.email,
2318                    password: plain.password,
2319                    preferred_name: plain.preferred_name,
2320                    full_name: plain.full_name,
2321                    stripeplan: plan.stripeplan,
2322                    coupon_code: plain.coupon_code || '',
2323                    profession: plain.profession,
2324                    country: plain.country,
2325                    agreement: plain.agreement,
2326                    privacy: plain.privacy,
2327                    conf: plain.conf,
2328                    signup_nonce: this.signupNonce,
2329                };
2330                sessionStorage.setItem(
2331                    'tossCustomerKey_' + this.tossCustomerKey,
2332                    JSON.stringify(customerData),
2333                );
2334                if (this.details.profession === 'ngo' && this.ngoDocumentFile) {
2335                    const prepFd = new FormData();
2336                    prepFd.append('action', 'prepare_toss_signup');
2337                    prepFd.append('customerKey', this.tossCustomerKey);
2338                    prepFd.append('customerData', JSON.stringify(customerData));
2339                    prepFd.append('ngo_document', this.ngoDocumentFile);
2340                    const prepRes = await fetch(this.ajaxUrl, {
2341                        method: 'POST',
2342                        body: prepFd,
2343                        credentials: 'same-origin',
2344                    });
2345                    const prepBody = await prepRes.json().catch(() => ({}));
2346                    if (!prepRes.ok || prepBody.success !== true) {
2347                        let msg = 'Could not prepare signup.';
2348                        const d = prepBody.data;
2349                        if (typeof d === 'string') {
2350                            msg = d;
2351                        } else if (d && typeof d.message === 'string') {
2352                            msg = d.message;
2353                        }
2354                        throw new Error(msg);
2355                    }
2356                } else {
2357                    try {
2358                        await fetch(
2359                            this.ajaxUrl + '?action=prepare_toss_signup',
2360                            {
2361                                method: 'POST',
2362                                headers: { 'Content-Type': 'application/json' },
2363                                credentials: 'same-origin',
2364                                body: JSON.stringify({
2365                                    customerKey: this.tossCustomerKey,
2366                                    customerData: customerData,
2367                                }),
2368                            },
2369                        );
2370                    } catch (_) {
2371                        /* Optional server backup; sessionStorage may suffice */
2372                    }
2373                }
2374                let successUrl =
2375                    typeof nkPaywall !== 'undefined' && nkPaywall.tossSuccessUrl
2376                        ? nkPaywall.tossSuccessUrl
2377                        : window.location.href;
2378                const home =
2379                    typeof nkPaywall !== 'undefined' && nkPaywall.homeUrl
2380                        ? nkPaywall.homeUrl
2381                        : window.location.origin + '/';
2382                const sep = home.indexOf('?') === -1 ? '?' : '&';
2383                const failUrl =
2384                    home +
2385                    sep +
2386                    'nk_paywall_toss=fail&customerKey=' +
2387                    encodeURIComponent(this.tossCustomerKey);
2388                try {
2389                    sessionStorage.setItem('nk_paywall_toss_pending', '1');
2390                } catch (_) {
2391                    /* ignore */
2392                }
2393                await this.tossPayment.requestBillingAuth({
2394                    method: 'CARD',
2395                    customerEmail: plain.email,
2396                    customerName: plain.full_name || plain.preferred_name,
2397                    successUrl: successUrl,
2398                    failUrl: failUrl,
2399                });
2400            } catch (e) {
2401                this.submitting = false;
2402                if (errEl) {
2403                    errEl.innerHTML =
2404                        '<div>' +
2405                        (e && e.message
2406                            ? e.message
2407                            : 'Payment request failed.') +
2408                        '</div>';
2409                }
2410            }
2411        },
2412
2413        processPayment(pm) {
2414            this.step = 4;
2415            this.details.stripeToken = pm.id;
2416            this.details.livemode = pm.livemode;
2417            this.details.signup_nonce = this.signupNonce;
2418            this.details.plan = this.currentPlan();
2419            this.details.is_trial = this.isTrial;
2420            if (this.couponError.error) this.details.coupon_code = '';
2421
2422            this.postCompleteSignup((result) => {
2423                if (result.error) {
2424                    this.submitting = false;
2425                    this.step = 3;
2426                    const el = this.$el.querySelector(
2427                        '#nk-paywall-card-errors',
2428                    );
2429                    if (el) el.innerHTML = '<div>' + result.msg + '</div>';
2430                } else if (result.requires_action) {
2431                    this.handle3DS(result);
2432                } else {
2433                    this.onPaymentSuccess(result);
2434                }
2435            });
2436        },
2437
2438        handle3DS(response) {
2439            this.details.customer_id = response.customer_id;
2440            this.details.payment_intent_id = response.payment_intent_id;
2441
2442            this.stripe
2443                .handleCardPayment(response.payment_intent_client_secret)
2444                .then((result) => {
2445                    if (result.error) {
2446                        this.submitting = false;
2447                        this.step = 3;
2448                        const el = this.$el.querySelector(
2449                            '#nk-paywall-card-errors',
2450                        );
2451                        if (el)
2452                            el.innerHTML =
2453                                '<div>' +
2454                                (result.error.message || '') +
2455                                '</div>';
2456                    } else {
2457                        this.postCompleteSignup((r) => {
2458                            if (r.error) {
2459                                this.submitting = false;
2460                                this.step = 3;
2461                                const el = this.$el.querySelector(
2462                                    '#nk-paywall-card-errors',
2463                                );
2464                                if (el)
2465                                    el.innerHTML = '<div>' + r.msg + '</div>';
2466                            } else {
2467                                this.onPaymentSuccess(r);
2468                            }
2469                        });
2470                    }
2471                });
2472        },
2473
2474        onPaymentSuccess(result) {
2475            if (this.details.livemode) {
2476                this.googleConversion(this.details.plan.plan_id);
2477                this.twitterConversion(this.details.plan.plan_id);
2478            }
2479            this.step = 5;
2480            this.trackPlanGoal('complete');
2481            if (result.redirect) {
2482                setTimeout(() => {
2483                    window.location.href = result.redirect;
2484                }, 3000);
2485            }
2486        },
2487
2488        trackPlanGoal(stage) {
2489            const p = this.currentPlan();
2490            if (!p) return;
2491            const map = {
2492                Annual: { select: 11, payment: 14, complete: 17 },
2493                Monthly: { select: 12, payment: 15, complete: 18 },
2494                Student: { select: 13, payment: 16, complete: 19 },
2495            };
2496            if (map[p.name] && map[p.name][stage])
2497                this.trackGoal(map[p.name][stage]);
2498        },
2499
2500        trackGoal(goalId) {
2501            if (
2502                typeof nkPaywall === 'undefined' ||
2503                (nkPaywall.site !== 'nknews' && nkPaywall.site !== 'koreapro')
2504            ) {
2505                return;
2506            }
2507            window._paq = window._paq || [];
2508            window._paq.push(['trackGoal', goalId]);
2509        },
2510
2511        googleConversion(planId) {
2512            const s = document.createElement('script');
2513            s.async = true;
2514            s.src = 'https://www.googletagmanager.com/gtag/js?id=AW-994398360';
2515            document.body.appendChild(s);
2516
2517            
vendor: 194 bytes, lines 2517-2522
2517window.dataLayer = window.dataLayer || [];
2518            function gtag() {
2519                dataLayer.push(arguments);
2520            }
2521            gtag('js', new Date());
2522            gtag('config', '
2522AW-994398360
vendor: 17 bytes, lines 2522-2524
2522');
2523
2524            
2524const conversions = {
2525                3: 'AW-994398360/2891CI_1-aIYEJihldoD',
2526                4: 'AW-994398360/2891CI_1-aIYEJihldoD',
2527                5: 'AW-994398360/zs4aCLm4jKMYEJihldoD',
2528            };
2529            if (conversions[planId]) {
2530                gtag('event', 'conversion', {
2531                    send_to: conversions[planId],
2532                    transaction_id: '',
2533                });
2534            }
2535        },
2536
2537        twitterConversion(planId) {
2538            !(function (e, t, n, s, u, a) {
2539                e.twq ||
2540                    ((s = e.twq =
2541                        function () {
2542                            s.exe
2543                                ? s.exe.apply(s, arguments)
2544                                : s.queue.push(arguments);
2545                        }),
2546                    (s.version = '1.1'),
2547                    (s.queue = []),
2548                    (u = t.createElement(n)),
2549                    (u.async = !0),
2550                    (u.src = 'https://static.ads-twitter.com/uwt.js'),
2551                    (a = t.getElementsByTagName(n)[0]),
2552                    a.parentNode.insertBefore(u, a));
2553            })(window, document, 'script');
2554            twq('init', 'nyi3w');
2555            twq('track', 'PageView');
2556
2557            const events = {
2558                3: 'tw-nyi3w-oecjf',
2559                4: 'tw-nyi3w-oecjg',
2560                5: 'tw-nyi3w-oecjh',
2561            };
2562            if (events[planId]) twq('event', events[planId], {});
2563        },
2564    }));
2565
2566    /**
2567     * Sign-in popup: client-side email/password checks (aligned with signup UX).
2568     */
2569    Alpine.data('nkPaywallSignin', () => ({
2570        email: '',
2571        password: '',
2572        authCode: '',
2573        mode: 'credentials',
2574        errors: [],
2575
2576        hasError(field) {
2577            return (
2578                Array.isArray(this.errors) && this.errors.indexOf(field) > -1
2579            );
2580        },
2581
2582        clearFieldError(field) {
2583            if (!Array.isArray(this.errors)) {
2584                return;
2585            }
2586            const i = this.errors.indexOf(field);
2587            if (i > -1) {
2588                this.errors.splice(i, 1);
2589            }
2590        },
2591
2592        validate() {
2593            this.errors = [];
2594            if (this.mode === 'auth_code') {
2595                if (!(this.authCode || '').trim().length) {
2596                    this.errors.push('authCode');
2597                }
2598            } else {
2599                const e = (this.email || '').trim();
2600                if (!e.length || !nkPaywallValidateEmail(e)) {
2601                    this.errors.push('email');
2602                }
2603                if (!(this.password || '').length) {
2604                    this.errors.push('password');
2605                }
2606            }
2607            return this.errors.length === 0;
2608        },
2609
2610        onSubmit(event) {
2611            const errEl = document.getElementById('nk-paywall-signin-error');
2612            if (errEl) {
2613                errEl.style.display = 'none';
2614                errEl.textContent = '';
2615            }
2616            if (!this.validate()) {
2617                event.preventDefault();
2618            }
2619        },
2620    }));
2621}
2622
2623document.addEventListener('alpine:init', registerNkPaywallAlpineComponents);
2624registerNkPaywallAlpineComponents();
2625
2626/**
2627 * WordPress [gallery] shortcode outputs static HTML; bxSlider adds .bx-wrapper.
2628 * processEmbeds() runs after AJAX injects article HTML; this covers
2629 * server-rendered full posts (always-visible, ungated config, etc.) on any theme.
2630 * Skips galleries already inside .bx-viewport.
2631 */
2632(function nkPaywallInitServerRenderedGalleries() {
2633    const selector =
2634        '.news-full-desc .gallery, #fullContent .gallery, .editor-content .gallery';
2635
2636    function run() {
2637        if (
2638            typeof jQuery === 'undefined' ||
2639            typeof jQuery.fn.bxSlider !== 'function' ||
2640            !document.body ||
2641            !document.body.classList.contains('single')
2642        ) {
2643            return false;
2644        }
2645        jQuery(selector).each(function () {
2646            const $g = jQuery(this);
2647            if ($g.closest('.bx-viewport').length) {
2648                return;
2649            }
2650            $g.bxSlider({
2651                slideSelector: '.gallery-item',
2652                mode: 'fade',
2653                speed: 0,
2654                keyboardEnabled: true,
2655                controls: true,
2656            });
2657        });
2658        return true;
2659    }
2660
2661    function schedule() {
2662        if (!document.body || !document.body.classList.contains('single')) {
2663            return;
2664        }
2665        if (run()) {
2666            return;
2667        }
2668        if (typeof jQuery === 'undefined') {
2669            return;
2670        }
2671        let attempts = 0;
2672        const max = 100;
2673        const id = window.setInterval(function () {
2674            attempts += 1;
2675            if (run()) {
2676                window.clearInterval(id);
2677            } else if (attempts >= max) {
2678                window.clearInterval(id);
2679            }
2680        }, 50);
2681    }
2682
2683    if (document.readyState === 'loading') {
2684        document.addEventListener('DOMContentLoaded', schedule);
2685    } else {
2686        schedule();
2687    }
2688})();

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.