PageSourceSearch

https://www.summitdaily.com/wp-content/plugins/swift-auth/public/js/swift-auth.bundle.js?ver=1780617830

js summitdaily.com collected 2026-10-02 03:23:16 UTC 304,705 bytes, 7,307 lines download raw bytes

1/**
2 * Swift Auth - Frontend Bundle
3 *
4 * Handles:
5 * - Authentication state management
6 * - Cognito integration (User Pool + Identity Pool)
7 * - Direct DynamoDB access for user data (profile, bookmarks, topics)
8 * - Regwall display
9 * - Paywall enforcement
10 * - UI state updates
11 */
12
13(function() {
14    'use strict';
15
16    // =========================================================================
17    // Configuration
18    // =========================================================================
19
20    const config = window.swiftAuthConfig || {};
21    const ajax = window.swiftAuthAjax || {};
22
23    // =========================================================================
24    // State
25    // =========================================================================
26
27    const state = {
28        authLevel: 'anonymous',      // resolved client-side (no longer in config)
29        sub: null,                    // resolved client-side (no longer in config)
30        sessionExpires: null,         // resolved client-side (no longer in config)
31        cognitoUser: null,
32        initialized: false,
33        localStorageMigrated: false, // Set to true once user data is migrated to DynamoDB
34        followedTopics: [], // Followed topics from DynamoDB
35        readingHistory: [], // Reading history from DynamoDB
36        profile: null, // User profile from DynamoDB (includes trackReadingHistory preference)
37        userDataManager: null, // UserDataManager instance for direct DynamoDB access
38        userDataManagerReady: false, // Flag to track if UserDataManager is initialized
39        forgotPasswordParams: null, // Stores params from forgot password link
40        loggedOut: false, // Set to true when logout() is called, prevents init() from re-elevating
41    };
42
43    // Content lock state for hard walls
44    let storedPremiumContent = null;
45
46    function syncLoggedInSignal() {
47        const isLoggedIn = (state.authLevel === 'full' || state.authLevel === 'lite' || state.authLevel === 'magic');
48        const value = isLoggedIn ? 'yes' : 'no';
49
50        window.swiftloggedIn = value;
51        document.cookie = 'swift_logged_in=' + value + '; path=/; max-age=' + (30 * 86400) + '; SameSite=Lax';
52        gtag('set', 'user_properties', { Page_LoggedIn: value });
53        console.log('[SwiftAuth] GA4 Page_LoggedIn set (sync):', value, '| authLevel:', state.authLevel);
54    }
55
56    // =========================================================================
57    // Utility Functions
58    // =========================================================================
59
60    /**
61     * Get current Cognito ID token (if available and valid)
62     * Returns null if no valid session
63     */
64    async function getIdToken() {
65        if (!window.swiftAuthUserPool) {
66            return null;
67        }
68
69        const cognitoUser = window.swiftAuthUserPool.getCurrentUser();
70        if (!cognitoUser) {
71            return null;
72        }
73
74        return new Promise((resolve) => {
75            cognitoUser.getSession((err, session) => {
76                if (err || !session || !session.isValid()) {
77                    resolve(null);
78                    return;
79                }
80                resolve(session.getIdToken().getJwtToken());
81            });
82        });
83    }
84
85    /**
86     * Make AJAX request to WordPress
87     * Automatically includes JWT token in:
88     * - Authorization header (preferred)
89     * - POST data or query params (fallback for servers that strip Auth header)
90     */
91    async function ajaxRequest(action, data = {}, method = 'POST', needsNonce = true) {
92        const headers = {};
93
94        // Lazily resolve a nonce only for actions that verify one server-side.
95        // Read-only calls (e.g. swift_auth_status) pass needsNonce=false so a
96        // fresh/anonymous visitor doesn't boot WordPress for a nonce on every
97        // page load — the nonce is fetched on first write action instead.
98        if (needsNonce) {
99            await fetchNonce();
100        }
101
102        // Get ID token
103        const idToken = await getIdToken();
104        if (idToken) {
105            headers['Authorization'] = `Bearer ${idToken}`;
106        }
107
108        let url = ajax.ajaxUrl;
109        let body = null;
110
111        if (method === 'POST') {
112            // POST: use FormData
113            const formData = new FormData();
114            formData.append('action', action);
115            formData.append('nonce', ajax.nonce);
116
117            for (const [key, value] of Object.entries(data)) {
118                formData.append(key, value);
119            }
120
121            // Add token to POST data as fallback
122            if (idToken) {
123                formData.append('id_token', idToken);
124            }
125
126            body = formData;
127        } else {
128            // GET: use query params
129            const params = new URLSearchParams({
130                action: action,
131                nonce: ajax.nonce,
132                ...data,
133            });
134
135            // Add token to query params as fallback
136            if (idToken) {
137                params.append('id_token', idToken);
138            }
139
140            url = `${ajax.ajaxUrl}?${params.toString()}`;
141        }
142
143        const options = {
144            method: method,
145            credentials: 'same-origin',
146            headers: headers,
147        };
148
149        if (body) {
150            options.body = body;
151        }
152
153        const response = await fetch(url, options);
154        return response.json();
155    }
156
157    // =========================================================================
158    // Cache-Safe Async Fetchers
159    // =========================================================================
160
161    /**
162     * Fetch a fresh WordPress nonce from the server.
163     * Nonces are no longer embedded in HTML (breaks CloudFront caching).
164     * Caches in sessionStorage for 6 hours.
165     */
166    let nonceInFlight = null;
167    async function fetchNonce() {
168        const CACHE_KEY = 'swift_nonce';
169        const CACHE_TTL = 2 * 60 * 60 * 1000; // 2 hours
170
171        // Already resolved earlier this page load — reuse it.
172        if (ajax.nonce) return;
173
174        // Warm from sessionStorage (persists across page loads, 2h TTL).
175        try {
176            const cached = sessionStorage.getItem(CACHE_KEY);
177            if (cached) {
178                const parsed = JSON.parse(cached);
179                if (parsed.ts && (Date.now() - parsed.ts < CACHE_TTL) && parsed.nonce) {
180                    ajax.nonce = parsed.nonce;
181                    return;
182                }
183            }
184        } catch (e) { /* ignore parse errors */ }
185
186        // Dedupe concurrent callers so we only ever fire one network request.
187        if (nonceInFlight) return nonceInFlight;
188
189        nonceInFlight = (async () => {
190            try {
191                const params = new URLSearchParams({ action: 'swift_get_nonce' });
192                const resp = await fetch(`${ajax.ajaxUrl}?${params.toString()}`, {
193                    method: 'GET',
194                    credentials: 'same-origin',
195                });
196                const data = await resp.json();
197                if (data.success && data.data?.nonce) {
198                    ajax.nonce = data.data.nonce;
199                    try {
200                        sessionStorage.setItem(CACHE_KEY, JSON.stringify({ nonce: data.data.nonce, ts: Date.now() }));
201                    } catch (e) { /* storage full */ }
202                }
203            } catch (err) {
204                console.warn('SwiftAuth: Failed to fetch nonce', err);
205            } finally {
206                nonceInFlight = null;
207            }
208        })();
209        return nonceInFlight;
210    }
211
212    /**
213     * Fetch wall config JSON from the static CDN file.
214     * Populates config.paywall.walls, config.regwall (walls), and config.whyRegisterHtml.
215     * Caches in sessionStorage for 5 minutes.
216     */
217    async function fetchWallConfig() {
218        const url = config.wallsJsonUrl;
219        if (!url) {
220            console.log('SwiftAuth: No wallsJsonUrl configured, skipping wall config fetch');
221            return;
222        }
223
224        const CACHE_KEY = 'swift_wall_config';
225        const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
226
227        let wallData = null;
228
229        try {
230            const cached = sessionStorage.getItem(CACHE_KEY);
231            if (cached) {
232                const parsed = JSON.parse(cached);
233                if (parsed.ts && (Date.now() - parsed.ts < CACHE_TTL) && parsed.data) {
234                    wallData = parsed.data;
235                }
236            }
237        } catch (e) { /* ignore parse errors */ }
238
239        if (!wallData) {
240            try {
241                const resp = await fetch(url, { credentials: 'same-origin' });
242                if (resp.ok) {
243                    wallData = await resp.json();
244                    try {
245                        sessionStorage.setItem(CACHE_KEY, JSON.stringify({ data: wallData, ts: Date.now() }));
246                    } catch (e) { /* storage full */ }
247                } else {
248                    console.warn('SwiftAuth: Wall config fetch failed:', resp.status);
249                }
250            } catch (err) {
251                console.warn('SwiftAuth: Failed to fetch wall config', err);
252            }
253        }
254
255        if (wallData) {
256            console.log('SwiftAuth: wallData fetched, paywall keys:', Object.keys(wallData.paywall || {}), 'walls count:', wallData.paywall?.walls?.length, 'config.paywall exists:', !!config.paywall);
257            // Populate paywall walls into config
258            if (wallData.paywall?.walls && config.paywall) {
259                config.paywall.walls = wallData.paywall.walls;
260                console.log('SwiftAuth: Assigned paywall walls:', config.paywall.walls.length, 'verify:', config.paywall.walls === wallData.paywall.walls);
261            } else {
262                console.warn('SwiftAuth: SKIPPED paywall walls assignment - wallData.paywall?.walls:', !!wallData.paywall?.walls, 'config.paywall:', !!config.paywall);
263            }
264            // Populate regwall walls into config (for future client-side use)
265            if (wallData.regwall?.walls && config.regwall) {
266                config.regwall.walls = wallData.regwall.walls;
267            }
268            // Populate whyRegisterHtml
269            if (wallData.whyRegisterHtml !== undefined) {
270                config.whyRegisterHtml = wallData.whyRegisterHtml;
271            }
272            // Populate subscriptionRequiredHtml
273            if (wallData.subscriptionRequiredHtml !== undefined) {
274                config.subscriptionRequiredHtml = wallData.subscriptionRequiredHtml;
275            }
276        } else {
277            console.warn('SwiftAuth: wallData is null/undefined after fetch');
278        }
279    }
280
281    /**
282     * Get current article URL
283     */
284    function getArticleUrl() {
285        return window.location.href.split('?')[0].split('#')[0];
286    }
287
288    /**
289     * Get current article post ID from shortlink or swiftArticleMeta
290     * Returns null if not on an article page
291     */
292    function getPostId() {
293        // First try swiftArticleMeta (most reliable if available)
294        if (window.swiftArticleMeta?.postId) {
295            return parseInt(window.swiftArticleMeta.postId, 10);
296        }
297
298        // Fallback: parse from shortlink element
299        // <link rel='shortlink' href='https://example.com/?p=12345' />
300        const shortlink = document.querySelector('link[rel="shortlink"]');
301        if (shortlink && shortlink.href) {
302            const match = shortlink.href.match(/[?&]p=(\d+)/);
303            if (match) {
304                return parseInt(match[1], 10);
305            }
306        }
307
308        return null;
309    }
310
311    /**
312     * Get article metadata from page
313     */
314    function getArticleMeta() {
315        const defaults = {
316            url: getArticleUrl(),
317            title: document.title,
318            image: document.querySelector('meta[property="og:image"]')?.content || '',
319        };
320        return window.swiftArticleMeta
321            ? { ...defaults, ...window.swiftArticleMeta }
322            : defaults;
323    }
324
325    // =========================================================================
326    // Sync Cache Utilities (localStorage-based, no async)
327    // =========================================================================
328
329    /**
330     * Decode Cognito ID token from localStorage (sync, no network)
331     * Returns { sub, email } or null if no valid token
332     */
333    function getCachedIdTokenPayload() {
334        try {
335            const clientId = config.cognito?.clientId;
336            if (!clientId) return null;
337
338            const lastUserKey = `CognitoIdentityServiceProvider.${clientId}.LastAuthUser`;
339            const username = localStorage.getItem(lastUserKey);
340            if (!username) return null;
341
342            const tokenKey = `CognitoIdentityServiceProvider.${clientId}.${username}.idToken`;
343            const idToken = localStorage.getItem(tokenKey);
344            if (!idToken) return null;
345
346            const parts = idToken.split('.');
347            if (parts.length !== 3) return null;
348
349            const payload = JSON.parse(atob(parts[1]));
350
351            // Check expiry
352            if (payload.exp && payload.exp * 1000 < Date.now()) {
353                return null;
354            }
355
356            return { sub: payload.sub || null, email: payload.email || null };
357        } catch (e) {
358            return null;
359        }
360    }
361
362    /**
363     * Read cached subscription from localStorage (sync, no network)
364     */
365    function getCachedSubscription() {
366        try {
367            const pubID = config.pubID || '';
368            const data = localStorage.getItem(pubID + 'Subscription');
369            return data ? JSON.parse(data) : null;
370        } catch (e) {
371            return null;
372        }
373    }
374
375    /**
376     * Pure function to evaluate entitlement from a subscription object
377     * Returns { hasActive, tier, endDate, isExpired, autoRenew }
378     */
379    function evaluateEntitlement(entitlement) {
380        if (!entitlement) {
381            return { hasActive: false, tier: 'free', endDate: null, isExpired: false, autoRenew: false };
382        }
383
384        const status = entitlement.status;
385        const tier = entitlement.tier || entitlement.type || 'free';
386
387        // Comp tiers (employee/complimentary) are perpetual — grant regardless of status/endDate.
388        if (['employee', 'gratis'].includes(tier)) {
389            const compEnd = entitlement.endDate || entitlement.expires_at || entitlement.expiresAt || entitlement.expires || null;
390            return { hasActive: true, tier, endDate: compEnd, isExpired: false, autoRenew: entitlement.autoRenew || entitlement.auto_renew || false };
391        }
392
393        const hasActiveTier = (status === 'active' || ['subscriber', 'employee', 'gratis', 'trial'].includes(tier));
394
395        const endDate = entitlement.endDate || entitlement.expires_at || entitlement.expiresAt || entitlement.expires;
396        let isExpired = false;
397        if (endDate && hasActiveTier) {
398            const expiry = new Date(typeof endDate === 'number' ? endDate * 1000 : endDate);
399            isExpired = expiry < new Date();
400        }
401
402        const hasActive = hasActiveTier && !isExpired;
403        const autoRenew = entitlement.autoRenew || entitlement.auto_renew || false;
404
405        return { hasActive, tier, endDate, isExpired, autoRenew };
406    }
407
408    // =========================================================================
409    // Content Lock (for hard walls)
410    // =========================================================================
411
412    /**
413     * Show the dark overlay behind modals
414     */
415    function showOverlay() {
416        let overlay = document.querySelector('.swift-wall-overlay');
417        if (!overlay) {
418            overlay = document.createElement('div');
419            overlay.className = 'swift-wall-overlay';
420            document.body.appendChild(overlay);
421        }
422        overlay.style.display = 'block';
423    }
424
425    /**
426     * Hide the dark overlay
427     */
428    function hideOverlay() {
429        const overlay = document.querySelector('.swift-wall-overlay');
430        if (overlay) {
431            overlay.style.display = 'none';
432        }
433    }
434
435    /**
436     * Remove content from .p402_premium elements
437     */
438    function lockContent() {
439        const article = document.querySelector('.p402_premium');
440        if (!article) return;
441
442        // Store original content
443        storedPremiumContent = article.innerHTML;
444
445        // Clear the content
446        article.innerHTML = '';
447
448        // Add locked class for styling
449        article.classList.add('swift-content-locked');
450
451        // Show overlay
452        showOverlay();
453    }
454
455    /**
456     * Defer fn() until the user scrolls past firstAd (exits viewport from top).
457     * Falls back to fn() immediately if no ad, no IntersectionObserver support,
458     * or the ad is already above the viewport on load.
459     *
460     * @param {HTMLElement|null} firstAd
461     * @param {Function} fn
462     */
463    function waitForScrollPastFirstAd(firstAd, fn) {
464        if (!firstAd || typeof IntersectionObserver === 'undefined') {
465            fn();
466            return;
467        }
468        const observer = new IntersectionObserver(function(entries) {
469            entries.forEach(function(entry) {
470                if (!entry.isIntersecting && entry.boundingClientRect.top < 0) {
471                    observer.disconnect();
472                    fn();
473                }
474            });
475        }, { threshold: 0 });
476        observer.observe(firstAd);
477    }
478
479    /**
480     * Insert a wall element after the first .module-ad inside the article,
481     * then hide all remaining siblings. If no .module-ad exists, inserts
482     * at the start of the article and hides everything after.
483     *
484     * @param {HTMLElement} article - The .p402_premium container
485     * @param {HTMLElement} wallElement - The wall div to insert
486     * @returns {Function} restore function that unhides content (for soft/dismissable walls)
487     */
488    function insertWallAfterFirstAd(article, wallElement, useBlur) {
489        const firstAd = article.querySelector('.module-ad');
490        const hiddenElements = [];
491        const blurredElements = [];
492
493        if (firstAd) {
494            // Insert wall right after the first ad
495            firstAd.after(wallElement);
496        } else {
497            // No .module-ad found — fall back to inserting before .p402_content or at start
498            const contentDiv = article.querySelector('.p402_content');
499            const insertBefore = contentDiv || article.firstChild;
500            article.insertBefore(wallElement, insertBefore);
501        }
502
503        // Process all remaining content after the wall.
504        // The wall is inserted after .module-ad, which may be nested inside wrappers
505        // (e.g. div.row > div.col-sm-12 > .module-ad). We need to:
506        // 1. Process siblings after the wall at its insertion level
507        // 2. Walk up each ancestor to the article, processing siblings after each ancestor
508        let current = wallElement;
509        while (current && current !== article) {
510            let sibling = current.nextElementSibling;
511            while (sibling) {
512                // Never blur/hide ad units
513                const isAd = sibling.classList.contains('module-ad') || sibling.querySelector('.module-ad');
514                if (!isAd) {
515                    if (useBlur) {
516                        sibling.classList.add('swift-wall-blur-content');
517                        blurredElements.push(sibling);
518                    } else {
519                        sibling.classList.add('swift-wall-hidden');
520                        hiddenElements.push(sibling);
521                    }
522                }
523                sibling = sibling.nextElementSibling;
524            }
525            current = current.parentElement;
526        }
527
528        // Return a restore function for dismissable walls
529        return function restoreContent() {
530            hiddenElements.forEach(el => el.classList.remove('swift-wall-hidden'));
531            blurredElements.forEach(el => el.classList.remove('swift-wall-blur-content'));
532        };
533    }
534
535    /**
536     * Generate lorem ipsum HTML paragraphs approximating a target character length
537     */
538    function generateLipsumHtml(targetLength) {
539        const sentences = [
540            'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
541            'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
542            'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
543            'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore.',
544            'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia.',
545            'Nulla facilisi morbi tempus iaculis urna id volutpat lacus.',
546            'Amet consectetur adipiscing elit pellentesque habitant morbi tristique senectus.',
547            'Viverra accumsan in nisl nisi scelerisque eu ultrices vitae auctor.',
548            'Turpis egestas maecenas pharetra convallis posuere morbi leo urna.',
549            'Pellentesque elit eget gravida cum sociis natoque penatibus et magnis.',
550            'Faucibus pulvinar elementum integer enim neque volutpat ac tincidunt vitae.',
551            'Adipiscing commodo elit at imperdiet dui accumsan sit amet nulla.',
552        ];
553
554        let html = '';
555        let charCount = 0;
556        // Cap at a reasonable length to avoid enormous DOM
557        const maxLength = Math.min(targetLength || 2000, 5000);
558
559        while (charCount < maxLength) {
560            let para = '';
561            // 3-6 sentences per paragraph
562            const sentenceCount = 3 + Math.floor(Math.random() * 4);
563            for (let i = 0; i < sentenceCount && charCount < maxLength; i++) {
564                const sentence = sentences[Math.floor(Math.random() * sentences.length)];
565                para += sentence + ' ';
566                charCount += sentence.length + 1;
567            }
568            html += '<p>' + para.trim() + '</p>';
569        }
570
571        return html;
572    }
573
574    /**
575     * Restore content
576     */
577    function unlockContent() {
578        const article = document.querySelector('.p402_premium');
579
580        // Restore content if we have it stored
581        if (article && storedPremiumContent !== null) {
582            article.innerHTML = storedPremiumContent;
583            storedPremiumContent = null;
584        }
585
586        // Remove locked/hidden classes
587        if (article) {
588            article.classList.remove('swift-content-locked');
589            article.classList.remove('swift-content-blurred');
590            article.classList.remove('swift-content-hidden');
591        }
592
593        // Hide overlay
594        hideOverlay();
595    }
596
597    // =========================================================================
598    // Cognito Integration
599    // =========================================================================
600
601    /**
602     * Initialize Cognito (requires AWS Amplify or Cognito JS SDK)
603     */
604    function initCognito() {
605        if (!config.cognito || !config.cognito.userPoolId) {
606            console.log('SwiftAuth: Cognito not configured');
607            return;
608        }
609
610        // Check if AWS SDK is available
611        if (typeof AWS !== 'undefined' && typeof AmazonCognitoIdentity !== 'undefined') {
612            const poolData = {
613                UserPoolId: config.cognito.userPoolId,
614                ClientId: config.cognito.clientId,
615            };
616
617            window.swiftAuthUserPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
618        }
619    }
620
621    /**
622     * Initialize UserDataManager for direct DynamoDB access
623     * Requires valid Cognito ID token to get Identity Pool credentials
624     *
625     * @param {string}
625 idToken - JWT ID token from Cognito User Pool
626     * @returns {Promise<boolean>} - True if initialized successfully
627     */
628    async function initUserDataManager(idToken) {
629        console.log('SwiftAuth: initUserDataManager config:', {
630            identityPoolId: config.identityPoolId ? config.identityPoolId.substring(0, 20) + '...' : 'MISSING',
631            userPoolId: config.cognito?.userPoolId,
632            region: config.dynamodb?.region || config.cognito?.region || 'us-west-2',
633            tableName: config.dynamodb?.tableName,
634            tokenLength: idToken ? idToken.length : 0
635        });
636
637        if (!config.identityPoolId) {
638            console.error('SwiftAuth: Identity Pool ID not configured - user data operations will fail');
639            return false;
640        }
641
642        if (typeof SwiftUserDataManager === 'undefined') {
643            console.error('SwiftAuth: UserDataManager not loaded - user data operations will fail');
644            return false;
645        }
646
647        try {
648            state.userDataManager = new SwiftUserDataManager({
649                pubID: config.pubID,
650                tableName: config.dynamodb?.tableName || 'SwiftUserProfiles-prod',
651                region: config.dynamodb?.region || config.cognito?.region || 'us-west-2',
652                identityPoolId: config.identityPoolId,
653                userPoolId: config.cognito?.userPoolId,
654                debug: true // Enable debug logging
655            });
656
657            const success = await state.userDataManager.initialize(idToken);
658
659            if (success) {
660                state.userDataManagerReady = true;
661                console.log('SwiftAuth: UserDataManager initialized for direct DynamoDB access');
662                return true;
663            } else {
664                const initErr = state.userDataManager.initError;
665                console.error('SwiftAuth: UserDataManager initialization failed - user data operations will fail');
666                if (initErr) {
667                    console.error('SwiftAuth: Root cause:', initErr.code || initErr.name, '-', initErr.message);
668                }
669                return false;
670            }
671        } catch (error) {
672            console.error('SwiftAuth: Error initializing UserDataManager:', error);
673            return false;
674        }
675    }
676
677    /**
678     * Check if UserDataManager is ready for direct DynamoDB calls
679     */
680    function isDirectDynamoEnabled() {
681        return state.userDataManagerReady && state.userDataManager && state.userDataManager.isReady();
682    }
683
684    /**
685     * Get diagnostic info about why DynamoDB might not be enabled
686     */
687    function getDynamoDBDiagnostics() {
688        // Check if Cognito user exists
689        const cognitoUser = window.swiftAuthUserPool?.getCurrentUser?.();
690
691        const diagnostics = {
692            identityPoolIdConfigured: !!config.identityPoolId,
693            awsSdkLoaded: typeof AWS !== 'undefined',
694            cognitoSdkLoaded: typeof AmazonCognitoIdentity !== 'undefined',
695            userDataManagerClassLoaded: typeof SwiftUserDataManager !== 'undefined',
696            userDataManagerCreated: !!state.userDataManager,
697            userDataManagerReady: state.userDataManagerReady,
698            isReady: state.userDataManager?.isReady?.() || false,
699            authLevel: state.authLevel,
700            cognitoUserExists: !!cognitoUser,
701            userPoolInitialized: !!window.swiftAuthUserPool,
702        };
703
704        // Determine the likely cause
705        if (!diagnostics.identityPoolIdConfigured) {
706            diagnostics.likelyCause = 'Identity Pool ID not configured in WordPress admin settings';
707        } else if (!diagnostics.awsSdkLoaded) {
708            diagnostics.likelyCause = 'AWS SDK failed to load';
709        } else if (!diagnostics.userDataManagerClassLoaded) {
710            diagnostics.likelyCause = 'UserDataManager script failed to load';
711        } else if (!diagnostics.userPoolInitialized) {
712            diagnostics.likelyCause = 'Cognito User Pool not initialized - check userPoolId and clientId settings';
713        } else if (!diagnostics.cognitoUserExists) {
714            diagnostics.likelyCause = 'No Cognito user in localStorage - user needs to log in';
715        } else if (!diagnostics.userDataManagerCreated) {
716            diagnostics.likelyCause = 'UserDataManager not created - initUserDataManagerFromCognitoSession may not have run';
717        } else if (!diagnostics.userDataManagerReady) {
718            diagnostics.likelyCause = 'UserDataManager initialization failed - check console for AWS credential errors';
719        } else {
720            diagnostics.likelyCause = 'Unknown - DynamoDB should be working';
721        }
722
723        return diagnostics;
724    }
725
726    /**
727     * Update Pinpoint endpoint with cognito_sub after login
728     * This enables magic link token generation for email campaigns
729     *
730     * @param {string} sub - Cognito user sub
731     * @param {string} email - User's email address
732     */
733    async function updatePinpointEndpoint(sub, email) {
734        console.log('SwiftAuth Debug: updatePinpointEndpoint called', { sub, email });
735
736        // Check if Pinpoint integration is enabled and App ID is configured
737        const pinpointAppId = config.pinpoint?.appId;
738        console.log('SwiftAuth Debug: Pinpoint config', {
739            enabled: config.pinpoint?.enabled,
740            appId: pinpointAppId,
741            region: config.pinpoint?.region
742        });
743
744        if (!config.pinpoint?.enabled || !pinpointAppId || typeof pinpointAppId !== 'string' || pinpointAppId.trim() === '') {
745            console.log('SwiftAuth Debug: Pinpoint not configured or App ID missing, skipping endpoint update');
746            return;
747        }
748
749        // Check if AWS SDK is available
750        if (typeof AWS === 'undefined') {
751            console.log('SwiftAuth Debug: AWS SDK not loaded, skipping Pinpoint endpoint update');
752            return;
753        }
754
755        try {
756            // Check if Pinpoint is available in the AWS SDK bundle
757            if (typeof AWS.Pinpoint !== 'function') {
758                console.log('SwiftAuth Debug: AWS.Pinpoint not available in SDK bundle, skipping endpoint update');
759                return;
760            }
761
762            console.log('SwiftAuth Debug: Initializing AWS.Pinpoint client');
763            // Use the same credentials from the Identity Pool that UserDataManager uses
764            // The credentials should already be initialized from initUserDataManager
765            const pinpoint = new AWS.Pinpoint({
766                region: config.pinpoint.region || config.cognito?.region || 'us-west-2',
767            });
768
769            // Use email as endpoint ID, matching the legacy/external system format:
770            // 1. Normalize email (lowercase and trim)
771            // 2. Base64 encode the normalized email
772            // 3. Lowercase the resulting Base64 string
773            const normalizedEmail = email.toLowerCase().trim();
774            const endpointId = btoa(normalizedEmail).toLowerCase();
775
776            console.log('SwiftAuth Debug: Generated Endpoint ID', {
777                originalEmail: email,
778                normalizedEmail: normalizedEmail,
779                endpointId: endpointId
780            });
781
782            const params = {
783                ApplicationId: config.pinpoint.appId,
784                EndpointId: endpointId,
785                EndpointRequest: {
786                    Address: email,
787                    ChannelType: 'EMAIL',
788                    Attributes: {
789                        cognito_sub: [sub],
790                    },
791                    User: {
792                        UserId: sub,
793                    },
794                },
795            };
796
797            console.log('SwiftAuth Debug: Sending Pinpoint updateEndpoint request', params);
798
799            await pinpoint.updateEndpoint(params).promise();
800            console.log('SwiftAuth Debug: Pinpoint endpoint updated successfully with cognito_sub');
801        } catch (err) {
802            // Don't fail login if Pinpoint update fails - just log it
803            console.error('SwiftAuth Debug: Failed to update Pinpoint endpoint:', err);
804            console.error('SwiftAuth Debug: Error details:', {
805                message: err.message,
806                code: err.code,
807                statusCode: err.statusCode,
808                requestId: err.requestId
809            });
810        }
811    }
812
813    /**
814     * Sync user profile attributes from Cognito JWT to DynamoDB
815     * Called after login to ensure profile data (name, email, etc.) is available
816     *
817     * @param {string} idToken - The Cognito ID token (JWT)
818     */
819    async function syncUserProfileFromToken(idToken) {
820        if (!state.userDataManager) {
821            console.log('SwiftAuth: UserDataManager not ready, skipping profile sync');
822            return;
823        }
824
825        try {
826            // Parse JWT token to extract user attributes
827            const tokenParts = idToken.split('.');
828            if (tokenParts.length !== 3) {
829                console.log('SwiftAuth: Invalid token format for profile sync');
830                return;
831            }
832
833            const payload = JSON.parse(atob(tokenParts[1]));
834
835            // Extract relevant user attributes from Cognito token
836            const updates = {};
837
838            if (payload.email) {
839                updates.email = payload.email;
840            }
841            if (payload.given_name) {
842                updates.givenName = payload.given_name;
843            }
844            if (payload.family_name) {
845                updates.familyName = payload.family_name;
846            }
847            if (payload.name) {
848                updates.name = payload.name;
849            }
850            if (payload.picture) {
851                updates.picture = payload.picture;
852            }
853            if (payload.birthdate) {
854                updates.birthdate = payload.birthdate;
855            }
856            if (payload.gender) {
857                updates.gender = payload.gender;
858            }
859
860            // Always include identity metadata — for new users this is the first
861            // write that creates the profile item in DynamoDB.
862            if (payload.sub) {
863                updates.sub = payload.sub;
864            }
865            if (state.userDataManager.userPoolSub) {
866                updates.identityId = state.userDataManager.userPoolSub;
867            }
868            updates.entityType = 'PROFILE';
869
870            // Only update if we have attributes to sync
871            if (Object.keys(updates).length === 0) {
872                console.log('SwiftAuth: No user attributes to sync from token');
873                return;
874            }
875
876            console.log('SwiftAuth: Syncing user attributes to DynamoDB:', Object.keys(updates));
877            const existing = await state.userDataManager.getProfile();
878            if (existing) {
879                await state.userDataManager.updateProfile(updates);
880            } else {
881                await state.userDataManager.saveProfile(updates);
882            }
883            console.log('SwiftAuth: User profile synced successfully');
884
885        } catch (err) {
886            // Don't fail login if profile sync fails
887            console.error('SwiftAuth: Failed to sync user profile:', err.message);
888        }
889    }
890
891    /**
892     * Initialize UserDataManager from existing Cognito session
893     * Called on page load to ensure DynamoDB access is available for logged-in users
894     */
895    async function initUserDataManagerFromCognitoSession() {
896        if (!window.swiftAuthUserPool) {
897            console.log('SwiftAuth: User pool not initialized, skipping UserDataManager init');
898            return;
899        }
900
901        const cognitoUser = window.swiftAuthUserPool.getCurrentUser();
902        if (!cognitoUser) {
903            console.log('SwiftAuth: No Cognito user, skipping UserDataManager init');
904            return;
905        }
906
907        return new Promise((resolve) => {
908            cognitoUser.getSession(async (err, session) => {
909                if (err || !session || !session.isValid()) {
910                    console.log('SwiftAuth: No valid Cognito session for UserDataManager');
911                    resolve();
912                    return;
913                }
914
915                const idToken = session.getIdToken().getJwtToken();
916                console.log('SwiftAuth: Initializing UserDataManager from Cognito session');
917
918                // Initialize UserDataManager for direct DynamoDB access
919                const success = await initUserDataManager(idToken);
920
921                if (success) {
922                    console.log('SwiftAuth: UserDataManager ready for DynamoDB operations');
923                    // Conditional hydration: profile page needs everything, other pages only need subscription
924                    const isProfilePage = !!document.getElementById('swift-profile-root');
925                    if (isProfilePage) {
926                        await state.userDataManager.hydrate();
927                    } else {
928                        await state.userDataManager.hydrateSubscriptionOnly();
929                    }
930
931                    // Repair missing profile attributes (GSI1 keys, email, sub, identityId).
932                    // Profiles created before the UserDataService fix may be missing these.
933                    try {
934                        let profile = state.userDataManager.cache.profile;
935                        if (!profile) {
936                            profile = await state.userDataManager.getProfile();
937                        }
938                        if (profile) {
939                            const needsRepair = !profile.GSI1PK || !profile.email || !profile.sub || !profile.identityId || !profile.entityType;
940                            if (needsRepair) {
941                                console.log('SwiftAuth: Repairing profile — missing:', [
942                                    !profile.GSI1PK && 'GSI1PK',
943                                    !profile.email && 'email',
944                                    !profile.sub && 'sub',
945                                    !profile.identityId && 'identityId',
946                                    !profile.entityType && 'entityType'
947                                ].filter(Boolean).join(', '));
948                                // Parse email from the Cognito session JWT
949                                const repairUpdates = {
950                                    sub: profile.sub || state.sub,
951                                    identityId: state.userDataManager.userPoolSub,
952                                    entityType: 'PROFILE'
953                                };
954                                // Get email from the current session token
955                                try {
956                                    const tokenPayload = JSON.parse(atob(idToken.split('.')[1]));
957                                    if (tokenPayload.email) repairUpdates.email = tokenPayload.email;
958                                    if (tokenPayload.sub) repairUpdates.sub = tokenPayload.sub;
959                                } catch (e) { /* ignore parse errors */ }
960                                await state.userDataManager.updateProfile(repairUpdates);
961                                console.log('SwiftAuth: Profile repaired');
962                            }
963                        }
964                    } catch (repairErr) {
965                        console.error('SwiftAuth: Profile repair failed (non-blocking):', repairErr.message);
966                    }
967                } else {
968                    const err = state.userDataManager?.initError;
969                    console.error('SwiftAuth: Failed to initialize UserDataManager');
970                    if (err) {
971                        console.error('SwiftAuth: AWS Error:', err.code, '-', err.message);
972                    }
973                }
974
975                // If WordPress thinks user is anonymous but Cognito has session, sync them
976                if (state.authLevel === 'anonymous') {
977                    console.log('SwiftAuth: Syncing Cognito session with WordPress');
978                    try {
979                        const response = await ajaxRequest('swift_auth_complete', {
980                            id_token: idToken,
981                        });
982
983                        if (response.success) {
984                            state.authLevel = 'full';
985                            state.sub = response.sub;
986                            console.log('SwiftAuth: WordPress session synced with Cognito');
987                            updateUI();
988                        }
989                    } catch (syncErr) {
990                        console.error('SwiftAuth: Session sync error', syncErr);
991                    }
992                }
993
994                resolve();
995            });
996        });
997    }
998
999    /**
1000     * Check for existing Cognito session and sync with WordPress
1001     * Uses refresh token to get new ID token if needed
1002     * Also initializes UserDataManager for direct DynamoDB access
1003     * @deprecated Use initUserDataManagerFromCognitoSession instead
1004     */
1005    async function checkCognitoSession() {
1006        if (!window.swiftAuthUserPool) {
1007            return null;
1008        }
1009
1010        const cognitoUser = window.swiftAuthUserPool.getCurrentUser();
1011        if (!cognitoUser) {
1012            console.log('SwiftAuth: No Cognito user found in localStorage');
1013            return null;
1014        }
1015
1016        return new Promise((resolve) => {
1017            cognitoUser.getSession(async (err, session) => {
1018                if (err) {
1019                    console.log('SwiftAuth: Cognito session error', err.message);
1020                    resolve(null);
1021                    return;
1022                }
1023
1024                if (!session || !session.isValid()) {
1025                    console.log('SwiftAuth: Cognito session invalid or expired');
1026                    resolve(null);
1027                    return;
1028                }
1029
1030                console.log('SwiftAuth: Valid Cognito session found');
1031
1032                const idToken = session.getIdToken().getJwtToken();
1033
1034                // Initialize UserDataManager for direct DynamoDB access
1035                await initUserDataManager(idToken);
1036                
1037                // Hydrate cache immediately
1038                if (state.userDataManager) {
1039                    state.userDataManager.hydrate();
1040                }
1041
1042                // If WordPress session is anonymous but Cognito has valid session,
1043                // sync them by sending the ID token to WordPress
1044                if (state.authLevel === 'anonymous') {
1045                    console.log('SwiftAuth: Syncing Cognito session with WordPress');
1046
1047                    try {
1048                        const response = await ajaxRequest('swift_auth_complete', {
1049                            id_token: idToken,
1050                        });
1051
1052                        if (response.success) {
1053                            state.authLevel = 'full';
1054                            state.sub = response.sub;
1055                            console.log('SwiftAuth: WordPress session synced with Cognito');
1056                            updateUI();
1057                            resolve(session);
1058                        } else {
1059                            console.log('SwiftAuth: Failed to sync session', response.error);
1060                            resolve(null);
1061                        }
1062                    } catch (syncErr) {
1063                        console.error('SwiftAuth: Session sync error', syncErr);
1064                        resolve(null);
1065                    }
1066                } else {
1067                    resolve(session);
1068                }
1069            });
1070        });
1071    }
1072
1073    /**
1074     * Login with email/password
1075     */
1076    async function login(email, password) {
1077        if (!window.swiftAuthUserPool) {
1078            throw new Error('Cognito not initialized');
1079        }
1080
1081        return new Promise((resolve, reject) => {
1082            const authDetails = new AmazonCognitoIdentity.AuthenticationDetails({
1083                Username: email,
1084                Password: password,
1085            });
1086
1087            const userData = {
1088                Username: email,
1089                Pool: window.swiftAuthUserPool,
1090            };
1091
1092            const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
1093
1094            cognitoUser.authenticateUser(authDetails, {
1095                onSuccess: async (result) => {
1096                    const idToken = result.getIdToken().getJwtToken();
1097
1098                    // Initialize UserDataManager for direct DynamoDB access
1099                    await initUserDataManager(idToken);
1100
1101                    // Hydrate cache immediately
1102                    if (state.userDataManager) {
1103                        state.userDataManager.hydrate();
1104                    }
1105
1106                    // Send to server to sync profile (WordPress still handles this for session)
1107                    const response = await ajaxRequest('swift_auth_complete', {
1108                        id_token: idToken,
1109                    });
1110
1111                    if (response.success) {
1112                        state.authLevel = 'full';
1113                        state.sub = response.sub;
1114                        updateUI();
1115
1116                        // Track login completed
1117                        if (window.SwiftAnalytics) {
1118                            window.SwiftAnalytics.trackLoginCompleted('email');
1119                        }
1120
1121                        // Emit login event for authBridge to trigger hydration
1122                        window.dispatchEvent(new CustomEvent('swiftauth:login'));
1123
1124                        // Sync user profile attributes from Cognito to DynamoDB
1125                        // MUST be awaited — for new users this creates the profile,
1126                        // and the page redirects immediately after login resolves.
1127                        await syncUserProfileFromToken(idToken);
1128
1129                        // Record login event for dashboard analytics (fire-and-forget)
1130                        if (state.userDataManager) state.userDataManager.recordLogin();
1131
1132                        // Update Pinpoint endpoint for magic link support (fire-and-forget is OK)
1133                        updatePinpointEndpoint(response.sub, email);
1134
1135                        resolve(response);
1136                    } else {
1137                        // Track login failed
1138                        if (window.SwiftAnalytics) {
1139                            window.SwiftAnalytics.trackLoginFailed(response.error || 'server_error', 'email');
1140                        }
1141                        reject(new Error(response.error || 'Login failed'));
1142                    }
1143                },
1144                onFailure: (err) => {
1145                    // Track login failed
1146                    if (window.SwiftAnalytics) {
1147                        window.SwiftAnalytics.trackLoginFailed(err.code || err.message || 'cognito_error', 'email');
1148                    }
1149                    reject(err);
1150                },
1151                newPasswordRequired: (userAttributes, requiredAttributes) => {
1152                    // Handle new password required
1153                    state.cognitoUser = cognitoUser;
1154                    reject({ code: 'NewPasswordRequired', userAttributes, requiredAttributes });
1155                },
1156            });
1157        });
1158    }
1159
1160    /**
1161     * Register new user
1162     */
1163    async function register(email, password, attributes = {}) {
1164        if (!window.swiftAuthUserPool) {
1165            throw new Error('Cognito not initialized');
1166        }
1167
1168        return new Promise((resolve, reject) => {
1169            const attributeList = [];
1170
1171            if (attributes.email) {
1172                attributeList.push(new AmazonCognitoIdentity.CognitoUserAttribute({
1173                    Name: 'email',
1174                    Value: attributes.email,
1175                }));
1176            }
1177
1178            if (attributes.name) {
1179                attributeList.push(new AmazonCognitoIdentity.CognitoUserAttribute({
1180                    Name: 'name',
1181                    Value: attributes.name,
1182                }));
1183            }
1184
1185            // Track signup started
1186            if (window.SwiftAnalytics) {
1187                window.SwiftAnalytics.trackSignupStarted('email');
1188            }
1189
1190            window.swiftAuthUserPool.signUp(email, password, attributeList, null, (err, result) => {
1191                if (err) {
1192                    reject(err);
1193                } else {
1194                    // Track signup completed
1195                    if (window.SwiftAnalytics) {
1196                        window.SwiftAnalytics.trackSignupCompleted('email');
1197                    }
1198                    resolve(result);
1199                }
1200            });
1201        });
1202    }
1203
1204    /**
1205     * Confirm registration with code
1206     */
1207    async function confirmRegistration(email, code) {
1208        if (!window.swiftAuthUserPool) {
1209            throw new Error('Cognito not initialized');
1210        }
1211
1212        return new Promise((resolve, reject) => {
1213            const userData = {
1214                Username: email,
1215                Pool: window.swiftAuthUserPool,
1216            };
1217
1218            const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
1219
1220            cognitoUser.confirmRegistration(code, true, (err, result) => {
1221                if (err) {
1222                    reject(err);
1223                } else {
1224                    resolve(result);
1225                }
1226            });
1227        });
1228    }
1229
1230    /**
1231     * Forgot password - send code
1232     */
1233    async function forgotPassword(email) {
1234        if (!window.swiftAuthUserPool) {
1235            throw new Error('Cognito not initialized');
1236        }
1237
1238        // Track password reset requested
1239        if (window.SwiftAnalytics) {
1240            window.SwiftAnalytics.track('password_reset_requested', { email_provided: true });
1241        }
1242
1243        const attemptForgotPassword = () => {
1244            return new Promise((resolve, reject) => {
1245                const userData = {
1246                    Username: email,
1247                    Pool: window.swiftAuthUserPool,
1248                };
1249                const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
1250                cognitoUser.forgotPassword({
1251                    onSuccess: resolve,
1252                    onFailure: reject,
1253                });
1254            });
1255        };
1256
1257        try {
1258            return await attemptForgotPassword();
1259        } catch (err) {
1260            // Cognito rejects forgotPassword for FORCE_CHANGE_PASSWORD users.
1261            // Recover server-side (AdminSetUserPassword to CONFIRMED), then retry.
1262            const msg = (err.message || '').toLowerCase();
1263            if (msg.includes('password cannot be reset') || msg.includes('user password cannot be reset')) {
1264                console.log('SwiftAuth: User in FORCE_CHANGE_PASSWORD state, attempting recovery...');
1265                try {
1266                    await fetchNonce();
1267                    const resp = await fetch(ajax.ajaxUrl || '/wp-admin/admin-ajax.php', {
1268                        method: 'POST',
1269                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1270                        body: new URLSearchParams({
1271                            action: 'swift_forgot_password_recover',
1272                            nonce: ajax.nonce,
1273                            email: email,
1274                        }),
1275                    });
1276                    const data = await resp.json();
1277                    if (data.success || data.data?.success) {
1278                        console.log('SwiftAuth: Recovery successful, retrying forgotPassword...');
1279                        return await attemptForgotPassword();
1280                    }
1281                } catch (recoveryErr) {
1282                    console.error('SwiftAuth: FORCE_CHANGE_PASSWORD recovery failed', recoveryErr);
1283                }
1284            }
1285            throw err;
1286        }
1287    }
1288
1289    /**
1290     * Confirm new password
1291     */
1292    async function confirmPassword(email, code, newPassword) {
1293        if (!window.swiftAuthUserPool) {
1294            throw new Error('Cognito not initialized');
1295        }
1296
1297        return new Promise((resolve, reject) => {
1298            const userData = {
1299                Username: email,
1300                Pool: window.swiftAuthUserPool,
1301            };
1302
1303            const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
1304
1305            cognitoUser.confirmPassword(code, newPassword, {
1306                onSuccess: resolve,
1307                onFailure: reject,
1308            });
1309        });
1310    }
1311
1312    /**
1313     * Change password for the currently authenticated user
1314     * @param {string} oldPassword - Current password
1315     * @param {string} newPassword - New password
1316     * @returns {Promise<string>} - 'SUCCESS' on success
1317     */
1318    async function changePassword(oldPassword, newPassword) {
1319        return new Promise((resolve, reject) => {
1320            const cognitoUser = window.swiftAuthUserPool.getCurrentUser();
1321            if (!cognitoUser) {
1322                reject(new Error('No authenticated user'));
1323                return;
1324            }
1325            cognitoUser.getSession((err, session) => {
1326                if (err || !session?.isValid()) {
1327                    reject(new Error('Session expired. Please sign in again.'));
1328                    return;
1329                }
1330                cognitoUser.changePassword(oldPassword, newPassword, (err, result) => {
1331                    if (err) reject(err);
1332                    else resolve(result);
1333                });
1334            });
1335        });
1336    }
1337
1338    /**
1339     * Reset password and automatically log in
1340     * @param {string} email - User's email
1341     * @param {string} code - Reset code from email
1342     * @param {string} newPassword - New password
1343     * @returns {Promise<object>} - Login response
1344     */
1345    async function resetPasswordAndLogin(email, code, newPassword) {
1346        // First, reset the password via Cognito
1347        await confirmPassword(email, code, newPassword);
1348
1349        console.log('SwiftAuth: Password reset successful, logging in...');
1350
1351        // Now log in with the new password
1352        const loginResponse = await login(email, newPassword);
1353
1354        return loginResponse;
1355    }
1356
1357    /**
1358     * Handle forgot password link from email
1359     * Pre-fills form fields and prepares for auto-login after reset
1360     * @param {Element} authPage - The authentication page element
1361     * @param {object} params - The forgot password params from URL
1362     */
1363    function handleForgotPasswordLink(authPage, params) {
1364        console.log('SwiftAuth: Forgot password link detected', params);
1365
1366        // Store params for use after password reset
1367        state.forgotPasswordParams = params;
1368
1369        // Pre-fill code field
1370        const codeInput = authPage.querySelector('[name="reset_code"], [name="forgot_password_code"]');
1371        if (codeInput && params.code) {
1372            codeInput.value = params.code;
1373        }
1374
1375        // Pre-fill email field
1376        const emailInput = authPage.querySelector('[name="reset_email"], [name="email"]');
1377        if (emailInput && params.email) {
1378            emailInput.value = params.email;
1379            // Optionally make read-only since it came from the link
1380            emailInput.setAttribute('readonly', 'readonly');
1381        }
1382
1383        // Dispatch event so theme can switch to password reset view
1384        document.dispatchEvent(new CustomEvent('swiftauth:forgot_password_link', {
1385            detail: params
1386        }));
1387    }
1388
1389    /**
1390     * Logout
1391     */
1392    async function logout() {
1393        console.log('SwiftAuth: logout() starting...');
1394
1395        // Track logout before clearing state
1396        if (window.SwiftAnalytics) {
1397            window.SwiftAnalytics.trackLogout();
1398        }
1399
1400        // Mark as logged out so init() won't re-elevate auth level
1401        state.loggedOut = true;
1402
1403        // Reset all in-memory state
1404        state.authLevel = 'anonymous';
1405        state.sub = null;
1406        state.profile = null;
1407        state.followedTopics = [];
1408        state.readingHistory = [];
1409        state.userDataManager = null;
1410        state.userDataManagerReady = false;
1411
1412        // Clear all plugin localStorage data (except regwall meter)
1413        const pubID = config.pubID || '';
1414        if (pubID) {
1415            const keysToRemove = [
1416                pubID + 'Subscription',
1417                pubID + 'bookmarkedPosts',
1418                pubID + 'readingHistory',
1419                pubID + 'recentlyViewed',
1420                pubID + 'followedTopics',
1421                pubID + 'userProfile',
1422                pubID + 'pageViews',
1423                pubID + 'Profile',
1424                pubID + 'readingHistorySetting',
1425            ];
1426            keysToRemove.forEach(key => {
1427                try { localStorage.removeItem(key); } catch (e) { /* ignore */ }
1428            });
1429            console.log('SwiftAuth: localStorage cleared (' + keysToRemove.length + ' keys)');
1430        }
1431
1432        // Clear magic session token (lite-login)
1433        try { localStorage.removeItem(MAGIC_TOKEN_KEY); } catch (e) { /* ignore */ }
1434
1435        // Clear cached entitlement from sessionStorage
1436        try { sessionStorage.removeItem('swift_entitlement_cache'); } catch (e) { /* ignore */ }
1437
1438        // Update UI immediately
1439        updateUI();
1440
1441        // Emit logout event for authBridge
1442        window.dispatchEvent(new CustomEvent('swiftauth:logout'));
1443
1444        // Clear Cognito session — signOut() clears tokens but may leave LastAuthUser
1445        if (window.swiftAuthUserPool) {
1446            const cognitoUser = window.swiftAuthUserPool.getCurrentUser();
1447            if (cognitoUser) {
1448                cognitoUser.signOut();
1449                console.log('SwiftAuth: Cognito signOut() called');
1450            }
1451        }
1452
1453        // Belt-and-suspenders: remove all Cognito localStorage keys directly
1454        // This MUST run even if swiftAuthUserPool isn't initialized yet
1455        // (e.g. logout called before init on the /logout/ page)
1456        const clientId = config.cognito?.clientId;
1457        if (clientId) {
1458            const prefix = 'CognitoIdentityServiceProvider.' + clientId;
1459            const toRemove = [];
1460            for (let i = 0; i < localStorage.length; i++) {
1461                const key = localStorage.key(i);
1462                if (key && key.startsWith(prefix)) {
1463                    toRemove.push(key);
1464                }
1465            }
1466            toRemove.forEach(key => {
1467                try { localStorage.removeItem(key); } catch (e) { /* ignore */ }
1468            });
1469            console.log('SwiftAuth: Cognito localStorage keys removed (' + toRemove.length + ')');
1470        }
1471
1472        // Clear AWS credentials cache
1473        if (typeof AWS !== 'undefined' && AWS.config && AWS.config.credentials) {
1474            AWS.config.credentials.clearCachedId();
1475            console.log('SwiftAuth: AWS credentials cache cleared');
1476        }
1477
1478        // Send logout request to server (clears Cognito session server-side)
1479        const response = await ajaxRequest('swift_auth_logout');
1480        console.log('SwiftAuth: Server logout response:', response);
1481
1482        return response;
1483    }
1484
1485    /**
1486     * Delete user account permanently
1487     * Requires double confirmation from user
1488     * Deletes DynamoDB data and Cognito user directly from frontend
1489     */
1490    /**
1491     * Read the Cognito ID token synchronously from localStorage.
1492     */
1493    function getIdTokenSync() {
1494        try {
1495            const clientId = config.cognito?.clientId;
1496            if (!clientId) return null;
1497            const lastUserKey = `CognitoIdentityServiceProvider.${clientId}.LastAuthUser`;
1498            const username = localStorage.getItem(lastUserKey);
1499            if (!username) return null;
1500            const tokenKey = `CognitoIdentityServiceProvider.${clientId}.${username}.idToken`;
1501            return localStorage.getItem(tokenKey);
1502        } catch (e) {
1503            return null;
1504        }
1505    }
1506
1507    /**
1508     * Get the current user's email from their Cognito ID token.
1509     */
1510    function getTokenEmail() {
1511        try {
1512            const idToken = getIdTokenSync();
1513            if (!idToken) return null;
1514            const payload = JSON.parse(atob(idToken.split('.')[1]));
1515            return payload.email || null;
1516        } catch (e) {
1517            return null;
1518        }
1519    }
1520
1521    async function deleteAccount() {
1522        // Show confirmation modal
1523        const confirmed = await new Promise((resolve) => {
1524            // Remove any existing modal
1525            const existing = document.querySelector('.swift-delete-modal-overlay');
1526            if (existing) existing.remove();
1527
1528            const overlay = document.createElement('div');
1529            overlay.className = 'swift-delete-modal-overlay';
1530            overlay.innerHTML = `
1531                <div class="swift-delete-modal">
1532                    <h3>Delete your account?</h3>
1533                    <p>This action is <strong>permanent</strong> and cannot be undone. All your data including bookmarks, reading history, and profile information will be deleted.</p>
1534                    <div class="swift-delete-modal-actions">
1535                        <button type="button" class="swift-btn swift-btn-outline swift-delete-cancel">Cancel</button>
1536                        <button type="button" class="swift-btn swift-btn-danger swift-delete-confirm">Delete My Account</button>
1537                    </div>
1538                </div>
1539            `;
1540
1541            overlay.querySelector('.swift-delete-cancel').addEventListener('click', () => {
1542                overlay.remove();
1543                resolve(false);
1544            });
1545            overlay.querySelector('.swift-delete-confirm').addEventListener('click', () => {
1546                overlay.remove();
1547                resolve(true);
1548            });
1549            overlay.addEventListener('click', (e) => {
1550                if (e.target === overlay) {
1551                    overlay.remove();
1552                    resolve(false);
1553                }
1554            });
1555
1556            document.body.appendChild(overlay);
1557        });
1558
1559        if (!confirmed) {
1560            return { success: false, reason: 'cancelled' };
1561        }
1562
1563        try {
1564            // Server-side deletion: DynamoDB data, User Pool user, Identity Pool identity
1565            console.log('SwiftAuth: Requesting server-side account deletion...');
1566            const response = await ajaxRequest('swift_delete_account');
1567
1568            if (!response.success) {
1569                throw new Error(response.error || 'Account deletion failed on the server.');
1570            }
1571
1572            console.log('SwiftAuth: Server deletion results:', response.results);
1573
1574            // Clear local state
1575            state.authLevel = 'anonymous';
1576            state.sub = null;
1577            state.profile = null;
1578            state.followedTopics = [];
1579            state.readingHistory = [];
1580            state.userDataManager = null;
1581            state.userDataManagerReady = false;
1582
1583            // Sign out locally to clear cached Cognito tokens from localStorage
1584            if (window.swiftAuthUserPool) {
1585                const cognitoUser = window.swiftAuthUserPool.getCurrentUser();
1586                if (cognitoUser) {
1587                    cognitoUser.signOut();
1588                }
1589            }
1590
1591            // Clear any localStorage data
1592            const pubID = config.pubID || '';
1593            const keysToRemove = [
1594                pubID + 'bookmarkedPosts',
1595                pubID + 'readingHistory',
1596                pubID + 'followedTopics',
1597                pubID + 'userProfile',
1598                pubID + 'pageViews',
1599                pubID + 'Profile',
1600                pubID + 'Subscription'
1601            ];
1602            keysToRemove.forEach(key => {
1603                try {
1604                    localStorage.removeItem(key);
1605                } catch (e) {
1606                    // Ignore localStorage errors
1607                }
1608            });
1609
1610            // Show success message and redirect
1611            alert('Your account has been deleted successfully.');
1612            window.location.href = '/';
1613
1614            return { success: true };
1615
1616        } catch (error) {
1617            console.error('SwiftAuth: Account deletion failed', error);
1618            alert('Failed to delete account: ' + error.message);
1619            return { success: false, error: error.message };
1620        }
1621    }
1622
1623    // =========================================================================
1624    // Regwall
1625    // =========================================================================
1626
1627    /**
1628     * Get/update regwall meter from localStorage.
1629     * Tracks unique article views per calendar month.
1630     */
1631    function getRegwallMeter() {
1632        const STORAGE_KEY = 'swift_regwall_meter';
1633        const rawPostId = getPostId();
1634        const postId = (typeof rawPostId === 'number' && !isNaN(rawPostId)) ? rawPostId : null;
1635
1636        let meter;
1637        try {
1638            meter = JSON.parse(localStorage.getItem(STORAGE_KEY));
1639        } catch (e) {
1640            meter = null;
1641        }
1642
1643        // Calendar-month reset: compare YYYY-MM of meter start vs now
1644        const nowMonth = new Date().toISOString().slice(0, 7);
1645        const meterMonth = meter && meter.started ? new Date(meter.started).toISOString().slice(0, 7) : null;
1646        if (!meter || !meter.started || !Array.isArray(meter.posts) || nowMonth !== meterMonth) {
1647            const firstOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime();
1648            meter = { count: 0, posts: [], started: firstOfMonth };
1649        }
1650
1651        // Increment for new unique article
1652        if (postId !== null) {
1653            const alreadyViewed = meter.posts.some(id => Number(id) === postId);
1654            if (!alreadyViewed) {
1655                meter.count++;
1656                meter.posts.push(postId);
1657                if (meter.posts.length > 200) {
1658                    meter.posts = meter.posts.slice(-200);
1659                }
1660            }
1661        }
1662
1663        localStorage.setItem(STORAGE_KEY, JSON.stringify(meter));
1664        return meter.count;
1665    }
1666
1667    /**
1668     * Determine which regwall wall to show based on count (client-side).
1669     * Mirrors determinePaywallWall() logic but uses config.regwall.walls.
1670     */
1671    function determineRegwallWall(count) {
1672        const walls = config.regwall?.walls || [];
1673        if (!walls.length) return null;
1674
1675        const enabled = walls
1676            .map((w, i) => ({ ...w, originalIndex: i }))
1677            .filter(w => w.enabled !== false)
1678            .sort((a, b) => (b.threshold || 0) - (a.threshold || 0));
1679
1680        for (const wall of enabled) {
1681            if (count >= (wall.threshold || 0)) {
1682                return wall;
1683            }
1684        }
1685        return null;
1686    }
1687
1688    /**
1689     * Check and handle regwall (fully client-side).
1690     * Uses localStorage meter + wall config fetched from static JSON.
1691     */
1692    function isWallBypassed() {
1693        const meta = window.swiftArticleMeta;
1694        if (!meta) return false;
1695
1696        if (meta.freeContent === true) return true;
1697
1698        const bypassTags = config.wallBypassTags;
1699        if (bypassTags && bypassTags.length && meta.tags && meta.tags.length) {
1700            if (meta.tags.some(tag => bypassTags.includes(tag))) return true;
1701        }
1702
1703        const bypassCategories = config.wallBypassCategories;
1704        if (bypassCategories && bypassCategories.length && meta.categories && meta.categories.length) {
1705            if (meta.categories.some(cat => bypassCategories.includes(cat))) return true;
1706        }
1707
1708        return false;
1709    }
1710
1711    function checkRegwall() {
1712        if (!config.regwall || !config.regwall.enabled) {
1713            return;
1714        }
1715
1716        if (isWallBypassed()) {
1717            console.log('SwiftAuth: Regwall - bypassed (tag bypass)');
1718            return;
1719        }
1720
1721        if (state.authLevel !== 'anonymous') {
1722            return; // Authenticated users bypass regwall
1723        }
1724
1725        const count = getRegwallMeter();
1726        const wall = determineRegwallWall(count);
1727
1728        if (!wall) {
1729            console.log('SwiftAuth: Regwall - no wall triggered, count:', count);
1730            return;
1731        }
1732
1733        console.log('SwiftAuth: Regwall - wall triggered, type:', wall.type, 'threshold:', wall.threshold, 'count:', count);
1734        const html = wall.html || null;
1735        const wallType = wall.type || 'hard';
1736        const shouldBlur = wallType === 'hard';
1737        const wallLevel = (wall.originalIndex || 0) + 1;
1738        const creativeSlot = 'regwall_' + wallType + '_' + wallLevel;
1739
1740        showRegwall(html, wallType, shouldBlur, creativeSlot);
1741
1742        // Track regwall shown
1743        if (window.SwiftAnalytics) {
1744            window.SwiftAnalytics.trackRegwallShown(
1745                wallLevel,
1746                wallType,
1747                wall.threshold || 0,
1748                count
1749            );
1750            window.SwiftAnalytics.trackWallSelectPromotion(wallType, 'Shown', creativeSlot, !!config.paywall?.enabled);
1751        }
1752    }
1753
1754    /**
1755     * Display regwall modal or inline message
1756     */
1757    function showRegwall(html, wallType, shouldBlur, creativeSlot) {
1758        const displayMode = config.regwall?.displayMode || 'modal';
1759
1760        // Store current article URL so user returns here after login
1761        const article = document.querySelector('.p402_premium');
1762        if (article) {
1763            setReturnUrl(window.location.href);
1764        }
1765
1766        // Fallback HTML if none configured
1767        const fallbackHtml = wallType === 'hard'
1768            ? `<div class="swift-regwall-message">
1769                <h2>Create a free account to continue</h2>
1770                <p>Sign up for free to access this content and more.</p>
1771                <a href="/authentication/" class="swift-btn-primary" data-swift-login>Sign Up Free</a>
1772                <p style="margin-top: 12px; font-size: 14px;">Already have an account? <a href="/authentication/" data-swift-login>Sign In</a></p>
1773               </div>`
1774            : `<div class="swift-regwall-message">
1775                <button data-swift-close type="button">&times;</button>
1776                <h2>Want to keep reading?</h2>
1777                <p>Create a free account to access unlimited articles.</p>
1778                <a href="/authentication/" class="swift-btn-primary" data-swift-login>Sign Up Free</a>
1779               </div>`;
1780
1781        const content = html || fallbackHtml;
1782
1783        if (displayMode === 'inline') {
1784            // Inline mode: insert wall after first ad, hide remaining content
1785            const article = document.querySelector('.p402_premium');
1786            if (article) {
1787                // Check if inline regwall already exists
1788                let inlineWall = article.querySelector('.swift-regwall-inline');
1789                if (!inlineWall) {
1790                    inlineWall = document.createElement('div');
1791                    inlineWall.className = 'swift-regwall-inline';
1792                    inlineWall.setAttribute('data-wall-type', wallType);
1793                    inlineWall.setAttribute('data-wall-name', creativeSlot || '');
1794                }
1795                inlineWall.innerHTML = content;
1796
1797                // Insert after first .module-ad and hide remaining content
1798                const restoreContent = insertWallAfterFirstAd(article, inlineWall, !!config.regwall?.inlineBlur);
1799
1800                // Add close handler for soft walls
1801                if (wallType !== 'hard') {
1802                    const closeBtn = inlineWall.querySelector('[data-swift-close], [data-dismiss="modal"], .close');
1803                    if (closeBtn) {
1804                        closeBtn.addEventListener('click', (e) => {
1805                            e.preventDefault();
1806                            console.log('SwiftAuth: Regwall dismissed via close button');
1807                            if (window.SwiftAnalytics) {
1808                                window.SwiftAnalytics.trackRegwallDismissed(null, wallType);
1809                                window.SwiftAnalytics.trackWallSelectPromotion(wallType, 'ClickClose', creativeSlot, !!config.paywall?.enabled);
1810                            }
1811                            restoreContent();
1812                            inlineWall.remove();
1813                        });
1814                    }
1815                }
1816            }
1817        } else if (displayMode === 'bottom-sheet') {
1818            // Bottom-sheet mode: fixed sheet slides up from bottom, content stays visible
1819            const bsArticle = document.querySelector('.p402_premium');
1820            let restoreContent = function() {};
1821
1822            let sheet = document.querySelector('.swift-regwall-bottom-sheet');
1823            if (!sheet) {
1824                sheet = document.createElement('div');
1825                sheet.className = 'swift-bottom-sheet swift-regwall-bottom-sheet';
1826                document.body.appendChild(sheet);
1827            }
1828            sheet.setAttribute('data-wall-type', wallType);
1829            sheet.setAttribute('data-wall-name', creativeSlot || '');
1830            sheet.innerHTML = `<div class="swift-bottom-sheet-handle"></div><div class="swift-bottom-sheet-content">${splitBottomSheetContent(content)}</div>`;
1831
1832            if (wallType !== 'hard') {
1833                const closeBtn = sheet.querySelector('[data-swift-close], [data-dismiss="modal"], .close');
1834                if (closeBtn) {
1835                    closeBtn.addEventListener('click', (e) => {
1836                        e.preventDefault();
1837                        console.log('SwiftAuth: Regwall bottom sheet dismissed');
1838                        if (window.SwiftAnalytics) {
1839                            window.SwiftAnalytics.trackRegwallDismissed(null, wallType);
1840                            window.SwiftAnalytics.trackWallSelectPromotion(wallType, 'ClickClose', creativeSlot, !!config.paywall?.enabled);
1841                        }
1842                        restoreContent();
1843                        hideBottomSheet(sheet);
1844                    });
1845                }
1846            }
1847
1848            const bsFirstAd = bsArticle ? bsArticle.querySelector('.module-ad') : null;
1849            waitForScrollPastFirstAd(bsFirstAd, function() {
1850                if (bsArticle && shouldBlur) {
1851                    // Blur content below first ad using an invisible sentinel — no inline wall inserted
1852                    const sentinel = document.createElement('div');
1853                    sentinel.className = 'swift-bottom-sheet-sentinel';
1854                    sentinel.style.cssText = 'height:0;overflow:hidden;margin:0;padding:0;';
1855                    restoreContent = insertWallAfterFirstAd(bsArticle, sentinel, true);
1856                }
1857                sheet.classList.add('swift-bottom-sheet-active');
1858            });
1859        } else {
1860            // Modal mode: show overlay
1861            const container = document.getElementById('swift-regwall-container');
1862            if (!container) return;
1863
1864            container.innerHTML = `<div class="swift-regwall-modal">${content}</div>`;
1865            container.setAttribute('data-wall-type', wallType);
1866            container.setAttribute('data-wall-name', creativeSlot || '');
1867
1868            if (wallType !== 'hard') {
1869                // Soft walls: attach close handler now (container is in DOM, just hidden)
1870                const closeBtn = container.querySelector('[data-swift-close], [data-dismiss="modal"], .close');
1871                if (closeBtn) {
1872                    closeBtn.addEventListener('click', (e) => {
1873                        e.preventDefault();
1874                        console.log('SwiftAuth: Regwall dismissed via close button');
1875                        if (window.SwiftAnalytics) {
1876                            window.SwiftAnalytics.trackRegwallDismissed(null, wallType);
1877                            window.SwiftAnalytics.trackWallSelectPromotion(wallType, 'ClickClose', creativeSlot, !!config.paywall?.enabled);
1878                        }
1879                        hideRegwall();
1880                    });
1881                }
1882
1883                // Allow clicking overlay to dismiss soft walls
1884                const overlay = document.querySelector('.swift-wall-overlay');
1885                if (overlay) {
1886                    overlay.addEventListener('click', (e) => {
1887                        if (e.target === overlay) {
1888                            console.log('SwiftAuth: Regwall dismissed via overlay click');
1889                            hideRegwall();
1890                        }
1891                    });
1892                }
1893            }
1894
1895            // Defer visibility until user scrolls past the first ad
1896            const regwallArticle = document.querySelector('.p402_premium');
1897            const firstAd = regwallArticle ? regwallArticle.querySelector('.module-ad') : null;
1898            waitForScrollPastFirstAd(firstAd, function() {
1899                container.style.display = 'block';
1900                requestAnimationFrame(function() { container.classList.add('swift-wall-active'); });
1901                if (wallType === 'hard') {
1902                    if (regwallArticle) regwallArticle.classList.add('swift-content-hidden');
1903                    showOverlay();
1904                }
1905            });
1906        }
1907
1908        // Initialize login buttons in the newly added HTML
1909        initLoginButtons();
1910
1911        // Hide login prompts for already-authenticated users
1912        if (state.authLevel === 'full') {
1913            document.querySelectorAll('[data-swift-wall-login]').forEach(el => el.remove());
1914        }
1915    }
1916
1917    /**
1918     * Hide regwall
1919     */
1920    function hideRegwall() {
1921        const container = document.getElementById('swift-regwall-container');
1922        if (container) {
1923            container.classList.remove('swift-wall-active');
1924            container.style.display = 'none';
1925        }
1926        unlockContent();
1927    }
1928
1929    function hideBottomSheet(sheet) {
1930        const el = sheet || document.querySelector('.swift-bottom-sheet');
1931        if (!el) return;
1932        el.classList.remove('swift-bottom-sheet-active');
1933        setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 400);
1934    }
1935
1936    /**
1937     * Split wall HTML into text pane + actions pane for the two-column bottom sheet layout.
1938     * Unwraps a single wrapper div if needed, then separates headings/paragraphs (left)
1939     * from buttons, links, and other elements (right).
1940     */
1941    function splitBottomSheetContent(html) {
1942        const temp = document.createElement('div');
1943        temp.innerHTML = html;
1944
1945        // Unwrap a single container div (common wall HTML pattern)
1946        let root = temp;
1947        if (temp.children.length === 1 && temp.firstElementChild.tagName === 'DIV') {
1948            root = temp.firstElementChild;
1949        }
1950
1951        const TEXT_TAGS = new Set(['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P']);
1952        const textNodes = [];
1953        const actionNodes = [];
1954
1955        Array.from(root.children).forEach(function(el) {
1956            if (TEXT_TAGS.has(el.tagName)) {
1957                // Strip text-center class and inline text-align from text elements
1958                el.classList.remove('text-center', 'text-left', 'text-right');
1959                el.style.removeProperty('text-align');
1960                textNodes.push(el.outerHTML);
1961            } else {
1962                actionNodes.push(el.outerHTML);
1963            }
1964        });
1965
1966        if (textNodes.length && actionNodes.length) {
1967            return '<div class="swift-bs-text">' + textNodes.join('') + '</div>'
1968                 + '<div class="swift-bs-actions">' + actionNodes.join('') + '</div>';
1969        }
1970
1971        return html; // can't split cleanly — fall back to original
1972    }
1973
1974    // =========================================================================
1975    // Paywall
1976    // =========================================================================
1977
1978    /**
1979     * Check if current IP is in the paywall bypass allowlist (server-side check).
1980     * Caches result in sessionStorage for 5 minutes to avoid repeat AJAX calls.
1981     *
1982     * @returns {Promise<boolean>} true if IP bypass is granted
1983     */
1984    async function checkIpBypass() {
1985        if (!config.paywall?.ipAllowlistEnabled) return false;
1986
1987        const CACHE_KEY = 'swift_ip_bypass';
1988        const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
1989
1990        try {
1991            const cached = sessionStorage.getItem(CACHE_KEY);
1992            if (cached) {
1993                const parsed = JSON.parse(cached);
1994                if (parsed.ts && (Date.now() - parsed.ts < CACHE_TTL)) {
1995                    return parsed.bypass === true;
1996                }
1997            }
1998        } catch (e) { /* ignore parse errors */ }
1999
2000        try {
2001            // No nonce: swift_ip_bypass_check is a read-only IP check (server
2002            // verifies no nonce). Skipping it avoids a swift_get_nonce round-trip
2003            // on anonymous paywall loads.
2004            const formData = new FormData();
2005            formData.append('action', 'swift_ip_bypass_check');
2006
2007            const resp = await fetch(ajax.ajaxUrl, { method: 'POST', body: formData });
2008            const data = await resp.json();
2009            const bypass = data.bypass === true;
2010
2011            try {
2012                sessionStorage.setItem(CACHE_KEY, JSON.stringify({ bypass, ts: Date.now() }));
2013            } catch (e) { /* storage full, ignore */ }
2014
2015            return bypass;
2016        } catch (err) {
2017            console.warn('SwiftAuth: IP bypass check failed:', err);
2018            return false;
2019        }
2020    }
2021
2022    function resolveSubscribeNavBtn(isSubscriber) {
2023        const btn = document.querySelector('.js-subscribe-nav-btn');
2024        if (!btn) return;
2025        btn.classList.toggle('d-none', isSubscriber);
2026    }
2027
2028    /**
2029     * Check and handle paywall (client-side)
2030     * Uses cached subscription for instant decision, then verifies from DynamoDB
2031     */
2032    async function checkPaywall() {
2033        if (!config.paywall || !config.paywall.enabled) {
2034            console.log('SwiftAuth: Paywall not enabled');
2035            return;
2036        }
2037
2038        // Tag bypass — checked before any paywall logic
2039        if (isWallBypassed()) {
2040            console.log('SwiftAuth: Paywall - bypassed (tag bypass)');
2041            return;
2042        }
2043
2044        // IP allowlist bypass — checked before any paywall logic
2045        if (await checkIpBypass()) {
2046            console.log('SwiftAuth: Paywall - granted (IP bypass)');
2047            return;
2048        }
2049
2050        console.log('SwiftAuth: Checking paywall, auth level:', state.authLevel);
2051
2052        // Anonymous users - check walls client-side (localStorage meter)
2053        if (state.authLevel === 'anonymous') {
2054            console.log('SwiftAuth: Paywall - checking walls for anonymous user (client-side)');
2055            checkPaywallAnonymous();
2056            return;
2057        }
2058
2059        // Lite-login (magic link) users go straight to server-side entitlement check.
2060        // They have no Cognito session in the browser, so skip client-side DynamoDB path.
2061        if (state.authLevel === 'lite') {
2062            console.log('SwiftAuth: Paywall - lite-login, checking server entitlement');
2063            await checkPaywallWalls();
2064            return;
2065        }
2066
2067        // Optimistic check: use cached subscription from localStorage
2068        const cachedSub = getCachedSubscription();
2069        if (cachedSub) {
2070            const evalResult = evaluateEntitlement(cachedSub);
2071            if (evalResult.hasActive) {
2072                console.log('SwiftAuth: Paywall - granted (cached entitlement)');
2073                updateEntitlementBanner({
2074                    status: cachedSub.status || 'unknown',
2075                    expires_at: evalResult.endDate,
2076                    plan: evalResult.tier,
2077                    is_active: true,
2078                });
2079                // Verify in background — will show paywall if cache was stale
2080                verifyPaywallInBackground();
2081                return;
2082            }
2083        }
2084
2085        // No cached entitlement (or not active) — fall through to DynamoDB check
2086        try {
2087            let hasEntitlement = false;
2088            let entitlementData = null;
2089
2090            if (state.userDataManager) {
2091                entitlementData = await state.userDataManager.getSubscriptionOnly();
2092                const evalResult = evaluateEntitlement(entitlementData);
2093                hasEntitlement = evalResult.hasActive;
2094                resolveSubscribeNavBtn(hasEntitlement);
2095
2096                if (entitlementData) {
2097                    updateEntitlementBanner({
2098                        status: entitlementData.status || 'unknown',
2099                        expires_at: evalResult.endDate,
2100                        plan: evalResult.tier,
2101                        is_active: hasEntitlement,
2102                    });
2103                }
2104            }
2105
2106            if (hasEntitlement) {
2107                console.log('SwiftAuth: Paywall - granted (has entitlement)');
2108                return;
2109            }
2110
2111            // No entitlement - check server-side walls for metered access
2112            console.log('SwiftAuth: Paywall - no entitlement, checking server walls');
2113            await checkPaywallWalls();
2114
2115        } catch (err) {
2116            console.error('SwiftAuth: Paywall check error:', err);
2117            // Fail-open: grant access on error
2118            console.log('SwiftAuth: Paywall - granted (error fallback)');
2119            resolveSubscribeNavBtn(false);
2120        }
2121    }
2122
2123    /**
2124     * Background verification of paywall entitlement after optimistic cache grant.
2125     * If DynamoDB says no active entitlement, shows the paywall.
2126     */
2127    function verifyPaywallInBackground() {
2128        // Wait for UserDataManager to be ready, then verify
2129        const check = async () => {
2130            if (!state.userDataManager) return;
2131            try {
2132                const entitlement = await state.userDataManager.getSubscriptionOnly(true); // force refresh
2133                const evalResult = evaluateEntitlement(entitlement);
2134                resolveSubscribeNavBtn(evalResult.hasActive);
2135                if (!evalResult.hasActive) {
2136                    console.log('SwiftAuth: Paywall - cached entitlement was stale, checking server walls');
2137                    await checkPaywallWalls();
2138                }
2139            } catch (err) {
2140                // Fail-open on background check errors (user already has access from cache)
2141                console.warn('SwiftAuth: Background paywall verification failed:', err);
2142            }
2143        };
2144
2145        // If UserDataManager is already ready, check immediately
2146        if (state.userDataManagerReady) {
2147            check();
2148        } else {
2149            // Otherwise wait for it via a polling interval
2150            const interval = setInterval(() => {
2151                if (state.userDataManagerReady) {
2152                    clearInterval(interval);
2153                    check();
2154                }
2155            }, 500);
2156            // Give up after 15 seconds
2157            setTimeout(() => clearInterval(interval), 15000);
2158        }
2159    }
2160
2161    /**
2162     * Get/update anonymous paywall meter from localStorage
2163     * Tracks unique article views per period, resets after periodDays
2164     */
2165    function getAnonymousPaywallMeter() {
2166        const STORAGE_KEY = 'swift_paywall_meter';
2167        const rawPostId = getPostId();
2168        // Ensure postId is a valid number, ignore NaN
2169        const postId = (typeof rawPostId === 'number' && !isNaN(rawPostId)) ? rawPostId : null;
2170
2171        let meter;
2172        try {
2173            meter = JSON.parse(localStorage.getItem(STORAGE_KEY));
2174        } catch (e) {
2175            meter = null;
2176        }
2177
2178        // Validate meter structure. If posts array is missing or invalid, treat as new/reset.
2179        // Calendar-month reset: compare YYYY-MM of meter start vs now
2180        const nowMonth = new Date().toISOString().slice(0, 7);
2181        const meterMonth = meter && meter.started ? new Date(meter.started).toISOString().slice(0, 7) : null;
2182        if (!meter || !meter.started || !Array.isArray(meter.posts) || nowMonth !== meterMonth) {
2183            // New calendar month, missing data, or corrupted — reset meter
2184            const firstOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime();
2185            meter = { count: 0, posts: [], started: firstOfMonth };
2186        }
2187
2188        // Increment for new unique article
2189        if (postId !== null) {
2190            // Use loose check or explicit Number conversion to be safe against type mismatches
2191            const alreadyViewed = meter.posts.some(id => Number(id) === postId);
2192            
2193            if (!alreadyViewed) {
2194                meter.count++;
2195                meter.posts.push(postId);
2196                // Cap stored post IDs at 200
2197                if (meter.posts.length > 200) {
2198                    meter.posts = meter.posts.slice(-200);
2199                }
2200            }
2201        }
2202
2203        localStorage.setItem(STORAGE_KEY, JSON.stringify(meter));
2204        return meter.count;
2205    }
2206
2207    /**
2208     * Determine which paywall wall to show based on count (client-side)
2209     * Mirrors server-side determine_wall() logic
2210     */
2211    function determinePaywallWall(count) {
2212        const walls = config.paywall?.walls || [];
2213        if (!walls.length) return null;
2214
2215        // Filter enabled walls, sort by threshold descending
2216        const enabled = walls
2217            .map((w, i) => ({ ...w, originalIndex: i }))
2218            .filter(w => w.enabled !== false)
2219            .sort((a, b) => (b.threshold || 0) - (a.threshold || 0));
2220
2221        for (const wall of enabled) {
2222            if (count >= (wall.threshold || 0)) {
2223                return wall;
2224            }
2225        }
2226        return null;
2227    }
2228
2229    /**
2230     * Check paywall for anonymous users entirely client-side
2231     * Uses localStorage meter + wall config from swiftAuthConfig
2232     */
2233    function checkPaywallAnonymous() {
2234        const count = getAnonymousPaywallMeter();
2235        const wall = determinePaywallWall(count);
2236
2237        if (!wall) {
2238            console.log('SwiftAuth: Paywall - no wall triggered, count:', count);
2239            return;
2240        }
2241
2242        console.log('SwiftAuth: Paywall - wall triggered, type:', wall.type, 'threshold:', wall.threshold, 'count:', count);
2243        const html = wall.html || null;
2244        const wallType = wall.type || 'hard';
2245        const shouldBlur = wallType === 'hard';
2246
2247        showPaywall(html, { access: wallType === 'hard' ? 'blocked' : 'metered' }, wallType, shouldBlur);
2248    }
2249
2250    /**
2251     * Check paywall walls via AJAX (server-side metering with multi-wall support)
2252     * Called for authenticated users without entitlement
2253     */
2254    async function checkPaywallWalls() {
2255        try {
2256            const magicSession = getMagicToken();
2257            const body = {
2258                article_url: window.location.href,
2259                post_id: getPostId(),
2260            };
2261            if (magicSession) {
2262                body.magic_token = magicSession.raw;
2263            }
2264            const response = await ajaxRequest('swift_paywall_check', body);
2265
2266            if (!response.success) {
2267                console.error('SwiftAuth: Paywall wall check failed', response.error);
2268                return;
2269            }
2270
2271            const { paywall, wall_html, wall_type, should_blur } = response;
2272
2273            if (paywall.should_show && wall_html) {
2274                const wt = wall_type || paywall.wall_type;
2275                const paywallCreativeSlot = 'paywall_' + (wt || 'hard') + '_' + ((paywall.wall_index || 0) + 1);
2276                showPaywall(wall_html, paywall, wt, should_blur, paywallCreativeSlot);
2277
2278                // Track paywall shown (client-side)
2279                if (window.SwiftAnalytics) {
2280                    if (wt === 'subscription_required') {
2281                        window.SwiftAnalytics.trackSubscriptionRequiredShown(paywall.used || 0);
2282                    } else {
2283                        window.SwiftAnalytics.trackPaywallShown(
2284                            paywall.wall_index || 0,
2285                            wt,
2286                            paywall.remaining || 0,
2287                            state.authLevel || 'anonymous'
2288                        );
2289                    }
2290                    window.SwiftAnalytics.trackWallSelectPromotion(wt, 'Shown', paywallCreativeSlot, !!config.paywall?.enabled);
2291                }
2292
2293                // Record wall impression for authenticated users
2294                if (state.userDataManager && state.authLevel === 'full') {
2295                    const wallLevel = paywall.wall_index != null ? paywall.wall_index + 1 : 1;
2296                    state.userDataManager.recordWallEvent('paywall', wall_type || paywall.wall_type || 'hard', 'shown', wallLevel);
2297                }
2298            } else if (paywall.access === 'blocked') {
2299                // Blocked but no wall HTML from server - use default
2300                showPaywall(null, paywall, 'hard', true);
2301            }
2302            // If access is 'metered' and no wall to show, user reads freely
2303        } catch (err) {
2304            console.error('SwiftAuth: Paywall wall check error:', err);
2305            // Fail-open: grant access on error
2306        }
2307    }
2308
2309    /**
2310     * Show login required modal or inline message
2311     */
2312    function showLoginRequired(html) {
2313        // Store current article URL to redirect back after login (only if on an article page)
2314        const article = document.querySelector('.p402_premium');
2315        if (article) {
2316            setReturnUrl(window.location.href);
2317        }
2318
2319        const displayMode = config.paywall?.displayMode || 'modal';
2320        const defaultHtml = html || `
2321            <div class="swift-login-required-message">
2322                <h2>Sign in to continue reading</h2>
2323                <p>Create a free account or sign in to access this content.</p>
2324                <a href="/authentication/" class="swift-btn-primary">Sign In</a>
2325            </div>
2326        `;
2327
2328        if (displayMode === 'inline') {
2329            // Inline mode: insert login message after first ad, hide remaining content
2330            const article = document.querySelector('.p402_premium');
2331            if (article) {
2332                const loginWall = document.createElement('div');
2333                loginWall.className = 'swift-paywall-inline';
2334                loginWall.innerHTML = defaultHtml;
2335                insertWallAfterFirstAd(article, loginWall, !!config.paywall?.inlineBlur);
2336            }
2337            // Ensure modal container stays hidden in inline mode
2338            const container = document.getElementById('swift-paywall-container');
2339            if (container) container.style.display = 'none';
2340        } else if (displayMode === 'bottom-sheet') {
2341            const lrArticle = document.querySelector('.p402_premium');
2342            let sheet = document.querySelector('.swift-paywall-bottom-sheet');
2343            if (!sheet) {
2344                sheet = document.createElement('div');
2345                sheet.className = 'swift-bottom-sheet swift-paywall-bottom-sheet';
2346                document.body.appendChild(sheet);
2347            }
2348            sheet.setAttribute('data-wall-type', 'hard');
2349            sheet.innerHTML = `<div class="swift-bottom-sheet-handle"></div><div class="swift-bottom-sheet-content">${splitBottomSheetContent(defaultHtml)}</div>`;
2350            const lrFirstAd = lrArticle ? lrArticle.querySelector('.module-ad') : null;
2351            waitForScrollPastFirstAd(lrFirstAd, function() {
2352                const sentinel = document.createElement('div');
2353                sentinel.className = 'swift-bottom-sheet-sentinel';
2354                sentinel.style.cssText = 'height:0;overflow:hidden;margin:0;padding:0;';
2355                if (lrArticle) insertWallAfterFirstAd(lrArticle, sentinel, true);
2356                sheet.classList.add('swift-bottom-sheet-active');
2357            });
2358            const container = document.getElementById('swift-paywall-container');
2359            if (container) container.style.display = 'none';
2360        } else {
2361            // Modal mode: show overlay
2362            const container = document.getElementById('swift-paywall-container');
2363            if (!container) return;
2364
2365            container.innerHTML = `<div class="swift-paywall-modal swift-login-required">${defaultHtml}</div>`;
2366            container.style.display = 'block';
2367
2368            // Lock content (remove from DOM and prevent scrolling)
2369            lockContent();
2370        }
2371    }
2372
2373    /**
2374     * Show paywall modal or inline message
2375     * Supports soft walls (dismissable) and hard walls (blocks content)
2376     *
2377     * @param {string|null} html - Wall HTML content
2378     * @param {object} paywallData - Paywall status from server
2379     * @param {string} wallType - 'soft', 'hard', or 'subscription_required' (default: 'hard')
2380     * @param {boolean} shouldBlur - Whether to blur/lock content
2381     * @param {string} creativeSlot - Wall identifier for GA4 select_promotion events
2382     */
2383    function showPaywall(html, paywallData, wallType, shouldBlur, creativeSlot) {
2384        // Default to hard wall behavior for backward compatibility
2385        wallType = wallType || 'hard';
2386        shouldBlur = shouldBlur !== undefined ? shouldBlur : (wallType === 'hard' || wallType === 'subscription_required');
2387
2388        if (wallType === 'hard' || wallType === 'subscription_required') {
2389            window.dispatchEvent(new CustomEvent('swiftauth:paywall-hard', { detail: { wallType } }));
2390        }
2391
2392        // Store current article URL so user returns here after login
2393        const article = document.querySelector('.p402_premium');
2394        if (article) {
2395            setReturnUrl(window.location.href);
2396        }
2397
2398        const displayMode = config.paywall?.displayMode || 'modal';
2399        const subscribeUrl = (config.paywall.redirectUrl || '/subscribe/') + '?return_to=' + encodeURIComponent(window.location.href);
2400        // Set cookie so subscribe page knows which article to return to
2401        document.cookie = 'swift_subscribe_return_to=' + encodeURIComponent(window.location.href) + ';path=/;max-age=3600;SameSite=Lax';
2402
2403        const fallbackHtml = wallType === 'subscription_required'
2404            ? `<div class="swift-subscription-required">
2405                <h2>Subscription Required</h2>
2406                <p>You've used all your free articles. Subscribe to continue reading.</p>
2407                <a href="${subscribeUrl}" class="swift-btn-primary" data-subscribe-redirect>Subscribe</a>
2408               </div>`
2409            : wallType === 'hard'
2410            ? `<div class="swift-paywall-message">
2411                <h2>Subscribe to continue reading</h2>
2412                <p>You've used all your free articles this month.</p>
2413                <a href="${subscribeUrl}" class="swift-btn-primary" data-subscribe-redirect>Subscribe Now</a>
2414               </div>`
2415            : `<div class="swift-paywall-message">
2416                <button data-swift-close type="button">&times;</button>
2417                <h2>Enjoying our content?</h2>
2418                <p>Subscribe to get unlimited access.</p>
2419                <a href="${subscribeUrl}" class="swift-btn-primary" data-subscribe-redirect>Subscribe Now</a>
2420               </div>`;
2421
2422        const content = html || fallbackHtml;
2423
2424        if (displayMode === 'inline') {
2425            const article = document.querySelector('.p402_premium');
2426            if (article) {
2427                const isHard = wallType === 'hard' || wallType === 'subscription_required';
2428                const wallClass = isHard ? 'swift-paywall-inline-hard' : 'swift-paywall-inline-soft';
2429
2430                let inlineWall = article.querySelector('.' + wallClass);
2431                if (!inlineWall) {
2432                    inlineWall = document.createElement('div');
2433                    inlineWall.className = wallClass;
2434                    inlineWall.setAttribute('data-wall-type', wallType);
2435                    inlineWall.setAttribute('data-wall-name', creativeSlot || '');
2436                }
2437                inlineWall.innerHTML = content;
2438
2439                // Insert after first .module-ad and hide remaining content
2440                const restoreContent = insertWallAfterFirstAd(article, inlineWall, !!config.paywall?.inlineBlur);
2441
2442                // Add close handler for soft walls
2443                if (!isHard) {
2444                    const closeBtn = inlineWall.querySelector('[data-swift-close], [data-dismiss="modal"], .close');
2445                    if (closeBtn) {
2446                        closeBtn.addEventListener('click', (e) => {
2447                            e.preventDefault();
2448                            console.log('SwiftAuth: Paywall soft wall dismissed');
2449                            if (window.SwiftAnalytics) {
2450                                window.SwiftAnalytics.trackPaywallDismissed(paywallData?.wall_index || 0);
2451                                window.SwiftAnalytics.trackWallSelectPromotion(paywallData?.wall_type || 'soft', 'ClickClose', creativeSlot, !!config.paywall?.enabled);
2452                            }
2453                            restoreContent();
2454                            inlineWall.remove();
2455                        });
2456                    }
2457                }
2458            }
2459            const container = document.getElementById('swift-paywall-container');
2460            if (container) container.style.display = 'none';
2461        } else if (displayMode === 'bottom-sheet') {
2462            // Bottom-sheet mode: fixed sheet slides up from bottom, content stays visible
2463            const pwArticle = document.querySelector('.p402_premium');
2464            const isHardPaywall = wallType === 'hard' || wallType === 'subscription_required';
2465            let restoreContent = function() {};
2466
2467            let sheet = document.querySelector('.swift-paywall-bottom-sheet');
2468            if (!sheet) {
2469                sheet = document.createElement('div');
2470                sheet.className = 'swift-bottom-sheet swift-paywall-bottom-sheet';
2471                document.body.appendChild(sheet);
2472            }
2473            sheet.setAt
2473tribute('data-wall-type', wallType);
2474            sheet.setAttribute('data-wall-name', creativeSlot || '');
2475            sheet.innerHTML = `<div class="swift-bottom-sheet-handle"></div><div class="swift-bottom-sheet-content">${splitBottomSheetContent(content)}</div>`;
2476
2477            if (!isHardPaywall) {
2478                const closeBtn = sheet.querySelector('[data-swift-close], [data-dismiss="modal"], .close');
2479                if (closeBtn) {
2480                    closeBtn.addEventListener('click', (e) => {
2481                        e.preventDefault();
2482                        console.log('SwiftAuth: Paywall bottom sheet dismissed');
2483                        if (window.SwiftAnalytics) {
2484                            window.SwiftAnalytics.trackPaywallDismissed(paywallData?.wall_index || 0);
2485                            window.SwiftAnalytics.trackWallSelectPromotion(paywallData?.wall_type || 'soft', 'ClickClose', creativeSlot, !!config.paywall?.enabled);
2486                        }
2487                        restoreContent();
2488                        hideBottomSheet(sheet);
2489                    });
2490                }
2491            }
2492
2493            const pwFirstAd = pwArticle ? pwArticle.querySelector('.module-ad') : null;
2494            waitForScrollPastFirstAd(pwFirstAd, function() {
2495                if (pwArticle && shouldBlur) {
2496                    const sentinel = document.createElement('div');
2497                    sentinel.className = 'swift-bottom-sheet-sentinel';
2498                    sentinel.style.cssText = 'height:0;overflow:hidden;margin:0;padding:0;';
2499                    restoreContent = insertWallAfterFirstAd(pwArticle, sentinel, true);
2500                }
2501                sheet.classList.add('swift-bottom-sheet-active');
2502            });
2503
2504            const container = document.getElementById('swift-paywall-container');
2505            if (container) container.style.display = 'none';
2506        } else {
2507            // Modal mode
2508            const container = document.getElementById('swift-paywall-container');
2509            if (!container) return;
2510
2511            container.innerHTML = `<div class="swift-paywall-modal">${content}</div>`;
2512            container.setAttribute('data-wall-type', wallType);
2513            container.setAttribute('data-wall-name', creativeSlot || '');
2514
2515            const isHardPaywall = wallType === 'hard' || wallType === 'subscription_required';
2516            if (!isHardPaywall) {
2517                // Soft walls: attach close handler now (container is in DOM, just hidden)
2518                const closeBtn = container.querySelector('[data-swift-close], [data-dismiss="modal"], .close');
2519                if (closeBtn) {
2520                    closeBtn.addEventListener('click', (e) => {
2521                        e.preventDefault();
2522                        console.log('SwiftAuth: Paywall soft wall dismissed via close button');
2523                        if (window.SwiftAnalytics) {
2524                            window.SwiftAnalytics.trackPaywallDismissed(paywallData?.wall_index || 0);
2525                            window.SwiftAnalytics.trackWallSelectPromotion(paywallData?.wall_type || 'soft', 'ClickClose', creativeSlot, !!config.paywall?.enabled);
2526                        }
2527                        hidePaywall();
2528                    });
2529                }
2530
2531                // Allow clicking overlay to dismiss soft walls
2532                const overlay = document.querySelector('.swift-wall-overlay');
2533                if (overlay) {
2534                    overlay.addEventListener('click', (e) => {
2535                        if (e.target === overlay) {
2536                            console.log('SwiftAuth: Paywall soft wall dismissed via overlay click');
2537                            hidePaywall();
2538                        }
2539                    });
2540                }
2541            }
2542
2543            // Defer visibility until user scrolls past the first ad
2544            const paywallArticle = document.querySelector('.p402_premium');
2545            const firstAd = paywallArticle ? paywallArticle.querySelector('.module-ad') : null;
2546            waitForScrollPastFirstAd(firstAd, function() {
2547                container.style.display = 'block';
2548                requestAnimationFrame(function() { container.classList.add('swift-wall-active'); });
2549                if (isHardPaywall) {
2550                    if (paywallArticle) paywallArticle.classList.add('swift-content-hidden');
2551                    showOverlay();
2552                }
2553            });
2554        }
2555
2556        // Initialize login buttons in the newly added HTML (if any)
2557        initLoginButtons();
2558
2559        // Hide login prompts for already-authenticated users
2560        if (state.authLevel === 'full') {
2561            document.querySelectorAll('[data-swift-wall-login]').forEach(el => el.remove());
2562        }
2563
2564        // Add click handlers for subscribe buttons in the paywall
2565        // Match both data-subscribe-redirect and plain links to /subscribe/
2566        const wallLevel = paywallData?.wall_index != null ? (paywallData.wall_index + 1) : 1;
2567        const paywallContainers = document.querySelectorAll('.swift-paywall-modal, .swift-paywall-inline-hard, .swift-paywall-inline-soft, .swift-paywall, .swift-paywall-bottom-sheet, #swift-paywall-container');
2568        const subscribeButtons = new Set();
2569        paywallContainers.forEach(container => {
2570            container.querySelectorAll('[data-subscribe-redirect]').forEach(btn => subscribeButtons.add(btn));
2571            container.querySelectorAll('a[href*="/subscribe"]').forEach(btn => subscribeButtons.add(btn));
2572        });
2573        subscribeButtons.forEach(btn => {
2574            btn.addEventListener('click', (e) => {
2575                e.preventDefault();
2576                // Track subscribe button clicked
2577                if (window.SwiftAnalytics) {
2578                    const source = btn.getAttribute('data-source') || 'paywall';
2579                    window.SwiftAnalytics.trackSubscribeButtonClicked(source);
2580                    window.SwiftAnalytics.trackWallSelectPromotion(wallType || 'hard', 'ClickSubscribe', creativeSlot, !!config.paywall?.enabled);
2581                }
2582                // Record paywall CTA click for authenticated users
2583                if (state.userDataManager) {
2584                    state.userDataManager.recordWallEvent('paywall', wallType || 'hard', 'cta_clicked', wallLevel);
2585                }
2586                // Store conversion attribution
2587                setConversionSource('paywall_' + (wallType || 'hard') + '_' + wallLevel, window.location.href);
2588                window.location.href = subscribeUrl;
2589            });
2590        });
2591    }
2592
2593    /**
2594     * Hide paywall container
2595     */
2596    function hidePaywall() {
2597        const container = document.getElementById('swift-paywall-container');
2598        if (container) {
2599            container.classList.remove('swift-wall-active');
2600            container.style.display = 'none';
2601            container.innerHTML = '';
2602        }
2603        unlockContent();
2604    }
2605
2606    /**
2607     * Show meter notice (remaining free articles)
2608     * Disabled - we don't show metering messages, just grant or block access
2609     */
2610    function showMeterNotice(paywallData) {
2611        // Intentionally empty - no meter notices shown
2612        return;
2613    }
2614
2615    /**
2616     * Update entitlement banner
2617     */
2618    function updateEntitlementBanner(entitlement) {
2619        const banner = document.getElementById('swift-entitlement-banner');
2620        if (!banner) return;
2621
2622        if (entitlement.is_active) {
2623            banner.style.display = 'none';
2624        } else if (entitlement.expires_at) {
2625            const renewUrl = (config.paywall.redirectUrl || '/subscribe/') + '?return_to=' + encodeURIComponent(window.location.href);
2626            banner.innerHTML = `<p>Your subscription expired. <a href="${renewUrl}" data-conversion-source="banner_expired">Renew now</a></p>`;
2627            banner.style.display = 'block';
2628            const renewLink = banner.querySelector('[data-conversion-source]');
2629            if (renewLink) {
2630                renewLink.addEventListener('click', () => {
2631                    setConversionSource('banner_expired', window.location.href);
2632                });
2633            }
2634        }
2635    }
2636
2637    // =========================================================================
2638    // Magic Link Session (localStorage-based, cache-compatible)
2639    // =========================================================================
2640
2641    const MAGIC_TOKEN_KEY = 'swift_magic_token';
2642
2643    /**
2644     * Read and validate the magic session token from localStorage.
2645     * Returns the decoded payload if valid and not expired, otherwise null.
2646     */
2647    function getMagicToken() {
2648        try {
2649            const raw = localStorage.getItem(MAGIC_TOKEN_KEY);
2650            if (!raw) return null;
2651            const [payloadB64] = raw.split('.');
2652            if (!payloadB64) return null;
2653            const payload = JSON.parse(atob(payloadB64));
2654            if (!payload || !payload.expiresAt) return null;
2655            if (payload.expiresAt < Math.floor(Date.now() / 1000)) {
2656                localStorage.removeItem(MAGIC_TOKEN_KEY);
2657                return null;
2658            }
2659            return { payload, raw };
2660        } catch (e) {
2661            return null;
2662        }
2663    }
2664
2665    /**
2666     * Store a magic session HMAC token in localStorage.
2667     */
2668    function storeMagicToken(tokenString) {
2669        try {
2670            localStorage.setItem(MAGIC_TOKEN_KEY, tokenString);
2671        } catch (e) {
2672            console.warn('SwiftAuth: Could not store magic token in localStorage', e);
2673        }
2674    }
2675
2676    /**
2677     * Detect ?mlt= in URL, call AJAX to validate + mark used,
2678     * store returned HMAC token in localStorage, and update auth state.
2679     * Strips ?mlt= from URL silently on completion or failure.
2680     */
2681    async function redeemMagicLink() {
2682        const params = new URLSearchParams(window.location.search);
2683        const mlt = params.get('mlt');
2684        if (!mlt) return;
2685
2686        // Strip ?mlt= from URL immediately (before any async work)
2687        params.delete('mlt');
2688        const cleanSearch = params.toString();
2689        const cleanUrl = window.location.pathname + (cleanSearch ? '?' + cleanSearch : '') + window.location.hash;
2690        history.replaceState(null, '', cleanUrl);
2691
2692        try {
2693            const response = await ajaxRequest('swift_magic_redeem', { token: mlt }, 'POST');
2694
2695            if (!response || !response.success) {
2696                console.log('SwiftAuth: Magic link invalid or already used', response?.error);
2697                return;
2698            }
2699
2700            storeMagicToken(response.token);
2701            state.authLevel = 'lite';
2702            state.sub = response.sub || null;
2703            console.log('SwiftAuth: Magic session started (lite-login)', { sub: state.sub });
2704        } catch (err) {
2705            console.log('SwiftAuth: Magic link redemption failed', err);
2706        }
2707    }
2708
2709    // =========================================================================
2710    // Why Register Modal
2711    // =========================================================================
2712
2713    /**
2714     * Show the "Why Register" modal
2715     */
2716    function showWhyRegisterModal() {
2717        console.log('SwiftAuth: showWhyRegisterModal called');
2718
2719        // Remove existing modal if present
2720        hideWhyRegisterModal();
2721
2722        const html = config.whyRegisterHtml;
2723        console.log('SwiftAuth: whyRegisterHtml content:', html ? html.substring(0, 100) + '...' : '(empty)');
2724
2725        if (!html) {
2726            console.warn('SwiftAuth: No Why Register content configured in config.whyRegisterHtml');
2727            return;
2728        }
2729
2730        // Create modal overlay (z-index 10000, higher than walls at 9999)
2731        const overlay = document.createElement('div');
2732        overlay.className = 'swift-why-register-overlay';
2733        overlay.setAttribute('data-swift-why-register-overlay', '');
2734
2735        // Create modal container
2736        const modal = document.createElement('div');
2737        modal.className = 'swift-why-register-modal';
2738        modal.innerHTML = `
2739            <button type="button" class="swift-why-register-close" data-swift-why-register-close aria-label="Close">&times;</button>
2740            <div class="swift-why-register-content">${html}</div>
2741        `;
2742
2743        overlay.appendChild(modal);
2744        document.body.appendChild(overlay);
2745        console.log('SwiftAuth: Why Register modal shown');
2746
2747        // Prevent body scroll
2748        document.body.classList.add('swift-modal-open');
2749
2750        // Close on overlay click
2751        overlay.addEventListener('click', (e) => {
2752            if (e.target === overlay) {
2753                console.log('SwiftAuth: Why Register modal closed via overlay click');
2754                hideWhyRegisterModal();
2755            }
2756        });
2757
2758        // Close on button click
2759        const closeBtn = modal.querySelector('[data-swift-why-register-close]');
2760        if (closeBtn) {
2761            closeBtn.addEventListener('click', () => {
2762                console.log('SwiftAuth: Why Register modal closed via close button');
2763                hideWhyRegisterModal();
2764            });
2765        }
2766
2767        // Close on escape key
2768        document.addEventListener('keydown', handleWhyRegisterEscape);
2769    }
2770
2771    /**
2772     * Hide the "Why Register" modal
2773     */
2774    function hideWhyRegisterModal() {
2775        const overlay = document.querySelector('[data-swift-why-register-overlay]');
2776        if (overlay) {
2777            overlay.remove();
2778            console.log('SwiftAuth: Why Register modal hidden');
2779        }
2780        document.body.classList.remove('swift-modal-open');
2781        document.removeEventListener('keydown', handleWhyRegisterEscape);
2782    }
2783
2784    /**
2785     * Handle escape key for Why Register modal
2786     */
2787    function handleWhyRegisterEscape(e) {
2788        if (e.key === 'Escape') {
2789            console.log('SwiftAuth: Why Register modal closed via Escape key');
2790            hideWhyRegisterModal();
2791        }
2792    }
2793
2794    /**
2795     * Initialize Why Register button handlers using event delegation
2796     * This handles dynamically injected content (like paywall HTML)
2797     */
2798    function initWhyRegisterButtons() {
2799        // Use event delegation on document to catch dynamically added elements
2800        document.addEventListener('click', (e) => {
2801            const trigger = e.target.closest('[data-swift-why-register]');
2802            if (trigger) {
2803                // If the element has an href, let it navigate naturally (just fire GA event)
2804                if (trigger.href && trigger.tagName === 'A') {
2805                    // Fire GA event, let the link navigate
2806                    if (window.SwiftAnalytics) {
2807                        const wallContainer = trigger.closest('.swift-regwall-modal, .swift-regwall-inline, .swift-paywall-modal, .swift-paywall-inline-hard, .swift-paywall-inline-soft');
2808                        if (wallContainer) {
2809                            const wallType = wallContainer.getAttribute('data-wall-type') || 'soft';
2810                            const wallName = wallContainer.getAttribute('data-wall-name') || '';
2811                            window.SwiftAnalytics.trackWallSelectPromotion(wallType, 'ClickMore', wallName, !!config.paywall?.enabled);
2812                        }
2813                    }
2814                    return; // Let the link navigate
2815                }
2816                e.preventDefault();
2817                console.log('SwiftAuth: Why Register trigger clicked', trigger);
2818                showWhyRegisterModal();
2819            }
2820        });
2821        console.log('SwiftAuth: Why Register event delegation initialized');
2822    }
2823
2824    // =========================================================================
2825    // Developer Shortcuts
2826    // =========================================================================
2827
2828    /**
2829     * Check if element is a text input
2830     */
2831    function isTextInput(element) {
2832        if (!element || !element.tagName) {
2833            return false;
2834        }
2835        const tagName = element.tagName.toLowerCase();
2836        return tagName === 'input' || tagName === 'textarea' || element.isContentEditable;
2837    }
2838
2839    /**
2840     * Check if this is the session shortcut (Cmd + Shift + 1)
2841     */
2842    function isSessionShortcut(event) {
2843        if (!event || event.repeat || event.defaultPrevented) {
2844            return false;
2845        }
2846
2847        if (isTextInput(event.target)) {
2848            return false;
2849        }
2850
2851        const key = event.key;
2852        const modifierPressed = event.shiftKey && event.metaKey;
2853
2854        return modifierPressed && key === '1';
2855    }
2856
2857    /**
2858     * Get current regwall session count from cookie
2859     */
2860    function getSessionCount() {
2861        const pubID = config.pubID || '';
2862        const key = pubID + 'regwallMeter';
2863
2864        try {
2865            const stored = localStorage.getItem(key);
2866            if (!stored) return 0;
2867
2868            const meter = JSON.parse(stored);
2869            return meter.count || 0;
2870        } catch (err) {
2871            return 0;
2872        }
2873    }
2874
2875    /**
2876     * Set session count (updates both regwall and paywall meters)
2877     */
2878    function setSessionCount(count) {
2879        const pubID = config.pubID || '';
2880        const firstOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
2881        const firstOfMonthSec = Math.floor(firstOfMonth.getTime() / 1000);
2882        const firstOfMonthMs = firstOfMonth.getTime();
2883
2884        // Update regwall meter (cookie-based)
2885        const regwallKey = pubID + 'regwallMeter';
2886        const regwallMeter = {
2887            count: count,
2888            started_at: firstOfMonthSec,
2889            viewed_urls: []
2890        };
2891        localStorage.setItem(regwallKey, JSON.stringify(regwallMeter));
2892
2893        // Update paywall meter (localStorage-based for anonymous users)
2894        const paywallKey = 'swift_paywall_meter';
2895        const paywallMeter = {
2896            count: count,
2897            posts: [],
2898            started: firstOfMonthMs
2899        };
2900        localStorage.setItem(paywallKey, JSON.stringify(paywallMeter));
2901
2902        // Also update via AJAX to sync server-side cookie
2903        ajaxRequest('swift_regwall_set_count', { count: count }).catch(err => {
2904            console.log('SwiftAuth: Could not sync session count to server', err);
2905        });
2906
2907        console.log('SwiftAuth: Session count set to', count, '(both regwall and paywall meters)');
2908    }
2909
2910    /**
2911     * Handle developer keyboard shortcuts
2912     */
2913    function handleDeveloperShortcuts(event) {
2914        if (!isSessionShortcut(event)) {
2915            return;
2916        }
2917
2918        event.preventDefault();
2919
2920        const currentCount = getSessionCount();
2921        const userInput = window.prompt('Set Swift session count (integer ≥ 0)', currentCount);
2922
2923        if (userInput === null) {
2924            return; // User cancelled
2925        }
2926
2927        const parsedCount = parseInt(userInput, 10);
2928        if (Number.isNaN(parsedCount) || parsedCount < 0) {
2929            window.alert('Session count must be a number greater than or equal to 0.');
2930            return;
2931        }
2932
2933        setSessionCount(parsedCount);
2934
2935        // Reload page to trigger wall re-evaluation with new count
2936        setTimeout(() => {
2937            window.location.reload();
2938        }, 500);
2939    }
2940
2941    /**
2942     * Initialize developer shortcuts
2943     */
2944    function initDeveloperShortcuts() {
2945        document.addEventListener('keydown', handleDeveloperShortcuts);
2946        console.log('SwiftAuth: Developer shortcuts initialized (Cmd+Shift+1)');
2947    }
2948
2949    // =========================================================================
2950    // Bookmarks (Direct DynamoDB when available, AJAX fallback)
2951    // =========================================================================
2952
2953    /**
2954     * Add bookmark
2955     * Uses direct DynamoDB when UserDataManager is ready
2956     */
2957    async function addBookmark(articleUrlOrPostId, metadata = {}) {
2958        if (state.authLevel !== 'full') {
2959            throw new Error('Authentication required');
2960        }
2961
2962        const meta = getArticleMeta();
2963        // Support both post ID and URL - if it looks like a number, treat as post ID
2964        const isPostId = /^\d+$/.test(articleUrlOrPostId);
2965        const postId = metadata.postId || (isPostId ? articleUrlOrPostId : null) || articleUrlOrPostId?.replace(/[^a-zA-Z0-9]/g, '-') || getArticleUrl().replace(/[^a-zA-Z0-9]/g, '-');
2966        const url = metadata.url || (isPostId ? getArticleUrl() : articleUrlOrPostId) || getArticleUrl();
2967
2968        const bookmarkData = {
2969            postId: postId,
2970            articleTitle: metadata.title || meta.title,
2971            articleUrl: url,
2972            articleImage: metadata.image || meta.image,
2973        };
2974
2975        // Direct DynamoDB only - no AJAX fallback for user data
2976        if (!isDirectDynamoEnabled()) {
2977            console.error('SwiftAuth: DynamoDB not initialized - cannot add bookmark');
2978            return { success: false, error: 'DynamoDB not initialized' };
2979        }
2980
2981        try {
2982            const result = await state.userDataManager.addBookmark(bookmarkData);
2983            return { success: true, bookmarked: true, bookmark: result };
2984        } catch (error) {
2985            console.error('SwiftAuth: addBookmark failed:', error);
2986            return { success: false, error: error.message };
2987        }
2988    }
2989
2990    /**
2991     * Remove bookmark
2992     * Uses direct DynamoDB when UserDataManager is ready
2993     */
2994    async function removeBookmark(articleUrlOrPostId) {
2995        if (state.authLevel !== 'full') {
2996            throw new Error('Authentication required');
2997        }
2998
2999        // Support both post ID and URL - if it looks like a number, treat as post ID
3000        const isPostId = /^\d+$/.test(articleUrlOrPostId);
3001        const postId = isPostId ? articleUrlOrPostId : (articleUrlOrPostId || getArticleUrl()).replace(/[^a-zA-Z0-9]/g, '-');
3002        const url = isPostId ? getArticleUrl() : (articleUrlOrPostId || getArticleUrl());
3003
3004        // Direct DynamoDB only - no AJAX fallback for user data
3005        if (!isDirectDynamoEnabled()) {
3006            console.error('SwiftAuth: DynamoDB not initialized - cannot remove bookmark');
3007            return { success: false, error: 'DynamoDB not initialized' };
3008        }
3009
3010        try {
3011            await state.userDataManager.removeBookmark(postId);
3012            return { success: true, bookmarked: false };
3013        } catch (error) {
3014            console.error('SwiftAuth: removeBookmark failed:', error);
3015            return { success: false, error: error.message };
3016        }
3017    }
3018
3019    /**
3020     * Toggle bookmark
3021     */
3022    async function toggleBookmark(articleUrlOrPostId) {
3023        console.log('SwiftAuth: toggleBookmark called with:', articleUrlOrPostId);
3024
3025        const checkResponse = await checkBookmark(articleUrlOrPostId);
3026        console.log('SwiftAuth: checkBookmark response:', checkResponse);
3027
3028        if (checkResponse.bookmarked) {
3029            console.log('SwiftAuth: Removing bookmark...');
3030            return removeBookmark(articleUrlOrPostId);
3031        } else {
3032            console.log('SwiftAuth: Adding bookmark...');
3033            return addBookmark(articleUrlOrPostId);
3034        }
3035    }
3036
3037    /**
3038     * Check if article is bookmarked
3039     * Uses direct DynamoDB when UserDataManager is ready
3040     * @param {string} articleUrlOrPostId - Article URL or numeric post ID
3041     */
3042    async function checkBookmark(articleUrlOrPostId) {
3043        // Support both post ID and URL - if it looks like a number, treat as post ID
3044        const isPostId = /^\d+$/.test(articleUrlOrPostId);
3045        const postId = isPostId ? articleUrlOrPostId : (articleUrlOrPostId || getArticleUrl()).replace(/[^a-zA-Z0-9]/g, '-');
3046        const url = isPostId ? getArticleUrl() : (articleUrlOrPostId || getArticleUrl());
3047
3048        console.log('SwiftAuth: checkBookmark called', { articleUrlOrPostId, isPostId, postId, dynamoEnabled: isDirectDynamoEnabled() });
3049
3050        // Direct DynamoDB only - no AJAX fallback for user data
3051        if (!isDirectDynamoEnabled()) {
3052            console.log('SwiftAuth: DynamoDB not enabled, returning false');
3053            return { success: true, bookmarked: false, reason: 'not_initialized' };
3054        }
3055
3056        try {
3057            const isBookmarked = await state.userDataManager.isBookmarked(postId);
3058            console.log('SwiftAuth: isBookmarked result for postId', postId, '=', isBookmarked);
3059            return { success: true, bookmarked: isBookmarked };
3060        } catch (error) {
3061            console.error('SwiftAuth: checkBookmark failed:', error);
3062            return { success: false, bookmarked: false, error: error.message };
3063        }
3064    }
3065
3066    /**
3067     * Get all bookmarks
3068     * Uses direct DynamoDB when UserDataManager is ready
3069     */
3070    async function getBookmarks() {
3071        // Direct DynamoDB only - no AJAX fallback for user data
3072        if (!isDirectDynamoEnabled()) {
3073            return { success: true, bookmarks: [] };
3074        }
3075
3076        try {
3077            const bookmarks = await state.userDataManager.getBookmarks();
3078            return { success: true, bookmarks: bookmarks || [] };
3079        } catch (error) {
3080            console.error('SwiftAuth: getBookmarks failed:', error);
3081            return { success: false, bookmarks: [], error: error.message };
3082        }
3083    }
3084
3085    // =========================================================================
3086    // Profile Page
3087    // =========================================================================
3088
3089    /**
3090     * Get user profile
3091     * Uses direct DynamoDB when UserDataManager is ready
3092     */
3093    async function getProfile() {
3094        // Direct DynamoDB only - no AJAX fallback for user data
3095        if (!isDirectDynamoEnabled()) {
3096            const diagnostics = getDynamoDBDiagnostics();
3097            console.error('SwiftAuth: DynamoDB not initialized - cannot get profile');
3098            console.error('SwiftAuth: Diagnostics:', diagnostics);
3099            console.error('SwiftAuth: Likely cause:', diagnostics.likelyCause);
3100            return { success: false, error: diagnostics.likelyCause, profile: null };
3101        }
3102
3103        try {
3104            const profile = await state.userDataManager.getProfile();
3105            const bookmarks = await state.userDataManager.getBookmarks();
3106            const topics = await state.userDataManager.getFollowedTopics();
3107            const pageviews = await state.userDataManager.getPageviews();
3108            const subscription = await state.userDataManager.getSubscription();
3109
3110            // Debug logging - raw DynamoDB data
3111            console.log('SwiftAuth: Raw DynamoDB profile:', profile);
3112            console.log('SwiftAuth: Raw DynamoDB bookmarks:', bookmarks);
3113            console.log('SwiftAuth: Raw DynamoDB topics:', topics);
3114            console.log('SwiftAuth: Raw DynamoDB pageviews (reading history):', pageviews);
3115            console.log('SwiftAuth: Raw DynamoDB subscription:', subscription);
3116
3117            // Build profile response
3118            const fullProfile = {
3119                ...profile,
3120                bookmarks: bookmarks || [],
3121                followedTopics: topics || [],
3122                readingHistory: pageviews || [],
3123                entitlement: subscription ? {
3124                    status: subscription.status,
3125                    tier: subscription.tier,
3126                    endDate: subscription.endDate,
3127                    autoRenew: subscription.autoRenew,
3128                    stripeSubscriptionId: subscription.stripeSubscriptionId,
3129                } : null,
3130                localStorageMigrated: profile?.localStorageMigrated || false,
3131            };
3132
3133            // Store profile in state for other functions to access
3134            state.profile = fullProfile;
3135
3136            // Sync trackReadingHistory preference to localStorage
3137            if (profile && profile.trackReadingHistory !== undefined) {
3138                const pubID = config.pubID || '';
3139                const key = pubID + 'readingHistorySetting';
3140                localStorage.setItem(key, profile.trackReadingHistory.toString());
3141            }
3142
3143            return {
3144                success: true,
3145                profile: fullProfile
3146            };
3147        } catch (error) {
3148            console.error('SwiftAuth: getProfile failed:', error);
3149            return { success: false, error: error.message, profile: null };
3150        }
3151    }
3152
3153    /**
3154     * Update user profile
3155     * Uses direct DynamoDB when UserDataManager is ready
3156     */
3157    async function updateProfile(updates) {
3158        // Direct DynamoDB only - no AJAX fallback for user data
3159        if (!isDirectDynamoEnabled()) {
3160            console.error('SwiftAuth: DynamoDB not initialized - cannot update profile');
3161            return { success: false, error: 'DynamoDB not initialized' };
3162        }
3163
3164        try {
3165            await state.userDataManager.updateProfile(updates);
3166            return { success: true, updated: updates };
3167        } catch (error) {
3168            console.error('SwiftAuth: updateProfile failed:', error);
3169            return { success: false, error: error.message };
3170        }
3171    }
3172
3173    /**
3174     * Get reading history from localStorage (legacy fallback)
3175     */
3176    function getLocalStorageReadingHistory(limit = 20) {
3177        const pubID = config.pubID || '';
3178        const key = pubID + 'pageViews';
3179
3180        try {
3181            const stored = localStorage.getItem(key);
3182            if (!stored) return [];
3183
3184            let pageviews;
3185            try {
3186                pageviews = JSON.parse(stored);
3187            } catch (parseErr) {
3188                // Attempt to salvage truncated JSON (e.g. localStorage hit size limit)
3189                console.warn('SwiftAuth: pageViews JSON is malformed, attempting repair');
3190                pageviews = repairTruncatedJsonArray(stored);
3191            }
3192            console.log('SwiftAuth: Reading history loaded:', pageviews?.length || 0, 'items');
3193
3194            if (Array.isArray(pageviews)) {
3195                return pageviews.slice(0, limit);
3196            }
3197            return [];
3198        } catch (err) {
3199            console.warn('SwiftAuth: Could not read localStorage reading history', err);
3200            return [];
3201        }
3202    }
3203
3204    /**
3205     * Attempt to repair a truncated JSON array by finding the last complete object.
3206     * Handles cases where localStorage data was cut off mid-write.
3207     * @param {string} str - Malformed JSON string
3208     * @returns {Array} - Array of successfully parsed objects, or []
3209     */
3210    function repairTruncatedJsonArray(str) {
3211        if (!str || !str.startsWith('[')) return [];
3212
3213        // Find the last complete object by looking for the last "},"
3214        // or "}" followed by "]"
3215        const lastCompleteComma = str.lastIndexOf('},');
3216        const lastCompleteBracket = str.lastIndexOf('}]');
3217
3218        const cutPoint = Math.max(lastCompleteComma, lastCompleteBracket);
3219        if (cutPoint === -1) return [];
3220
3221        // Slice up to and including the "}", then close the array
3222        const repaired = str.substring(0, cutPoint + 1) + ']';
3223
3224        try {
3225            const result = JSON.parse(repaired);
3226            if (Array.isArray(result)) {
3227                console.log('SwiftAuth: Repaired truncated JSON, recovered', result.length, 'items');
3228                return result;
3229            }
3230        } catch (e) {
3231            console.warn('SwiftAuth: JSON repair failed', e);
3232        }
3233        return [];
3234    }
3235
3236    /**
3237     * Get followed topics from localStorage (legacy fallback)
3238     */
3239    function getLocalStorageFollowedTopics() {
3240        const pubID = config.pubID || '';
3241        const key = pubID + 'followedTopics';
3242
3243        try {
3244            const stored = localStorage.getItem(key);
3245            if (!stored) return [];
3246
3247            let topics;
3248            try {
3249                topics = JSON.parse(stored);
3250            } catch (parseErr) {
3251                console.warn('SwiftAuth: followedTopics JSON is malformed, attempting repair');
3252                topics = repairTruncatedJsonArray(stored);
3253            }
3254            console.log('SwiftAuth: Followed topics loaded:', topics?.length || 0, 'items');
3255
3256            if (Array.isArray(topics)) {
3257                return topics;
3258            }
3259            return [];
3260        } catch (err) {
3261            console.warn('SwiftAuth: Could not read localStorage followed topics', err);
3262            return [];
3263        }
3264    }
3265
3266    /**
3267     * Get profile data from localStorage (legacy fallback)
3268     */
3269    function getLocalStorageProfile() {
3270        const pubID = config.pubID || '';
3271        const key = pubID + 'Profile';
3272
3273        try {
3274            const stored = localStorage.getItem(key);
3275            if (!stored) return null;
3276
3277            return JSON.parse(stored);
3278        } catch (err) {
3279            console.warn('SwiftAuth: Could not read localStorage profile', err);
3280            return null;
3281        }
3282    }
3283
3284    /**
3285     * Save profile data to localStorage
3286     * Skipped if user has been migrated to DynamoDB
3287     */
3288    function saveLocalStorageProfile(profile) {
3289        // Skip localStorage writes if user has migrated to DynamoDB
3290        if (state.localStorageMigrated) {
3291            return true;
3292        }
3293
3294        const pubID = config.pubID || '';
3295        const key = pubID + 'Profile';
3296
3297        try {
3298            localStorage.setItem(key, JSON.stringify(profile));
3299            return true;
3300        } catch (err) {
3301            console.warn('SwiftAuth: Could not save localStorage profile', err);
3302            return false;
3303        }
3304    }
3305
3306    /**
3307     * Check if site is a paywall site
3308     */
3309    function isPaywallSite() {
3310        return config.paywall?.enabled === true;
3311    }
3312
3313    /**
3314     * Migrate localStorage data to DynamoDB
3315     * @returns {Promise<object>} Migration result
3316     */
3317    async function migrateLocalStorageToDb() {
3318        // Direct DynamoDB only - no AJAX
3319        if (!isDirectDynamoEnabled()) {
3320            console.error('SwiftAuth: DynamoDB not initialized - cannot migrate');
3321            return { success: false, error: 'DynamoDB not initialized' };
3322        }
3323
3324        const bookmarks = getLocalStorageBookmarks();
3325        const readingHistory = getLocalStorageReadingHistory(50);
3326        const followedTopics = getLocalStorageFollowedTopics();
3327
3328        // Check if there's anything to migrate
3329        if (bookmarks.length === 0 && readingHistory.length === 0 && followedTopics.length === 0) {
3330            console.log('SwiftAuth: No localStorage data to migrate');
3331            // Mark as migrated in profile
3332            await state.userDataManager.updateProfile({ localStorageMigrated: true });
3333            return { success: true, migrated: false, message: 'Nothing to migrate' };
3334        }
3335
3336        console.log('SwiftAuth: Migrating localStorage data to DynamoDB', {
3337            bookmarks: bookmarks.length,
3338            readingHistory: readingHistory.length,
3339            followedTopics: followedTopics.length
3340        });
3341
3342        const counts = { bookmarks: 0, readingHistory: 0, followedTopics: 0 };
3343
3344        try {
3345            // Migrate bookmarks
3346            for (const bookmark of bookmarks) {
3347                try {
3348                    await state.userDataManager.addBookmark(bookmark);
3349                    counts.bookmarks++;
3350                } catch (err) {
3351                    console.warn('SwiftAuth: Failed to migrate bookmark:', err);
3352                }
3353            }
3354
3355            // Migrate followed topics
3356            for (const topic of followedTopics) {
3357                try {
3358                    await state.userDataManager.followTopic(topic);
3359                    counts.followedTopics++;
3360                } catch (err) {
3361                    console.warn('SwiftAuth: Failed to migrate topic:', err);
3362                }
3363            }
3364
3365            // Migrate reading history
3366            for (const pageview of readingHistory) {
3367                try {
3368                    await state.userDataManager.recordPageview(pageview);
3369                    counts.readingHistory++;
3370                } catch (err) {
3371                    console.warn('SwiftAuth: Failed to migrate pageview:', err);
3372                }
3373            }
3374
3375            // Mark migration complete in profile
3376            await state.userDataManager.updateProfile({ localStorageMigrated: true });
3377
3378            console.log('SwiftAuth: Migration successful', counts);
3379            // Clear localStorage after successful migration
3380            clearMigratedLocalStorage();
3381
3382            return { success: true, migrated: true, counts };
3383        } catch (err) {
3384            console.error('SwiftAuth: Migration failed', err);
3385            return { success: false, error: err.message };
3386        }
3387    }
3388
3389    /**
3390     * Clear localStorage data after successful migration
3391     */
3392    function clearMigratedLocalStorage() {
3393        const pubID = config.pubID || '';
3394
3395        try {
3396            // Clear bookmarks
3397            localStorage.removeItem(pubID + 'bookmarkedPosts');
3398            // Clear reading history (both legacy key names)
3399            localStorage.removeItem(pubID + 'pageViews');
3400            localStorage.removeItem(pubID + 'readingHistory');
3401            localStorage.removeItem(pubID + 'recentlyViewed');
3402            // Clear followed topics
3403            localStorage.removeItem(pubID + 'followedTopics');
3404            // Clear profile
3405            localStorage.removeItem(pubID + 'Profile');
3406
3407            console.log('SwiftAuth: Cleared migrated localStorage data');
3408        } catch (err) {
3409            console.warn('SwiftAuth: Error clearing localStorage', err);
3410        }
3411    }
3412
3413    /**
3414     * Check if user's data has been migrated to DynamoDB
3415     * @param {object} profile - User profile from server
3416     * @returns {boolean}
3417     */
3418    function isLocalStorageMigrated(profile) {
3419        return profile?.localStorageMigrated === true;
3420    }
3421
3422    /**
3423     * Get bookmarks from localStorage (legacy fallback)
3424     */
3425    function getLocalStorageBookmarks() {
3426        const pubID = config.pubID || '';
3427        const key = pubID + 'bookmarkedPosts';
3428
3429        try {
3430            const stored = localStorage.getItem(key);
3431            console.log('SwiftAuth: localStorage key:', key);
3432            console.log('SwiftAuth: localStorage raw data:', stored);
3433
3434            if (!stored) return [];
3435
3436            let bookmarks;
3437            try {
3438                bookmarks = JSON.parse(stored);
3439            } catch (parseErr) {
3440                console.warn('SwiftAuth: bookmarkedPosts JSON is malformed, attempting repair');
3441                bookmarks = repairTruncatedJsonArray(stored);
3442            }
3443            console.log('SwiftAuth: Parsed bookmarks:', bookmarks);
3444
3445            // Log first bookmark structure for debugging
3446            if (bookmarks && bookmarks.length > 0) {
3447                console.log('SwiftAuth: First bookmark structure:', JSON.stringify(bookmarks[0], null, 2));
3448            }
3449
3450            // Handle if bookmarks is an object with nested array
3451            let bookmarkArray = bookmarks;
3452            if (!Array.isArray(bookmarks) && typeof bookmarks === 'object') {
3453                // Try common nested structures
3454                bookmarkArray = bookmarks.items || bookmarks.bookmarks || bookmarks.posts || Object.values(bookmarks);
3455            }
3456
3457            if (Array.isArray(bookmarkArray)) {
3458                return bookmarkArray.map(b => {
3459                    // Handle both object format and simple URL strings
3460                    if (typeof b === 'string') {
3461                        return { url: b, title: '', image: '' };
3462                    }
3463                    // Pass through the original object - renderProfile will handle field mapping
3464                    return b;
3465                });
3466            }
3467            return [];
3468        } catch (err) {
3469            console.warn('SwiftAuth: Could not read localStorage bookmarks', err);
3470            return [];
3471        }
3472    }
3473
3474    /**
3475     * Initialize profile page if on profile page
3476     */
3477    async function initProfilePage() {
3478        const profileRoot = document.getElementById('swift-profile-root');
3479        if (!profileRoot) {
3480            return; // Not on profile page
3481        }
3482
3483        console.log('SwiftAuth: Profile page detected');
3484
3485        // Must be fully authenticated (magic sessions cannot access profile)
3486        if (state.authLevel !== 'full') {
3487            renderProfileLoginRequired(profileRoot);
3488            return;
3489        }
3490
3491        // Show loading state
3492        renderProfileLoading(profileRoot);
3493
3494        try {
3495            let response = await getProfile();
3496
3497            if (!response.success) {
3498                renderProfileError(profileRoot, response.error || 'Failed to load profile');
3499                return;
3500            }
3501
3502            let profile = response.profile || {};
3503
3504            // Check if migration is needed
3505            if (!isLocalStorageMigrated(profile)) {
3506                console.log('SwiftAuth: Checking for localStorage data to migrate...');
3507
3508                // Check if there's localStorage data to migrate
3509                const localBookmarks = getLocalStorageBookmarks();
3510                const localHistory = getLocalStorageReadingHistory(50);
3511                const localTopics = getLocalStorageFollowedTopics();
3512
3513                if (localBookmarks.length > 0 || localHistory.length > 0 || localTopics.length > 0) {
3514                    console.log('SwiftAuth: Found localStorage data, migrating...');
3515
3516                    // Perform migration
3517                    const migrationResult = await migrateLocalStorageToDb();
3518
3519                    if (migrationResult.success && migrationResult.migrated) {
3520                        console.log('SwiftAuth: Migration complete, refreshing profile');
3521                        // Re-fetch profile to get the migrated data
3522                        response = await getProfile();
3523                        if (response.success) {
3524                            profile = response.profile || {};
3525                        }
3526                    }
3527                } else {
3528                    console.log('SwiftAuth: No localStorage data to migrate');
3529                    // Mark as migrated even if there was nothing to migrate
3530                    // This prevents checking localStorage on every page load
3531                    if (isDirectDynamoEnabled()) {
3532                        await state.userDataManager.updateProfile({ localStorageMigrated: true });
3533                    }
3534                }
3535            } else {
3536                console.log('SwiftAuth: localStorage already migrated, using DynamoDB data only');
3537            }
3538
3539            // Update global state with migration status and profile data
3540            state.localStorageMigrated = profile.localStorageMigrated === true;
3541
3542            // Filter out invalid/corrupted topics
3543            const rawTopics = profile.followedTopics || [];
3544            const validTopics = rawTopics.filter(topic => {
3545                if (!topic) return false;
3546                const topicId = topic.topicId || topic.id || '';
3547                const topicName = topic.topicName || topic.name || '';
3548                return topicId || topicName;
3549            });
3550
3551            // Filter out invalid/corrupted reading history
3552            const rawHistory = profile.readingHistory || [];
3553            const validHistory = rawHistory.filter(item => {
3554                if (!item) return false;
3555                const url = item.url || item.articleUrl || item.link || item.href || '';
3556                return !!url; // Must have a URL
3557            });
3558
3559            // Log if corrupted data was filtered out (data from DynamoDB should be valid)
3560            if (validTopics.length !== rawTopics.length) {
3561                console.warn('SwiftAuth: Filtered', rawTopics.length - validTopics.length, 'corrupted topics');
3562            }
3563            if (validHistory.length !== rawHistory.length) {
3564                console.warn('SwiftAuth: Filtered', rawHistory.length - validHistory.length, 'corrupted history items');
3565            }
3566
3567            state.followedTopics = validTopics;
3568            state.readingHistory = validHistory;
3569
3570            // Also filter bookmarks in the profile before rendering
3571            profile.bookmarks = (profile.bookmarks || []).filter(b => {
3572                if (!b) return false;
3573                const url = b.articleUrl || b.bookmarkShortlink || b.url || '';
3574                return !!url;
3575            });
3576
3577            renderProfile(profileRoot, profile);
3578        } catch (err) {
3579            console.error('SwiftAuth: Profile load failed', err);
3580            renderProfileError(profileRoot, 'Failed to load profile');
3581        }
3582    }
3583
3584    /**
3585     * Render login required message
3586     */
3587    function renderProfileLoginRequired(container) {
3588        container.innerHTML = `
3589            <div class="swift-profile-login-required text-center py-5">
3590                <h2>Sign in to view your profile</h2>
3591                <p class="text-muted mb-4">You need to be signed in to access your profile and bookmarks.</p>
3592                <a href="/authentication/" class="btn btn-primary btn-lg">Sign In</a>
3593            </div>
3594        `;
3595    }
3596
3597    /**
3598     * Render loading state
3599     */
3600    function renderProfileLoading(container) {
3601        injectProfileStyles();
3602
3603        const shimmerLine = (width) => `<div class="swift-shimmer" style="height:14px;width:${width};border-radius:4px;margin-bottom:0.6rem"></div>`;
3604        const shimmerBlock = (height, width) => `<div class="swift-shimmer" style="height:${height};width:${width || '100%'};border-radius:6px"></div>`;
3605
3606        const profileCard = `
3607            <div class="swift-profile-section">
3608                <div class="swift-section-header">
3609                    <div class="swift-shimmer" style="width:20px;height:20px;border-radius:4px"></div>
3610                    ${shimmerLine('140px')}
3611                </div>
3612                <div class="swift-section-content">
3613                    <div class="swift-form-row">
3614                        <div class="swift-form-group">
3615                            ${shimmerLine('70px')}
3616                            ${shimmerBlock('38px')}
3617                        </div>
3618                        <div class="swift-form-group">
3619                            ${shimmerLine('70px')}
3620                            ${shimmerBlock('38px')}
3621                        </div>
3622                    </div>
3623                    <div class="swift-form-row">
3624                        <div class="swift-form-group">
3625                            ${shimmerLine('50px')}
3626                            ${shimmerLine('180px')}
3627                        </div>
3628                        <div class="swift-form-group">
3629                            ${shimmerLine('80px')}
3630                            ${shimmerBlock('38px')}
3631                        </div>
3632                    </div>
3633                    ${shimmerBlock('38px', '120px')}
3634                </div>
3635            </div>`;
3636
3637        const listCard = (titleWidth) => `
3638            <div class="swift-profile-section">
3639                <div class="swift-section-header">
3640                    <div class="swift-shimmer" style="width:20px;height:20px;border-radius:4px"></div>
3641                    ${shimmerLine(titleWidth)}
3642                </div>
3643                <div class="swift-section-content">
3644                    ${shimmerLine('90%')}
3645                    ${shimmerLine('75%')}
3646                    ${shimmerLine('60%')}
3647                </div>
3648            </div>`;
3649
3650        container.innerHTML = `
3651            <div class="swift-profile-container">
3652                ${profileCard}
3653                ${listCard('120px')}
3654                ${listCard('90px')}
3655                ${listCard('130px')}
3656            </div>
3657        `;
3658    }
3659
3660    /**
3661     * Render error state
3662     */
3663    function renderProfileError(container, message) {
3664        container.innerHTML = `
3665            <div class="swift-profile-error text-center py-5">
3666                <div class="text-danger mb-3">
3667                    <svg width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3668                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
3669                    </svg>
3670                </div>
3671                <h3>Something went wrong</h3>
3672                <p class="text-muted">${message}</p>
3673                <button onclick="SwiftAuth.initProfilePage()" class="btn btn-outline-primary mt-3">Try Again</button>
3674                <div class="mt-2">
3675                    <a href="/logout/" class="btn btn-link text-muted">Log Out</a>
3676                </div>
3677            </div>
3678        `;
3679    }
3680
3681    /**
3682     * Get reading history tracking preference
3683     * Checks state first (loaded from profile), then falls back to localStorage
3684     */
3685    function getReadingHistoryEnabled() {
3686        // Check if we have the setting from profile state
3687        if (state.profile && state.profile.trackReadingHistory !== undefined) {
3688            return state.profile.trackReadingHistory;
3689        }
3690        // Fall back to localStorage
3691        const pubID = config.pubID || '';
3692        const key = pubID + 'readingHistorySetting';
3693        const value = localStorage.getItem(key);
3694        // Default to true, only false if explicitly set to 'false'
3695        return value !== 'false';
3696    }
3697
3698    /**
3699     * Set reading history tracking preference
3700     * Saves to DynamoDB (for authenticated users) and localStorage
3701     */
3702    async function setReadingHistoryEnabled(enabled) {
3703        const pubID = config.pubID || '';
3704        const key = pubID + 'readingHistorySetting';
3705        // Always save to localStorage as immediate/backup
3706        localStorage.setItem(key, enabled.toString());
3707
3708        // Update local state
3709        if (state.profile) {
3710            state.profile.trackReadingHistory = enabled;
3711        }
3712
3713        // Save to DynamoDB for authenticated users
3714        if (isDirectDynamoEnabled() && state.authLevel === 'full') {
3715            try {
3716                await state.userDataManager.updateProfile({ trackReadingHistory: enabled });
3717                console.log('SwiftAuth: Reading history preference saved to profile');
3718            } catch (err) {
3719                console.error('SwiftAuth: Failed to save reading history preference to profile:', err);
3720            }
3721        }
3722    }
3723
3724    /**
3725     * Remove a single reading history item
3726     * Direct DynamoDB for authenticated users, localStorage for anonymous
3727     * @param {string} url - The article URL
3728     * @param {string} articleId - The article ID for DynamoDB deletion
3729     */
3730    async function removeReadingHistoryItem(url, articleId) {
3731        // Update local state regardless of storage backend
3732        state.readingHistory = state.readingHistory.filter(item => {
3733            const itemUrl = item.url || item.articleUrl || item.link || item.href || '';
3734            return itemUrl !== url;
3735        });
3736
3737        // For authenticated users with DynamoDB, delete from DynamoDB
3738        if (isDirectDynamoEnabled() && state.authLevel === 'full') {
3739            try {
3740                // Use articleId if provided, otherwise derive from URL
3741                const idToDelete = articleId || url.replace(/[^a-zA-Z0-9]/g, '-');
3742                await state.userDataManager.removePageview(idToDelete);
3743                console.log('SwiftAuth: Reading history item removed from DynamoDB');
3744                return;
3745            } catch (err) {
3746                console.error('SwiftAuth: Failed to remove reading history item from DynamoDB:', err);
3747                // Continue to try localStorage as fallback
3748            }
3749        }
3750
3751        // localStorage for anonymous users or as fallback
3752        const pubID = config.pubID || '';
3753        const key = pubID + 'pageViews';
3754        try {
3755            const stored = localStorage.getItem(key);
3756            if (!stored) return;
3757            const history = JSON.parse(stored);
3758            const filtered = history.filter(item => {
3759                const itemUrl = item.url || item.articleUrl || item.link || item.href || '';
3760                return itemUrl !== url;
3761            });
3762            localStorage.setItem(key, JSON.stringify(filtered));
3763        } catch (err) {
3764            console.warn('SwiftAuth: Could not remove reading history item', err);
3765        }
3766    }
3767
3768    /**
3769     * Clear all reading history
3770     * Direct DynamoDB for authenticated users, localStorage for anonymous
3771     */
3772    async function clearReadingHistory() {
3773        state.readingHistory = [];
3774
3775        // Direct DynamoDB for authenticated users
3776        if (isDirectDynamoEnabled()) {
3777            try {
3778                await state.userDataManager.clearReadingHistory();
3779                return;
3780            } catch (err) {
3781                console.error('SwiftAuth: clearReadingHistory failed:', err);
3782                throw err;
3783            }
3784        }
3785
3786        // localStorage for anonymous users
3787        const pubID = config.pubID || '';
3788        const key = pubID + 'pageViews';
3789        localStorage.removeItem(key);
3790    }
3791
3792    /**
3793     * Record a pageview (for reading history)
3794     * Uses direct DynamoDB when UserDataManager is ready
3795     */
3796    async function recordPageview(pageviewData) {
3797        // Direct DynamoDB for authenticated users
3798        if (isDirectDynamoEnabled() && state.authLevel === 'full') {
3799            try {
3800                console.log('[PV-DIAG] recordPageview → UserDataManager.recordPageview()', {
3801                    userId: state.userDataManager?.getUserId?.(),
3802                    pubID: config.pubID,
3803                    postId: pageviewData.postId,
3804                    articleUrl: pageviewData.articleUrl,
3805                    udmUseLocalStorage: state.userDataManager?.useLocalStorage,
3806                    udmIsReady: state.userDataManager?.isReady?.(),
3807                });
3808                await state.userDataManager.recordPageview(pageviewData);
3809                console.log('[PV-DIAG] recordPageview completed (no error thrown)');
3810                return { success: true };
3811            } catch (err) {
3812                console.error('[PV-DIAG] recordPageview FAILED:', err);
3813                return { success: false, error: err.message };
3814            }
3815        }
3816
3817        // localStorage for anonymous users only
3818        if (state.authLevel === 'anonymous') {
3819            const pubID = config.pubID || '';
3820            const key = pubID + 'pageViews';
3821            try {
3822                const stored = localStorage.getItem(key);
3823                let history = stored ? JSON.parse(stored) : [];
3824
3825                // Deduplicate by postId or url
3826                const newPostId = pageviewData.postId;
3827                const newUrl = pageviewData.articleUrl || pageviewData.url;
3828                if (newPostId || newUrl) {
3829                    history = history.filter(item => {
3830                        const itemPostId = item.postId;
3831                        const itemUrl = item.articleUrl || item.url;
3832                        if (newPostId && itemPostId) {
3833                            return itemPostId !== newPostId;
3834                        }
3835                        if (newUrl && itemUrl) {
3836                            return itemUrl !== newUrl;
3837                        }
3838                        return true;
3839                    });
3840                }
3841
3842                history.unshift({
3843                    ...pageviewData,
3844                    viewedAt: pageviewData.viewedAt || new Date().toISOString()
3845                });
3846                localStorage.setItem(key, JSON.stringify(history.slice(0, 100)));
3847                return { success: true };
3848            } catch (err) {
3849                console.warn('SwiftAuth: Could not record pageview to localStorage', err);
3850                return { success: false };
3851            }
3852        }
3853
3854        // Authenticated but DynamoDB not ready - skip recording
3855        return { success: false, error: 'DynamoDB not initialized' };
3856    }
3857
3858    /**
3859     * Render profile content
3860     */
3861    function renderProfile(container, profile) {
3862        // Handle empty profile gracefully - user exists but no DynamoDB record yet
3863        const safeProfile = profile || {};
3864        const bookmarks = safeProfile.bookmarks || [];
3865        const readingHistory = safeProfile.readingHistory || [];
3866        const followedTopics = safeProfile.followedTopics || [];
3867        const entitlement = safeProfile.entitlement;
3868        const trackingEnabled = getReadingHistoryEnabled();
3869
3870        // Inject profile styles
3871        injectProfileStyles();
3872
3873        // 1. Profile Edit Section
3874        const profileEditHtml = `
3875            <div class="swift-profile-section">
3876                <div class="swift-section-header">
3877                    <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3878                        <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
3879                        <circle cx="12" cy="7" r="4"></circle>
3880                    </svg>
3881                    <h3>Profile Information</h3>
3882                </div>
3883                <div class="swift-section-content">
3884                    <div class="swift-form-row">
3885                        <div class="swift-form-group">
3886                            <label for="swift-profile-given-name">First Name</label>
3887                            <input type="text" class="swift-input" id="swift-profile-given-name"
3888                                   value="${safeProfile.givenName || ''}"
3889                                   placeholder="Enter first name">
3890                        </div>
3891                        <div class="swift-form-group">
3892                            <label for="swift-profile-family-name">Last Name</label>
3893                            <input type="text" class="swift-input" id="swift-profile-family-name"
3894                                   value="${safeProfile.familyName || ''}"
3895                                   placeholder="Enter last name">
3896                        </div>
3897                    </div>
3898                    <div class="swift-form-row">
3899                        <div class="swift-form-group">
3900                            <label>Email</label>
3901                            <div class="swift-email-display">${safeProfile.email || 'Not available'}</div>
3902                        </div>
3903                        <div class="swift-form-group">
3904                            <label for="swift-profile-postal">Postal Code</label>
3905                            <div class="swift-postal-wrapper">
3906                                <input type="text" class="swift-input" id="swift-profile-postal"
3907                                       value="${safeProfile.postalCode || ''}"
3908                                       placeholder="Enter postal code">
3909                                <button type="button" class="swift-postal-locate" id="swift-locate-postal"
3910                                        title="Detect my postal code">
3911                                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3912                                        <circle cx="12" cy="12" r="4"/>
3913                                        <line x1="12" y1="2" x2="12" y2="6"/>
3914                                        <line x1="12" y1="18" x2="12" y2="22"/>
3915                                        <line x1="2" y1="12" x2="6" y2="12"/>
3916                                        <line x1="18" y1="12" x2="22" y2="12"/>
3917                                    </svg>
3918                                </button>
3919                            </div>
3920                        </div>
3921                    </div>
3922                    <div class="swift-form-actions">
3923                        <button type="button" class="swift-btn swift-btn-primary" id="swift-profile-save">
3924                            Save Changes
3925                        </button>
3926                        <span id="swift-profile-save-status" class="swift-save-status"></span>
3927                    </div>
3928                </div>
3929            </div>
3930        `;
3931
3932        // 1b. Change Password Section
3933        const changePasswordHtml = `
3934            <div class="swift-profile-section">
3935                <div class="swift-section-header">
3936                    <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3937                        <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
3938                        <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
3939                    </svg>
3940                    <h3>Change Password</h3>
3941                    <button type="button" class="swift-btn swift-btn-outline swift-btn-sm" id="swift-change-password-toggle">
3942                        Change Password
3943                    </button>
3944                </div>
3945                <div class="swift-section-content" id="swift-change-password-form-wrapper" style="display: none;">
3946                    <p style="margin-bottom: 1rem; font-size: 0.875rem; color: #6c757d;">Don't remember your current password? <a href="/authentication/?view=forgot" style="color: #0d6efd;">Use the forgot password tool</a>.</p>
3947                    <div class="swift-form-group">
3948                        <label for="swift-current-password">Current Password</label>
3949                        <div class="swift-password-wrapper">
3950                            <input type="password" class="swift-input" id="swift-current-password" placeholder="Enter current password" autocomplete="current-password">
3951                            <button type="button" class="swift-password-toggle" aria-label="Toggle password visibility"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
3952                        </div>
3953                    </div>
3954                    <div class="swift-form-group">
3955                        <label for="swift-new-password">New Password</label>
3956                        <div class="swift-password-wrapper">
3957                            <input type="password" class="swift-input" id="swift-new-password" placeholder="Enter new password (min. 8 characters)" autocomplete="new-password">
3958                            <button type="button" class="swift-password-toggle" aria-label="Toggle password visibility"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
3959                        </div>
3960                    </div>
3961                    <div class="swift-form-group">
3962                        <label for="swift-confirm-password">Confirm New Password</label>
3963                        <div class="swift-password-wrapper">
3964                            <input type="password" class="swift-input" id="swift-confirm-password" placeholder="Confirm new password" autocomplete="new-password">
3965                            <button type="button" class="swift-password-toggle" aria-label="Toggle password visibility"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
3966                        </div>
3967                    </div>
3968                    <div class="swift-form-actions">
3969                        <button type="button" class="swift-btn swift-btn-primary" id="swift-change-password-submit">
3970                            Update Password
3971                        </button>
3972                        <button type="button" class="swift-btn swift-btn-outline" id="swift-change-password-cancel">
3973                            Cancel
3974                        </button>
3975                        <span id="swift-change-password-status" class="swift-save-status"></span>
3976                    </div>
3977                </div>
3978            </div>
3979        `;
3980
3981        // 2. Subscription Section (ONLY for paywall sites)
3982        let subscriptionHtml = '';
3983        if (isPaywallSite()) {
3984            const tier = entitlement?.tier || entitlement?.type || 'free';
3985            const expires = entitlement?.endDate || entitlement?.expires || entitlement?.expires_at || entitlement?.expiresAt;
3986            const autoRenew = entitlement?.autoRenew || entitlement?.auto_renew || false;
3987            const stripeId = entitlement?.stripeSubscriptionId || entitlement?.stripe_subscription_id || '';
3988
3989            const hasAccess = ['subscriber', 'employee', 'gratis', 'trial'].includes(tier);
3990            const isSubscriber = tier === 'subscriber';
3991            const isEmployee = tier === 'employee';
3992            const isGratis = tier === 'gratis';
3993            const isTrial = tier === 'trial';
3994
3995            let isExpired = false;
3996            let isInGracePeriod = false;
3997            let daysUntilExpiry = null;
3998
3999            if (expires) {
4000                const expiryDate = new Date(typeof expires === 'number' ? expires * 1000 : expires);
4001                const now = new Date();
4002                isExpired = now > expiryDate;
4003
4004                if (isExpired) {
4005                    const gracePeriodEnd = new Date(expiryDate.getTime() + (3 * 24 * 60 * 60 * 1000));
4006                    isInGracePeriod = now < gracePeriodEnd;
4007                } else {
4008                    daysUntilExpiry = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24));
4009                }
4010            }
4011
4012            const formatDate = (dateVal) => {
4013                if (!dateVal) return 'N/A';
4014                const date = new Date(typeof dateVal === 'number' ? dateVal * 1000 : dateVal);
4015                return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
4016                    + ' at ' + date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
4017            };
4018
4019            const getTierInfo = () => {
4020                if (isSubscriber) return { class: 'success', label: 'Subscriber', icon: '★' };
4021                if (isEmployee) return { class: 'info', label: 'Employee', icon: '◆' };
4022                if (isGratis) return { class: 'secondary', label: 'Complimentary', icon: '♦' };
4023                if (isTrial) return { class: 'warning', label: 'Trial', icon: '◇' };
4024                return { class: 'default', label: 'Free', icon: '○' };
4025            };
4026
4027            const tierInfo = getTierInfo();
4028
4029            subscriptionHtml = `
4030                <div class="swift-profile-section">
4031                    <div class="swift-section-header">
4032                        <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4033                            <rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
4034                            <line x1="1" y1="10" x2="23" y2="10"></line>
4035                        </svg>
4036                        <h3>Subscription</h3>
4037                        <span class="swift-tier-badge swift-tier-${tierInfo.class}">${tierInfo.label}</span>
4038                    </div>
4039                    <div class="swift-section-content">
4040                        <div class="swift-subscription-status ${isInGracePeriod || (isExpired && !isInGracePeriod) ? 'swift-status-expired' : hasAccess ? 'swift-status-active' : 'swift-status-inactive'}">
4041                            ${isInGracePeriod ? `
4042                                <p class="swift-status-message"><strong>Your subscription has expired.</strong></p>
4043                            ` : isExpired && !isInGracePeriod ? `
4044                                <p class="swift-status-message"><strong>Your subscription has expired.</strong></p>
4045                            ` : hasAccess && daysUntilExpiry !== null && daysUntilExpiry <= 7 ? `
4046                                <p class="swift-status-message">Your subscription expires in <strong>${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''}</strong>.</p>
4047                            ` : hasAccess ? `
4048                                <p class="swift-status-message">Your subscription is <strong>active</strong>.</p>
4049                            ` : `
4050                                <p class="swift-status-message">You do not have an active subscription.</p>
4051                            `}
4052                            ${expires ? `
4053                                <p class="swift-status-detail">${isExpired ? 'Ended' : (autoRenew ? 'Renews' : 'Expires')}: ${formatDate(expires)}</p>
4054                            ` : ''}
4055                        </div>
4056                        <div class="swift-subscription-actions">
4057                            ${!hasAccess || isExpired ? `
4058                                <a href="/subscribe/" class="swift-btn swift-btn-primary swift-profile-subscribe-btn">Subscribe Now</a>
4059                            ` : ''}
4060                            ${safeProfile.stripeCustomerId ? `
4061                                <a href="#" class="swift-btn swift-btn-outline swift-manage-sub-btn">Manage Subscription</a>
4062                                <a href="#" class="swift-btn swift-btn-outline swift-download-receipts-btn">Download Receipts</a>
4063                            ` : ''}
4064                        </div>
4065                    </div>
4066                </div>
4067            `;
4068        }
4069
4070        // 3. Followed Topics Section
4071        let followedTopicsHtml = '';
4072        // Filter out empty/invalid topics
4073        const validTopics = followedTopics.filter(topic => {
4074            if (!topic) return false;
4075            const topicId = topic.topicId || topic.id || '';
4076            const topicName = topic.topicName || topic.name || '';
4077            return topicId || topicName; // Must have at least an ID or name
4078        });
4079
4080        if (validTopics.length > 0) {
4081            const topicItems = validTopics.map(topic => {
4082                const topicId = topic.topicId || topic.id || '';
4083                let topicName = topic.topicName || topic.name || topicId;
4084
4085                // Fix malformed data where topicName equals topicId
4086                if (topicName === topicId && topicId.includes('-')) {
4087                    // Convert "category-politics" to "Politics", "author-john-doe" to "John Doe"
4088                    const parts = topicId.split('-');
4089                    parts.shift(); // Remove prefix (category, author, series, tag)
4090                    topicName = parts.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
4091                }
4092
4093                return `
4094                    <span class="swift-topic-tag">
4095                        ${topicName}
4096                        <button type="button" class="swift-tag-remove" data-swift-unfollow-topic="${topicId}" aria-label="Unfollow">
4097                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4098                                <line x1="18" y1="6" x2="6" y2="18"></line>
4099                                <line x1="6" y1="6" x2="18" y2="18"></line>
4100                            </svg>
4101                        </button>
4102                    </span>
4103                `;
4104            }).join('');
4105
4106            followedTopicsHtml = `
4107                <div class="swift-profile-section">
4108                    <div class="swift-section-header">
4109                        <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4110                            <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
4111                            <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
4112                        </svg>
4113                        <h3>Followed Topics</h3>
4114                        <span class="swift-count-badge">${validTopics.length}</span>
4115                    </div>
4116                    <div class="swift-section-content">
4117                        <div class="swift-topics-grid">
4118                            ${topicItems}
4119                        </div>
4120                    </div>
4121                </div>
4122            `;
4123        } else {
4124            followedTopicsHtml = `
4125                <div class="swift-profile-section">
4126                    <div class="swift-section-header">
4127                        <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4128                            <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
4129                            <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
4130                        </svg>
4131                        <h3>Followed Topics</h3>
4132                    </div>
4133                    <div class="swift-section-content">
4134                        <p class="swift-empty-message">You're not following any topics yet. Follow topics to get personalized content recommendations.</p>
4135                    </div>
4136                </div>
4137            `;
4138        }
4139
4140        // 4. Bookmarks Section
4141        let bookmarksHtml = '';
4142        if (bookmarks.length > 0) {
4143            // Deduplicate bookmarks by articleId/postId - keep the most recent one
4144            const seenIds = new Set();
4145            const deduplicatedBookmarks = bookmarks
4146                .sort((a, b) => {
4147                    // Sort by bookmarkedAt descending (most recent first)
4148                    const dateA = new Date(a.bookmarkedAt || 0);
4149                    const dateB = new Date(b.bookmarkedAt || 0);
4150                    return dateB - dateA;
4151                })
4152                .filter(b => {
4153                    const id = b.postId || b.articleId || b.articleUrl || '';
4154                    if (!id || seenIds.has(id)) return false;
4155                    seenIds.add(id);
4156                    return true;
4157                });
4158
4159            const bookmarkItems = deduplicatedBookmarks.map((b, index) => {
4160                const url = b.articleUrl || b.bookmarkShortlink || b.url || '';
4161                const title = b.articleTitle || b.bookmarkTitle || b.title || url;
4162                const postId = b.postId || b.articleId || '';
4163                // Store the SK for proper deletion of old-format records
4164                const sk = b.SK || '';
4165
4166                if (!url) return '';
4167
4168                return `
4169                    <div class="swift-list-item" ${postId ? `data-post-id="${postId}"` : ''} ${sk ? `data-sk="${sk}"` : ''}>
4170                        <a href="${url}" class="swift-item-title">${title}</a>
4171                        <button type="button" class="swift-item-remove" data-swift-remove-bookmark="${url}" ${postId ? `data-post-id="${postId}"` : ''} ${sk ? `data-sk="${sk}"` : ''} aria-label="Remove">
4172                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4173                                <polyline points="3 6 5 6 21 6"></polyline>
4174                                <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
4175                            </svg>
4176                        </button>
4177                    </div>
4178                `;
4179            }).filter(html => html).join('');
4180
4181            bookmarksHtml = `
4182                <div class="swift-profile-section">
4183                    <div class="swift-section-header">
4184                        <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4185                            <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
4186                        </svg>
4187                        <h3>Bookmarks</h3>
4188                        <span class="swift-count-badge">${deduplicatedBookmarks.length}</span>
4189                    </div>
4190                    <div class="swift-section-content">
4191                        <div class="swift-list">
4192                            ${bookmarkItems}
4193                        </div>
4194                    </div>
4195                </div>
4196            `;
4197        } else {
4198            bookmarksHtml = `
4199                <div class="swift-profile-section">
4200                    <div class="swift-section-header">
4201                        <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4202                            <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
4203                        </svg>
4204                        <h3>Bookmarks</h3>
4205                    </div>
4206                    <div class="swift-section-content">
4207                        <p class="swift-empty-message">You haven't saved any articles yet. Click the bookmark icon on articles to save them for later.</p>
4208                    </div>
4209                </div>
4210            `;
4211        }
4212
4213        // 5. Reading History Section with toggle and delete
4214        let readingHistoryHtml = '';
4215        const historyItems = readingHistory.slice(0, 15).map((item, index) => {
4216            const url = item.articleUrl || item.url || '';
4217            const title = item.articleTitle || item.title || url;
4218            const viewedAt = item.viewedAt || item.timestamp || item.date || '';
4219            // Get articleId for DynamoDB deletion - same logic as when storing
4220            const articleId = item.postId || item.articleId || url.replace(/[^a-zA-Z0-9]/g, '-');
4221
4222            if (!url) return '';
4223
4224            let dateStr = '';
4225            if (viewedAt) {
4226                try {
4227                    const date = new Date(viewedAt);
4228                    dateStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
4229                } catch (e) {
4230                    dateStr = '';
4231                }
4232            }
4233
4234            return `
4235                <div class="swift-list-item swift-history-item-row">
4236                    <a href="${url}" class="swift-item-title">${title}</a>
4237                    <div class="swift-item-meta">
4238                        ${dateStr ? `<span class="swift-item-date">${dateStr}</span>` : ''}
4239                        <button type="button" class="swift-item-remove" data-swift-remove-history="${url}" data-article-id="${articleId}" aria-label="Remove">
4240                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4241                                <line x1="18" y1="6" x2="6" y2="18"></line>
4242                                <line x1="6" y1="6" x2="18" y2="18"></line>
4243                            </svg>
4244                        </button>
4245                    </div>
4246                </div>
4247            `;
4248        }).filter(html => html).join('');
4249
4250        readingHistoryHtml = `
4251            <div class="swift-profile-section">
4252                <div class="swift-section-header">
4253                    <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4254                        <circle cx="12" cy="12" r="10"></circle>
4255                        <polyline points="12 6 12 12 16 14"></polyline>
4256                    </svg>
4257                    <h3>Reading History</h3>
4258                </div>
4259                <div class="swift-section-content">
4260                    <div class="swift-history-controls">
4261                        <label class="swift-toggle">
4262                            <input type="checkbox" id="swift-history-toggle" ${trackingEnabled ? 'checked' : ''}>
4263                            <span class="swift-toggle-slider"></span>
4264                            <span class="swift-toggle-label">Track reading history</span>
4265                        </label>
4266                        ${readingHistory.length > 0 ? `
4267                            <button type="button" class="swift-btn swift-btn-text swift-btn-danger" id="swift-clear-history">
4268                                Clear All History
4269                            </button>
4270                        ` : ''}
4271                    </div>
4272                    ${readingHistory.length > 0 ? `
4273                        <div class="swift-list swift-history-list">
4274                            ${historyItems}
4275                        </div>
4276                    ` : `
4277                        <p class="swift-empty-message">Your reading history will appear here as you browse articles.</p>
4278                    `}
4279                </div>
4280            </div>
4281        `;
4282
4283        // 6. Purchased Photos Section (only when photo sales enabled)
4284        let purchasedPhotosHtml = '';
4285        if (config.photoSales && config.photoSales.enabled) {
4286            purchasedPhotosHtml = `
4287                <div class="swift-profile-section" id="swift-purchased-photos-section">
4288                    <div class="swift-section-header">
4289                        <svg class="swift-section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4290                            <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
4291                            <circle cx="12" cy="13" r="4"/>
4292                        </svg>
4293                        <h3>Purchased Photos</h3>
4294                    </div>
4295                    <div class="swift-section-content">
4296                        <p class="swift-empty-message">Loading...</p>
4297                    </div>
4298                </div>
4299            `;
4300        }
4301
4302        // Render the full profile
4303        container.innerHTML = `
4304            <div class="swift-profile-container">
4305                ${profileEditHtml}
4306                ${changePasswordHtml}
4307                ${subscriptionHtml}
4308                ${followedTopicsHtml}
4309                ${bookmarksHtml}
4310                ${readingHistoryHtml}
4311                ${purchasedPhotosHtml}
4312
4313                <div class="swift-profile-footer">
4314                    <button type="button" id="swift-sign-out" class="swift-btn swift-btn-outline">Sign Out</button>
4315                    <button type="button" id="swift-delete-account" class="swift-btn swift-btn-danger-outline">Delete Account</button>
4316                </div>
4317            </div>
4318        `;
4319
4320        // Attach event handlers
4321        attachProfileEventHandlers(container, safeProfile);
4322
4323        // Load purchased photos asynchronously
4324        if (config.photoSales && config.photoSales.enabled) {
4325            loadPurchasedPhotos(container);
4326        }
4327    }
4328
4329    /**
4330     * Inject profile page styles
4331     */
4332    function injectProfileStyles() {
4333        if (document.getElementById('swift-profile-styles')) return;
4334
4335        const styles = `
4336            .swift-profile-container {
4337                max-width: 800px;
4338                margin: 0 auto;
4339                padding: 0 1rem;
4340            }
4341
4342            .swift-profile-section {
4343                background: #fff;
4344                border-radius: 12px;
4345                box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
4346                margin-bottom: 1.5rem;
4347                overflow: hidden;
4348            }
4349
4350            .swift-section-header {
4351                display: flex;
4352                align-items: center;
4353                gap: 0.75rem;
4354                padding: 1rem 1.25rem;
4355                border-bottom: 1px solid #eee;
4356                background: #fafafa;
4357            }
4358
4359            .swift-section-header h3 {
4360                font-size: 1rem;
4361                font-weight: 600;
4362                margin: 0;
4363                color: #333;
4364            }
4365
4366            .swift-section-icon {
4367                width: 20px;
4368                height: 20px;
4369                color: #666;
4370                flex-shrink: 0;
4371            }
4372
4373            .swift-section-content {
4374                padding: 1.25rem;
4375            }
4376
4377            .swift-count-badge {
4378                background: #e9ecef;
4379                color: #495057;
4380                font-size: 0.75rem;
4381                font-weight: 600;
4382                padding: 0.2rem 0.5rem;
4383                border-radius: 10px;
4384                margin-left: auto;
4385            }
4386
4387            .swift-tier-badge {
4388                font-size: 0.7rem;
4389                font-weight: 600;
4390                padding: 0.25rem 0.6rem;
4391                border-radius: 4px;
4392                margin-left: auto;
4393                text-transform: uppercase;
4394                letter-spacing: 0.5px;
4395            }
4396
4397            .swift-tier-success { background: #d4edda; color: #155724; }
4398            .swift-tier-info { background: #d1ecf1; color: #0c5460; }
4399            .swift-tier-warning { background: #fff3cd; color: #856404; }
4400            .swift-tier-secondary { background: #e2e3e5; color: #383d41; }
4401            .swift-tier-default { background: #f8f9fa; color: #6c757d; }
4402
4403            /* Form styles */
4404            .swift-form-row {
4405                display: grid;
4406                grid-template-columns: 1fr 1fr;
4407                gap: 1rem;
4408            }
4409
4410            @media (max-width: 576px) {
4411                .swift-form-row {
4412                    grid-template-columns: 1fr;
4413                }
4414            }
4415
4416            .swift-form-group {
4417                margin-bottom: 1rem;
4418            }
4419
4420            .swift-form-group label {
4421                display: block;
4422                font-size: 0.8rem;
4423                font-weight: 500;
4424                color: #666;
4425                margin-bottom: 0.4rem;
4426            }
4427
4428            .swift-input {
4429                width: 100%;
4430                padding: 0.6rem 0.8rem;
4431                font-size: 0.95rem;
4432                border: 1px solid #ddd;
4433                border-radius: 6px;
4434                transition: border-color 0.2s, box-shadow 0.2s;
4435            }
4436
4437            .swift-input:focus {
4438                outline: none;
4439                border-color: #0066cc;
4440                box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
4441            }
4442
4443            .swift-email-display {
4444                padding: 0.6rem 0;
4445                font-size: 0.95rem;
4446                color: #333;
4447            }
4448
4449            .swift-form-actions {
4450                display: flex;
4451                align-items: center;
4452                gap: 1rem;
4453                margin-top: 0.5rem;
4454            }
4455
4456            .swift-save-status {
4457                font-size: 0.85rem;
4458            }
4459
4460            /* Button styles */
4461            .swift-btn {
4462                display: inline-flex;
4463                align-items: center;
4464                justify-content: center;
4465                gap: 0.5rem;
4466                padding: 0.6rem 1.2rem;
4467                font-size: 0.9rem;
4468                font-weight: 500;
4469                border-radius: 6px;
4470                border: none;
4471                cursor: pointer;
4472                transition: all 0.2s;
4473                text-decoration: none;
4474            }
4475
4476            .swift-btn-primary {
4477                background: #0066cc;
4478                color: #fff;
4479            }
4480
4481            .swift-btn-primary:hover {
4482                background: #0052a3;
4483            }
4484
4485            .swift-btn-outline {
4486                background: transparent;
4487                border: 1px solid #ddd;
4488                color: #333;
4489            }
4490
4491            .swift-btn-outline:hover {
4492                background: #f5f5f5;
4493                border-color: #ccc;
4494            }
4495
4496            .swift-btn-text {
4497                background: transparent;
4498                padding: 0.4rem 0.8rem;
4499                color: #666;
4500            }
4501
4502            .swift-btn-text:hover {
4503                background: #f5f5f5;
4504            }
4505
4506            .swift-btn-danger {
4507                color: #dc3545;
4508            }
4509
4510            .swift-btn-danger:hover {
4511                background: #fff5f5;
4512            }
4513
4514            .swift-btn-danger-outline {
4515                background: transparent;
4516                border: 1px solid #dc3545;
4517                color: #dc3545;
4518                margin-left: 1rem;
4519            }
4520
4521            .swift-btn-danger-outline:hover {
4522                background: #dc3545;
4523                color: #fff;
4524            }
4525
4526            /* Topics grid */
4527            .swift-topics-grid {
4528                display: flex;
4529                flex-wrap: wrap;
4530                gap: 0.5rem;
4531            }
4532
4533            .swift-topic-tag {
4534                display: inline-flex;
4535                align-items: center;
4536                gap: 0.4rem;
4537                background: #e9ecef;
4538                color: #495057;
4539                padding: 0.4rem 0.6rem 0.4rem 0.8rem;
4540                border-radius: 20px;
4541                font-size: 0.85rem;
4542                font-weight: 500;
4543            }
4544
4545            .swift-tag-remove {
4546                display: flex;
4547                align-items: center;
4548                justify-content: center;
4549                width: 18px;
4550                height: 18px;
4551                padding: 0;
4552                border: none;
4553                background: rgba(0,0,0,0.1);
4554                border-radius: 50%;
4555                cursor: pointer;
4556                transition: background 0.2s;
4557            }
4558
4559            .swift-tag-remove:hover {
4560                background: rgba(0,0,0,0.2);
4561            }
4562
4563            .swift-tag-remove svg {
4564                width: 10px;
4565                height: 10px;
4566                color: #666;
4567            }
4568
4569            /* List items */
4570            .swift-list {
4571                display: flex;
4572                flex-direction: column;
4573            }
4574
4575            .swift-list-item {
4576                display: flex;
4577                align-items: center;
4578                justify-content: space-between;
4579                gap: 1rem;
4580                padding: 0.75rem 0;
4581                border-bottom: 1px solid #eee;
4582            }
4583
4584            .swift-list-item:last-child {
4585                border-bottom: none;
4586            }
4587
4588            .swift-item-title {
4589                flex: 1;
4590                color: #333;
4591                text-decoration: none;
4592                font-size: 0.95rem;
4593                line-height: 1.4;
4594                overflow: hidden;
4595                text-overflow: ellipsis;
4596                display: -webkit-box;
4597                -webkit-line-clamp: 2;
4598                -webkit-box-orient: vertical;
4599            }
4600
4601            .swift-item-title:hover {
4602                color: #0066cc;
4603            }
4604
4605            .swift-item-meta {
4606                display: flex;
4607                align-items: center;
4608                gap: 0.75rem;
4609                flex-shrink: 0;
4610            }
4611
4612            .swift-item-date {
4613                font-size: 0.8rem;
4614                color: #999;
4615                white-space: nowrap;
4616            }
4617
4618            .swift-item-remove {
4619                display: flex;
4620                align-items: center;
4621                justify-content: center;
4622                width: 28px;
4623                height: 28px;
4624                padding: 0;
4625                border: none;
4626                background: transparent;
4627                border-radius: 4px;
4628                cursor: pointer;
4629                transition: background 0.2s;
4630                flex-shrink: 0;
4631            }
4632
4633            .swift-item-remove:hover {
4634                background: #fee;
4635            }
4636
4637            .swift-item-remove svg {
4638                width: 16px;
4639                height: 16px;
4640                color: #999;
4641            }
4642
4643            .swift-item-remove:hover svg {
4644                color: #dc3545;
4645            }
4646
4647            .swift-list-more {
4648                font-size: 0.8rem;
4649                color: #999;
4650                margin: 0.75rem 0 0;
4651                padding-top: 0.75rem;
4652                border-top: 1px solid #eee;
4653            }
4654
4655            /* History controls */
4656            .swift-history-controls {
4657                display: flex;
4658                align-items: center;
4659                justify-content: space-between;
4660                margin-bottom: 1rem;
4661                padding-bottom: 1rem;
4662                border-bottom: 1px solid #eee;
4663            }
4664
4665            .swift-toggle {
4666                display: flex;
4667                align-items: center;
4668                gap: 0.75rem;
4669                cursor: pointer;
4670            }
4671
4672            .swift-toggle input {
4673                display: none;
4674            }
4675
4676            .swift-toggle-slider {
4677                position: relative;
4678                width: 40px;
4679                height: 22px;
4680                background: #ccc;
4681                border-radius: 22px;
4682                transition: background 0.3s;
4683            }
4684
4685            .swift-toggle-slider::before {
4686                content: '';
4687                position: absolute;
4688                top: 2px;
4689                left: 2px;
4690                width: 18px;
4691                height: 18px;
4692                background: #fff;
4693                border-radius: 50%;
4694                transition: transform 0.3s;
4695            }
4696
4697            .swift-toggle input:checked + .swift-toggle-slider {
4698                background: #0066cc;
4699            }
4700
4701            .swift-toggle input:checked + .swift-toggle-slider::before {
4702                transform: translateX(18px);
4703            }
4704
4705            .swift-toggle-label {
4706                font-size: 0.9rem;
4707                color: #333;
4708            }
4709
4710            /* Empty state */
4711            .swift-empty-message {
4712                color: #999;
4713                font-size: 0.9rem;
4714                margin: 0;
4715            }
4716
4717            /* Subscription status */
4718            .swift-subscription-status {
4719                padding: 1rem;
4720                border-radius: 8px;
4721                margin-bottom: 1rem;
4722            }
4723
4724            .swift-status-active {
4725                background: #d4edda;
4726            }
4727
4728            .swift-status-expired {
4729                background: #f8d7da;
4730            }
4731
4732            .swift-status-inactive {
4733                background: #f8f9fa;
4734            }
4735
4736            .swift-status-message {
4737                margin: 0 0 0.25rem;
4738                font-size: 0.95rem;
4739            }
4740
4741            .swift-status-detail {
4742                margin: 0;
4743                font-size: 0.85rem;
4744                opacity: 0.8;
4745            }
4746
4747            .swift-subscription-actions {
4748                display: flex;
4749                gap: 0.75rem;
4750            }
4751
4752            /* Footer */
4753            .swift-profile-footer {
4754                padding: 1.5rem 0;
4755                text-align: center;
4756            }
4757
4758            /* Shimmer skeleton */
4759            @keyframes swift-shimmer {
4760                0% { background-position: -400px 0; }
4761                100% { background-position: 400px 0; }
4762            }
4763
4764            .swift-shimmer {
4765                background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
4766                background-size: 800px 100%;
4767                animation: swift-shimmer 1.5s infinite ease-in-out;
4768            }
4769
4770            .swift-delete-modal-overlay {
4771                position: fixed;
4772                inset: 0;
4773                background: rgba(0, 0, 0, 0.5);
4774                display: flex;
4775                align-items: center;
4776                justify-content: center;
4777                z-index: 100000;
4778            }
4779            .swift-delete-modal {
4780                background: #fff;
4781                border-radius: 8px;
4782                padding: 32px;
4783                max-width: 440px;
4784                width: 90%;
4785                box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
4786            }
4787            .swift-delete-modal h3 {
4788                margin: 0 0 12px;
4789                font-size: 1.25rem;
4790            }
4791            .swift-delete-modal p {
4792                margin: 0 0 24px;
4793                color: #555;
4794                line-height: 1.5;
4795            }
4796            .swift-delete-modal-actions {
4797                display: flex;
4798                gap: 12px;
4799                justify-content: flex-end;
4800            }
4801            .swift-btn-danger {
4802                background: #dc3545;
4803                color: #fff;
4804                border: 1px solid #dc3545;
4805            }
4806            .swift-btn-danger:hover {
4807                background: #c82333;
4808            }
4809        `;
4810
4811        const styleEl = document.createElement('style');
4812        styleEl.id = 'swift-profile-styles';
4813        styleEl.textContent = styles;
4814        document.head.appendChild(styleEl);
4815    }
4816
4817    /**
4818     * Detect postal code (and full location data) via browser geolocation + Google Maps reverse geocoding.
4819     * Returns { postalCode, city, county, state, country, latitude, longitude, formattedAddress, locationUpdatedAt }
4820     */
4821    async function detectPostalCode() {
4822        // Get coordinates from browser
4823        const position = await new Promise((resolve, reject) => {
4824            if (!navigator.geolocation) {
4825                reject(new Error('Geolocation is not supported by this browser'));
4826                return;
4827            }
4828            navigator.geolocation.getCurrentPosition(resolve, reject, {
4829                enableHighAccuracy: true,
4830                timeout: 30000,
4831                maximumAge: 300000
4832            });
4833        });
4834
4835        const { latitude, longitude } = position.coords;
4836
4837        // Reverse geocode via Google Maps
4838        const apiKey = window.swiftAuthConfig?.googleMaps?.apiKey;
4839        if (!apiKey) {
4840            throw new Error('Google Maps API key not configured. Please contact support.');
4841        }
4842
4843        const url = new URL('https://maps.googleapis.com/maps/api/geocode/json');
4844        url.searchParams.set('latlng', `${latitude},${longitude}`);
4845        url.searchParams.set('key', apiKey);
4846
4847        const response = await fetch(url.toString());
4848        if (!response.ok) {
4849            throw new Error(`Reverse geocoding failed: ${response.statusText}`);
4850        }
4851
4852        const data = await response.json();
4853        if (data.status !== 'OK') {
4854            throw new Error(`Reverse geocoding error: ${data.status}`);
4855        }
4856
4857        const result = data.results?.[0];
4858        if (!result) {
4859            throw new Error('No results found for location');
4860        }
4861
4862        const components = {};
4863        result.address_components?.forEach(component => {
4864            const types = component.types;
4865            if (types.includes('postal_code')) components.postalCode = component.long_name;
4866            if (types.includes('locality')) components.city = component.long_name;
4867            if (types.includes('administrative_area_level_2')) components.county = component.long_name;
4868            if (types.includes('administrative_area_level_1')) components.state = component.long_name;
4869            if (types.includes('country')) components.country = component.long_name;
4870        });
4871
4872        return {
4873            postalCode: components.postalCode || null,
4874            city: components.city || null,
4875            county: components.county || null,
4876            state: components.state || null,
4877            country: components.country || null,
4878            latitude,
4879            longitude,
4880            formattedAddress: result.formatted_address || null,
4881            locationUpdatedAt: new Date().toISOString()
4882        };
4883    }
4884
4885    /**
4886     * Get a user-friendly error message from a geolocation error
4887     */
4888    function getGeoErrorMessage(error) {
4889        if (error.code === 1) {
4890            return "No problem! You can enter your postal code manually instead.";
4891        } else if (error.code === 2) {
4892            if (!window.isSecureContext && window.location.hostname !== 'localhost') {
4893                return "Location requires HTTPS. Please enter your postal code manually.";
4894            }
4895            const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
4896            if (isMac) {
4897                return "Location unavailable. On Mac, check System Settings → Privacy & Security → Location Services. Or enter your postal code manually.";
4898            }
4899            return "We couldn't detect your location. Please enter your postal code manually.";
4900        } else if (error.code === 3) {
4901            return "Location request timed out. Please try again or enter your postal code manually.";
4902        } else if (error.message) {
4903            return error.message;
4904        }
4905        return "Unable to get your location. Please enter your postal code manually.";
4906    }
4907
4908    /**
4909     * Attach event handlers for profile page interactions
4910     */
4911    /**
4912     * Load and render purchased photos into the profile section
4913     */
4914    async function loadPurchasedPhotos(container) {
4915        const section = container.querySelector('#swift-purchased-photos-section');
4916        if (!section) return;
4917
4918        const content = section.querySelector('.swift-section-content');
4919        const termsUrl = config.photoSales.licenseTermsUrl || '/photo-license-terms/';
4920
4921        try {
4922            const idToken = getIdTokenSync();
4923            if (!idToken) {
4924                content.innerHTML = '<p class="swift-empty-message">Sign in to view purchases.</p>';
4925                return;
4926            }
4927
4928            const nonce = await new Promise((resolve) => {
4929                const xhr = new XMLHttpRequest();
4930                xhr.open('GET', ajax.ajaxUrl + '?action=swift_get_nonce');
4931                xhr.withCredentials = true;
4932                xhr.onload = () => {
4933                    try { resolve(JSON.parse(xhr.responseText).data.nonce); }
4934                    catch(e) { resolve(null); }
4935                };
4936                xhr.onerror = () => resolve(null);
4937                xhr.send();
4938            });
4939
4940            if (!nonce) {
4941                content.innerHTML = '<p class="swift-empty-message">Failed to load purchases.</p>';
4942                return;
4943            }
4944
4945            const formData = new FormData();
4946            formData.append('action', 'swift_photo_purchases');
4947            formData.append('nonce', nonce);
4948
4949            const response = await fetch(ajax.ajaxUrl, {
4950                method: 'POST',
4951                body: formData,
4952                credentials: 'same-origin',
4953                headers: { 'Authorization': 'Bearer ' + idToken },
4954            });
4955
4956            const result = await response.json();
4957
4958            if (!result.success || !result.licenses || result.licenses.length === 0) {
4959                content.innerHTML = '<p class="swift-empty-message">
4959No photo purchases yet.</p>';
4960                // Update header count
4961                const h3 = section.querySelector('h3');
4962                if (h3) h3.textContent = 'Purchased Photos (0)';
4963                return;
4964            }
4965
4966            const licenses = result.licenses;
4967            const h3 = section.querySelector('h3');
4968            if (h3) h3.textContent = 'Purchased Photos (' + licenses.length + ')';
4969
4970            // Add license terms link to header
4971            const header = section.querySelector('.swift-section-header');
4972            if (header) {
4973                const termsLink = document.createElement('a');
4974                termsLink.href = termsUrl;
4975                termsLink.target = '_blank';
4976                termsLink.rel = 'noopener noreferrer';
4977                termsLink.className = 'swift-btn swift-btn-outline swift-btn-sm';
4978                termsLink.textContent = 'License Terms';
4979                header.appendChild(termsLink);
4980            }
4981
4982            content.innerHTML = '<div class="swift-photo-purchases-grid">' +
4983                licenses.map(function(license) {
4984                    const caption = license.photoCaption || 'Photo';
4985                    const articleTitle = license.postTitle || '';
4986                    const photoUrl = license.photoUrl || '';
4987                    const date = license.createdAt ? new Date(license.createdAt).toLocaleDateString() : '';
4988                    const amount = license.amountCents ? String.fromCharCode(36) + (license.amountCents / 100).toFixed(2) : '';
4989
4990                    return '<div class="swift-photo-purchase-item">' +
4991                        (photoUrl ? '<a href="' + photoUrl + '" target="_blank" rel="noopener noreferrer" class="swift-photo-purchase-thumb"><img src="' + photoUrl + '" alt="' + caption.replace(/"/g, '&quot;') + '" loading="lazy"></a>' : '') +
4992                        '<div class="swift-photo-purchase-info">' +
4993                            '<div class="swift-photo-purchase-caption">' + caption + '</div>' +
4994                            (articleTitle ? '<div class="swift-photo-purchase-article">From: ' + (license.postId ? '<a href="/?p=' + license.postId + '">' + articleTitle + '</a>' : articleTitle) + '</div>' : '') +
4995                            '<div class="swift-photo-purchase-meta">' + (date ? '<span>' + date + '</span>' : '') + (amount ? '<span> &middot; ' + amount + '</span>' : '') + '</div>' +
4996                        '</div>' +
4997                        (photoUrl ? '<a href="' + photoUrl + '" target="_blank" rel="noopener noreferrer" class="swift-btn swift-btn-primary swift-btn-sm">Download</a>' : '') +
4998                    '</div>';
4999                }).join('') +
5000            '</div>';
5001
5002        } catch (err) {
5003            console.error('Failed to load photo purchases:', err);
5004            content.innerHTML = '<p class="swift-empty-message">Failed to load purchases.</p>';
5005        }
5006    }
5007
5008    function attachProfileEventHandlers(container, profile) {
5009        // Store detected location data to include in profile save
5010        let detectedLocationData = null;
5011        // Save profile button
5012        const saveBtn = container.querySelector('#swift-profile-save');
5013        const statusEl = container.querySelector('#swift-profile-save-status');
5014
5015        if (saveBtn) {
5016            saveBtn.addEventListener('click', async () => {
5017                const givenNameInput = container.querySelector('#swift-profile-given-name');
5018                const familyNameInput = container.querySelector('#swift-profile-family-name');
5019                const postalInput = container.querySelector('#swift-profile-postal');
5020
5021                const updatedData = {
5022                    givenName: givenNameInput?.value || '',
5023                    familyName: familyNameInput?.value || '',
5024                    postalCode: postalInput?.value || ''
5025                };
5026
5027                // Include location fields if geolocation was used
5028                if (detectedLocationData) {
5029                    Object.assign(updatedData, {
5030                        city: detectedLocationData.city,
5031                        state: detectedLocationData.state,
5032                        county: detectedLocationData.county,
5033                        country: detectedLocationData.country,
5034                        latitude: detectedLocationData.latitude,
5035                        longitude: detectedLocationData.longitude,
5036                        formattedAddress: detectedLocationData.formattedAddress,
5037                        locationUpdatedAt: detectedLocationData.locationUpdatedAt
5038                    });
5039                }
5040
5041                saveBtn.disabled = true;
5042                saveBtn.textContent = 'Saving...';
5043                if (statusEl) statusEl.textContent = '';
5044
5045                try {
5046                    // Use the updateProfile function (handles direct DynamoDB or AJAX)
5047                    const response = await updateProfile(updatedData);
5048
5049                    if (!response.success) {
5050                        throw new Error(response.error || 'Failed to save');
5051                    }
5052
5053                    // Also save to localStorage for backwards compatibility (if not migrated)
5054                    if (!state.localStorageMigrated) {
5055                        const updatedProfile = { ...profile, ...updatedData };
5056                        saveLocalStorageProfile(updatedProfile);
5057                    }
5058
5059                    saveBtn.textContent = 'Saved!';
5060                    saveBtn.style.background = '#28a745';
5061
5062                    setTimeout(() => {
5063                        saveBtn.textContent = 'Save Changes';
5064                        saveBtn.style.background = '';
5065                        saveBtn.disabled = false;
5066                    }, 2000);
5067
5068                } catch (err) {
5069                    console.error('SwiftAuth: Profile save failed', err);
5070                    saveBtn.textContent = 'Save Changes';
5071                    saveBtn.disabled = false;
5072                    if (statusEl) {
5073                        statusEl.textContent = 'Failed to save';
5074                        statusEl.style.color = '#dc3545';
5075                    }
5076                }
5077            });
5078        }
5079
5080        // Locate postal code button
5081        const locateBtn = container.querySelector('#swift-locate-postal');
5082        if (locateBtn) {
5083            locateBtn.addEventListener('click', async () => {
5084                const postalInput = container.querySelector('#swift-profile-postal');
5085                const originalHtml = locateBtn.innerHTML;
5086
5087                // Show spinner
5088                locateBtn.disabled = true;
5089                locateBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>';
5090                locateBtn.classList.add('swift-postal-locating');
5091
5092                try {
5093                    const locationData = await detectPostalCode();
5094                    if (locationData.postalCode && postalInput) {
5095                        postalInput.value = locationData.postalCode;
5096                    }
5097                    // Store for inclusion in save
5098                    detectedLocationData = locationData;
5099                } catch (err) {
5100                    console.error('SwiftAuth: Geolocation failed', err);
5101                    const msg = getGeoErrorMessage(err);
5102                    const statusEl = container.querySelector('#swift-profile-save-status');
5103                    if (statusEl) {
5104                        statusEl.textContent = msg;
5105                        statusEl.style.color = '#dc3545';
5106                        setTimeout(() => { statusEl.textContent = ''; }, 5000);
5107                    }
5108                } finally {
5109                    locateBtn.disabled = false;
5110                    locateBtn.innerHTML = originalHtml;
5111                    locateBtn.classList.remove('swift-postal-locating');
5112                }
5113            });
5114        }
5115
5116        // Change Password toggle, cancel, and submit
5117        const cpToggle = container.querySelector('#swift-change-password-toggle');
5118        const cpWrapper = container.querySelector('#swift-change-password-form-wrapper');
5119        const cpCancel = container.querySelector('#swift-change-password-cancel');
5120        const cpSubmit = container.querySelector('#swift-change-password-submit');
5121        const cpStatus = container.querySelector('#swift-change-password-status');
5122
5123        const eyeSvg = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
5124        const eyeOffSvg = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
5125
5126        const resetPasswordForm = () => {
5127            if (cpWrapper) cpWrapper.style.display = 'none';
5128            const fields = ['#swift-current-password', '#swift-new-password', '#swift-confirm-password'];
5129            fields.forEach(sel => { const el = container.querySelector(sel); if (el) { el.value = ''; el.type = 'password'; } });
5130            // Reset all toggle buttons back to eye icon
5131            if (cpWrapper) {
5132                cpWrapper.querySelectorAll('.swift-password-toggle').forEach(btn => { btn.innerHTML = eyeSvg; });
5133            }
5134            if (cpStatus) { cpStatus.textContent = ''; cpStatus.style.color = ''; }
5135            if (cpSubmit) { cpSubmit.disabled = false; cpSubmit.textContent = 'Update Password'; }
5136        };
5137
5138        if (cpToggle && cpWrapper) {
5139            cpToggle.addEventListener('click', () => {
5140                const isHidden = cpWrapper.style.display === 'none';
5141                cpWrapper.style.display = isHidden ? 'block' : 'none';
5142                if (isHidden) {
5143                    const firstInput = container.querySelector('#swift-current-password');
5144                    if (firstInput) firstInput.focus();
5145                }
5146            });
5147        }
5148
5149        if (cpCancel) {
5150            cpCancel.addEventListener('click', resetPasswordForm);
5151        }
5152
5153        // Password visibility toggles
5154        if (cpWrapper) {
5155            cpWrapper.querySelectorAll('.swift-password-toggle').forEach(btn => {
5156                btn.addEventListener('click', () => {
5157                    const input = btn.parentElement.querySelector('input');
5158                    if (!input) return;
5159                    const isPassword = input.type === 'password';
5160                    input.type = isPassword ? 'text' : 'password';
5161                    btn.innerHTML = isPassword ? eyeOffSvg : eyeSvg;
5162                });
5163            });
5164        }
5165
5166        if (cpSubmit) {
5167            cpSubmit.addEventListener('click', async () => {
5168                const currentPw = container.querySelector('#swift-current-password')?.value || '';
5169                const newPw = container.querySelector('#swift-new-password')?.value || '';
5170                const confirmPw = container.querySelector('#swift-confirm-password')?.value || '';
5171
5172                if (cpStatus) { cpStatus.textContent = ''; cpStatus.style.color = ''; }
5173
5174                if (!currentPw || !newPw || !confirmPw) {
5175                    if (cpStatus) { cpStatus.textContent = 'All fields are required.'; cpStatus.style.color = '#dc3545'; }
5176                    return;
5177                }
5178                if (newPw.length < 8) {
5179                    if (cpStatus) { cpStatus.textContent = 'New password must be at least 8 characters.'; cpStatus.style.color = '#dc3545'; }
5180                    return;
5181                }
5182                if (newPw !== confirmPw) {
5183                    if (cpStatus) { cpStatus.textContent = 'New passwords do not match.'; cpStatus.style.color = '#dc3545'; }
5184                    return;
5185                }
5186
5187                cpSubmit.disabled = true;
5188                cpSubmit.textContent = 'Updating...';
5189
5190                try {
5191                    await changePassword(currentPw, newPw);
5192                    if (cpStatus) { cpStatus.textContent = 'Password changed successfully!'; cpStatus.style.color = '#28a745'; }
5193                    setTimeout(resetPasswordForm, 2000);
5194                } catch (err) {
5195                    console.error('SwiftAuth: Change password failed', err);
5196                    const msg = err.message || 'Failed to change password.';
5197                    if (cpStatus) { cpStatus.textContent = msg; cpStatus.style.color = '#dc3545'; }
5198                    cpSubmit.disabled = false;
5199                    cpSubmit.textContent = 'Update Password';
5200                }
5201            });
5202        }
5203
5204        // Manage Subscription button (Stripe portal)
5205        const manageSubBtn = container.querySelector('.swift-manage-sub-btn');
5206        if (manageSubBtn) {
5207            manageSubBtn.addEventListener('click', async (e) => {
5208                e.preventDefault();
5209                manageSubBtn.classList.add('swift-btn-loading');
5210                manageSubBtn.textContent = 'Opening...';
5211                try {
5212                    const result = await ajaxRequest('swift_checkout_portal', {
5213                        return_url: window.location.href,
5214                    });
5215                    if (result.success && result.portal_url) {
5216                        window.location.href = result.portal_url;
5217                    } else {
5218                        manageSubBtn.textContent = 'Manage Subscription';
5219                        manageSubBtn.classList.remove('swift-btn-loading');
5220                        alert(result.error || 'Could not open subscription portal.');
5221                    }
5222                } catch (err) {
5223                    manageSubBtn.textContent = 'Manage Subscription';
5224                    manageSubBtn.classList.remove('swift-btn-loading');
5225                    alert('Could not open subscription portal.');
5226                }
5227            });
5228        }
5229
5230        // Profile Subscribe Now button — track conversion source
5231        const profileSubBtn = container.querySelector('.swift-profile-subscribe-btn');
5232        if (profileSubBtn) {
5233            profileSubBtn.addEventListener('click', () => {
5234                setConversionSource('profile', window.location.href);
5235            });
5236        }
5237
5238        // Download Receipts button (Stripe portal)
5239        const receiptsBtn = container.querySelector('.swift-download-receipts-btn');
5240        if (receiptsBtn) {
5241            receiptsBtn.addEventListener('click', async (e) => {
5242                e.preventDefault();
5243                receiptsBtn.classList.add('swift-btn-loading');
5244                receiptsBtn.textContent = 'Opening...';
5245                try {
5246                    const result = await ajaxRequest('swift_checkout_portal', {
5247                        return_url: window.location.href,
5248                    });
5249                    if (result.success && result.portal_url) {
5250                        window.location.href = result.portal_url;
5251                    } else {
5252                        receiptsBtn.textContent = 'Download Receipts';
5253                        receiptsBtn.classList.remove('swift-btn-loading');
5254                        alert(result.error || 'Could not open billing portal.');
5255                    }
5256                } catch (err) {
5257                    receiptsBtn.textContent = 'Download Receipts';
5258                    receiptsBtn.classList.remove('swift-btn-loading');
5259                    alert('Could not open billing portal.');
5260                }
5261            });
5262        }
5263
5264        // Remove bookmark handlers
5265        container.querySelectorAll('[data-swift-remove-bookmark]').forEach(btn => {
5266            btn.addEventListener('click', async (e) => {
5267                e.preventDefault();
5268                const url = btn.getAttribute('data-swift-remove-bookmark');
5269                // Use postId if available - this is the key used in DynamoDB
5270                const postId = btn.getAttribute('data-post-id');
5271                const listItem = btn.closest('.swift-list-item');
5272
5273                if (listItem) {
5274                    listItem.style.opacity = '0.5';
5275                }
5276
5277                try {
5278                    // Remove from DynamoDB via API - prefer postId as it matches the DynamoDB key
5279                    await removeBookmark(postId || url);
5280
5281                    // Only remove from localStorage if not yet migrated
5282                    if (!state.localStorageMigrated) {
5283                        const pubID = config.pubID || '';
5284                        const key = pubID + 'bookmarkedPosts';
5285                        const stored = localStorage.getItem(key);
5286                        if (stored) {
5287                            const bookmarks = JSON.parse(stored);
5288                            const filtered = bookmarks.filter(b => {
5289                                const bUrl = b.articleUrl || b.bookmarkShortlink || b.url || '';
5290                                return bUrl !== url;
5291                            });
5292                            localStorage.setItem(key, JSON.stringify(filtered));
5293                        }
5294                    }
5295
5296                    initProfilePage();
5297                } catch (err) {
5298                    console.error('SwiftAuth: Remove bookmark failed', err);
5299                    if (listItem) {
5300                        listItem.style.opacity = '1';
5301                    }
5302                }
5303            });
5304        });
5305
5306        // Unfollow topic handlers
5307        container.querySelectorAll('[data-swift-unfollow-topic]').forEach(btn => {
5308            btn.addEventListener('click', async (e) => {
5309                e.preventDefault();
5310                const topicId = btn.getAttribute('data-swift-unfollow-topic');
5311                const tagEl = btn.closest('.swift-topic-tag');
5312
5313                if (tagEl) {
5314                    tagEl.style.opacity = '0.5';
5315                }
5316
5317                try {
5318                    // Use the unfollowTopic function (handles direct DynamoDB or AJAX)
5319                    const result = await unfollowTopic(topicId);
5320
5321                    if (!result.success) {
5322                        throw new Error(result.error || 'Failed to unfollow');
5323                    }
5324
5325                    initProfilePage();
5326                } catch (err) {
5327                    console.error('SwiftAuth: Unfollow topic failed', err);
5328                    if (tagEl) {
5329                        tagEl.style.opacity = '1';
5330                    }
5331                }
5332            });
5333        });
5334
5335        // Reading history toggle
5336        const historyToggle = container.querySelector('#swift-history-toggle');
5337        if (historyToggle) {
5338            historyToggle.addEventListener('change', (e) => {
5339                setReadingHistoryEnabled(e.target.checked);
5340                console.log('SwiftAuth: Reading history tracking', e.target.checked ? 'enabled' : 'disabled');
5341            });
5342        }
5343
5344        // Clear all history button
5345        const clearHistoryBtn = container.querySelector('#swift-clear-history');
5346        if (clearHistoryBtn) {
5347            clearHistoryBtn.addEventListener('click', async () => {
5348                if (confirm('Are you sure you want to clear all reading history? This cannot be undone.')) {
5349                    await clearReadingHistory();
5350                    initProfilePage();
5351                }
5352            });
5353        }
5354
5355        // Remove individual history items
5356        container.querySelectorAll('[data-swift-remove-history]').forEach(btn => {
5357            btn.addEventListener('click', async (e) => {
5358                e.preventDefault();
5359                const url = btn.getAttribute('data-swift-remove-history');
5360                const articleId = btn.getAttribute('data-article-id');
5361                const listItem = btn.closest('.swift-list-item');
5362
5363                if (listItem) {
5364                    listItem.style.opacity = '0.5';
5365                    listItem.style.transition = 'opacity 0.2s';
5366                }
5367
5368                try {
5369                    await removeReadingHistoryItem(url, articleId);
5370
5371                    // Animate out and re-render
5372                    setTimeout(() => {
5373                        initProfilePage();
5374                    }, 200);
5375                } catch (err) {
5376                    console.error('SwiftAuth: Remove history item failed', err);
5377                    if (listItem) {
5378                        listItem.style.opacity = '1';
5379                    }
5380                }
5381            });
5382        });
5383
5384        // Sign out button
5385        const signOutBtn = container.querySelector('#swift-sign-out');
5386        if (signOutBtn) {
5387            signOutBtn.addEventListener('click', async () => {
5388                signOutBtn.disabled = true;
5389                signOutBtn.textContent = 'Signing out...';
5390                try {
5391                    await logout();
5392                    window.location.href = '/';
5393                } catch (err) {
5394                    console.error('SwiftAuth: Sign out failed', err);
5395                    signOutBtn.disabled = false;
5396                    signOutBtn.textContent = 'Sign Out';
5397                }
5398            });
5399        }
5400
5401        // Delete account button
5402        const deleteAccountBtn = container.querySelector('#swift-delete-account');
5403        if (deleteAccountBtn) {
5404            deleteAccountBtn.addEventListener('click', async () => {
5405                await deleteAccount();
5406            });
5407        }
5408    }
5409
5410    // =========================================================================
5411    // UI Updates
5412    // =========================================================================
5413
5414    /**
5415     * Update UI based on auth state
5416     */
5417    function updateUI() {
5418        syncLoggedInSignal();
5419
5420        const authLevel = state.authLevel;
5421        const isAuthenticated = authLevel === 'full' || authLevel === 'lite';
5422
5423        // Update auth root
5424        const authRoots = document.querySelectorAll('[data-auth-root]');
5425        authRoots.forEach(root => {
5426            root.setAttribute('data-auth-level', authLevel);
5427        });
5428
5429        // Update auth nav elements (show/hide based on auth state)
5430        const guestNavEls = document.querySelectorAll('[data-swift-auth-nav="guest"]');
5431        const userNavEls = document.querySelectorAll('[data-swift-auth-nav="user"]');
5432
5433        guestNavEls.forEach(el => {
5434            el.classList.toggle('d-none', isAuthenticated);
5435        });
5436
5437        userNavEls.forEach(el => {
5438            el.classList.toggle('d-none', !isAuthenticated);
5439        });
5440
5441        // Update login buttons
5442        const loginButtons = document.querySelectorAll('[data-swift-login]');
5443        loginButtons.forEach(btn => {
5444            if (authLevel === 'full') {
5445                btn.textContent = btn.getAttribute('data-logged-in-text') || 'Account';
5446                btn.setAttribute('href', btn.getAttribute('data-profile-url') || '/profile/');
5447            } else {
5448                btn.textContent = btn.getAttribute('data-logged-out-text') || 'Sign In';
5449                btn.setAttribute('href', btn.getAttribute('data-login-url') || '/authentication/');
5450            }
5451        });
5452
5453        // Update bookmark buttons
5454        const bookmarkButtons = document.querySelectorAll('[data-swift-bookmark]');
5455        bookmarkButtons.forEach(btn => {
5456            // Find parent container (e.g., .toggle-bookmark) that may be hidden
5457            const container = btn.closest('.toggle-bookmark') || btn;
5458
5459            if (authLevel !== 'full') {
5460                btn.classList.add('swift-bookmark-disabled');
5461                btn.classList.remove('swift-bookmark-active');
5462                btn.setAttribute('title', 'Sign in to bookmark');
5463                // Show button for anonymous users (they can click to go to login)
5464                container.style.display = '';
5465            } else {
5466                btn.classList.remove('swift-bookmark-disabled');
5467                btn.removeAttribute('title');
5468                // Prefer post ID over URL for stability
5469                const postId = btn.getAttribute('data-post-id');
5470                const articleUrl = btn.getAttribute('data-article-url');
5471                const identifier = postId || articleUrl;
5472                // Check bookmark state and then show the button
5473                checkBookmark(identifier).then(response => {
5474                    btn.classList.toggle('swift-bookmark-active', response.bookmarked);
5475                    // Show the container after we know the state
5476                    container.style.display = '';
5477                });
5478            }
5479        });
5480
5481        // Unlock content and clear walls if authenticated
5482        if (authLevel !== 'anonymous') {
5483            unlockContent();
5484            hideRegwall();
5485            // Also hide paywall container
5486            const paywallContainer = document.getElementById('swift-paywall-container');
5487            if (paywallContainer) {
5488                paywallContainer.style.display = 'none';
5489            }
5490        }
5491
5492    }
5493
5494    /**
5495     * Update bookmark button states from DynamoDB
5496     * Called after UserDataManager is initialized
5497     */
5498    async function updateBookmarkButtonStates() {
5499        const bookmarkButtons = document.querySelectorAll('[data-swift-bookmark]');
5500        if (bookmarkButtons.length === 0) return;
5501
5502        console.log('SwiftAuth: Updating bookmark button states from DynamoDB');
5503
5504        for (const btn of bookmarkButtons) {
5505            const postId = btn.getAttribute('data-post-id');
5506            const articleUrl = btn.getAttribute('data-article-url');
5507            const identifier = postId || articleUrl;
5508
5509            if (!identifier) continue;
5510
5511            try {
5512                const response = await checkBookmark(identifier);
5513                console.log('SwiftAuth: Bookmark check for', identifier, '=', response.bookmarked);
5514                btn.classList.toggle('swift-bookmark-active', response.bookmarked);
5515
5516                // Show the container after we know the state
5517                const container = btn.closest('.toggle-bookmark') || btn;
5518                container.style.display = '';
5519            } catch (err) {
5520                console.warn('SwiftAuth: Could not check bookmark state for', identifier, err);
5521            }
5522        }
5523    }
5524
5525    /**
5526     * Initialize bookmark button handlers
5527     */
5528    function initBookmarkButtons() {
5529        document.querySelectorAll('[data-swift-bookmark]').forEach(btn => {
5530            btn.addEventListener('click', async (e) => {
5531                e.preventDefault();
5532
5533                if (state.authLevel !== 'full') {
5534                    // Store return URL and redirect to login
5535                    setReturnUrl(window.location.href);
5536                    window.location.href = '/authentication/';
5537                    return;
5538                }
5539
5540                // Prefer post ID over URL for stability (permalinks can change)
5541                const postId = btn.getAttribute('data-post-id');
5542                const articleUrl = btn.getAttribute('data-article-url') || getArticleUrl();
5543                const identifier = postId || articleUrl;
5544
5545                console.log('SwiftAuth: Bookmark button clicked', { postId, articleUrl, identifier });
5546
5547                try {
5548                    const response = await toggleBookmark(identifier);
5549                    console.log('SwiftAuth: toggleBookmark result:', response);
5550                    btn.classList.toggle('swift-bookmark-active', response.bookmarked);
5551                } catch (err) {
5552                    console.error('SwiftAuth: Bookmark failed', err);
5553                }
5554            });
5555        });
5556    }
5557
5558    /**
5559     * Initialize login button handlers
5560     */
5561    /**
5562     * Store return URL for post-login redirect
5563     * Uses sessionStorage so it only persists for the browser session
5564     */
5565    function setReturnUrl(url) {
5566        if (!url) return;
5567        try {
5568            sessionStorage.setItem('swift_auth_return_url', url);
5569            console.log('SwiftAuth: Stored return URL:', url);
5570        } catch (err) {
5571            console.warn('SwiftAuth: Could not store return URL', err);
5572        }
5573    }
5574
5575    /**
5576     * Store conversion attribution for subscribe flow
5577     * Tracks which button/wall led the user to subscribe
5578     */
5579    function setConversionSource(source, url) {
5580        try {
5581            sessionStorage.setItem('swift_conversion_source', source || 'direct');
5582            sessionStorage.setItem('swift_conversion_url', url || window.location.href);
5583        } catch (err) { /* silent */ }
5584        if (window.SwiftAnalytics) {
5585            window.SwiftAnalytics.trackBeginCheckout(source, url);
5586        }
5587    }
5588
5589    function getConversionSource() {
5590        try {
5591            return {
5592                source: sessionStorage.getItem('swift_conversion_source') || 'direct',
5593                url: sessionStorage.getItem('swift_conversion_url') || ''
5594            };
5595        } catch (err) {
5596            return { source: 'direct', url: '' };
5597        }
5598    }
5599
5600    /**
5601     * Get and clear the stored return URL
5602     * Returns null if no URL is stored
5603     */
5604    function getReturnUrl() {
5605        try {
5606            const url = sessionStorage.getItem('swift_auth_return_url');
5607            if (url) {
5608                sessionStorage.removeItem('swift_auth_return_url');
5609                console.log('SwiftAuth: Retrieved return URL:', url);
5610            }
5611            return url;
5612        } catch (err) {
5613            console.warn('SwiftAuth: Could not retrieve return URL', err);
5614            return null;
5615        }
5616    }
5617
5618    /**
5619     * Clear the stored return URL without returning it
5620     */
5621    function clearReturnUrl() {
5622        try {
5623            sessionStorage.removeItem('swift_auth_return_url');
5624        } catch (err) {
5625            // Ignore
5626        }
5627    }
5628
5629    function initLoginButtons() {
5630        document.querySelectorAll('[data-swift-login]:not(.swift-login-init)').forEach(btn => {
5631            btn.classList.add('swift-login-init');
5632            btn.addEventListener('click', (e) => {
5633                if (state.authLevel === 'full') {
5634                    // Let it navigate to profile
5635                    return;
5636                }
5637
5638                // Prevent default and show login modal or redirect
5639                e.preventDefault();
5640
5641                // Determine promotion_id from data-swift-wall-action or default to login
5642                const action = btn.getAttribute('data-swift-wall-action') || 'login';
5643
5644                // Record wall CTA click if this button is inside a wall
5645                const regwallContainer = btn.closest('.swift-regwall-modal, .swift-regwall-inline');
5646                if (regwallContainer) {
5647                    const wallType = regwallContainer.getAttribute('data-wall-type') || 'soft';
5648                    const wallName = regwallContainer.getAttribute('data-wall-name') || '';
5649                    if (state.userDataManager && state.authLevel !== 'full') {
5650                        state.userDataManager.recordWallEvent('regwall', wallType, 'cta_clicked');
5651                    }
5652                    if (window.SwiftAnalytics) {
5653                        const actionSuffix = action === 'register' ? 'ClickReg' : 'ClickLogin';
5654                        window.SwiftAnalytics.trackWallSelectPromotion(wallType, actionSuffix, wallName, !!config.paywall?.enabled);
5655                    }
5656                }
5657                const paywallContainer = btn.closest('.swift-paywall-modal, .swift-paywall-inline-hard, .swift-paywall-inline-soft');
5658                if (paywallContainer) {
5659                    const wallType = paywallContainer.getAttribute('data-wall-type') || 'hard';
5660                    const wallName = paywallContainer.getAttribute('data-wall-name') || '';
5661                    if (state.userDataManager && state.authLevel !== 'full') {
5662                        state.userDataManager.recordWallEvent('paywall', wallType, 'cta_clicked');
5663                    }
5664                    if (window.SwiftAnalytics) {
5665                        window.SwiftAnalytics.trackWallSelectPromotion(wallType, 'ClickLogin', wallName, !!config.paywall?.enabled);
5666                    }
5667                }
5668
5669                // Store current URL if on an article page (has regwall/paywall content)
5670                const article = document.querySelector('.p402_premium');
5671                if (article) {
5672                    setReturnUrl(window.location.href);
5673                }
5674
5675                window.location.href = '/authentication/';
5676            });
5677        });
5678
5679        // "Why register/subscribe" links inside walls
5680        // These are standard <a> links — we just fire the GA event before navigation
5681        document.querySelectorAll('[data-swift-wall-action="why"]:not(.swift-why-init)').forEach(link => {
5682            link.classList.add('swift-why-init');
5683            link.addEventListener('click', () => {
5684                if (window.SwiftAnalytics) {
5685                    const wallContainer = link.closest('.swift-regwall-modal, .swift-regwall-inline, .swift-paywall-modal, .swift-paywall-inline-hard, .swift-paywall-inline-soft');
5686                    if (wallContainer) {
5687                        const wallType = wallContainer.getAttribute('data-wall-type') || 'soft';
5688                        const wallName = wallContainer.getAttribute('data-wall-name') || '';
5689                        window.SwiftAnalytics.trackWallSelectPromotion(wallType, 'ClickMore', wallName, !!config.paywall?.enabled);
5690                    }
5691                }
5692                // Don't preventDefault — let the link navigate naturally
5693            });
5694        });
5695    }
5696
5697    /**
5698     * Check if a topic is followed
5699     */
5700    function isTopicFollowed(topicId) {
5701        return state.followedTopics.some(t => {
5702            const tId = (typeof t === 'object') ? (t.topicId || t.id) : t;
5703            return tId === topicId;
5704        });
5705    }
5706
5707    /**
5708     * Follow a topic
5709     * Uses direct DynamoDB when UserDataManager is ready
5710     */
5711    async function followTopic(topicId, topicName) {
5712        if (!topicId) {
5713            console.warn('SwiftAuth: Cannot follow topic without ID');
5714            return { success: false, error: 'Missing topic ID' };
5715        }
5716
5717        // Check if already followed
5718        if (isTopicFollowed(topicId)) {
5719            return { success: true, alreadyFollowed: true };
5720        }
5721
5722        const newTopic = { topicId, topicName: topicName || topicId };
5723
5724        // Direct DynamoDB only - no AJAX fallback for user data
5725        if (!isDirectDynamoEnabled()) {
5726            console.error('SwiftAuth: DynamoDB not initialized - cannot follow topic');
5727            return { success: false, error: 'DynamoDB not initialized' };
5728        }
5729
5730        try {
5731            const result = await state.userDataManager.followTopic(newTopic);
5732            state.followedTopics = [...state.followedTopics, result || newTopic];
5733            window.dispatchEvent(new CustomEvent('swiftauth:topics_changed', {
5734                detail: { topics: state.followedTopics }
5735            }));
5736            return { success: true, followed: true };
5737        } catch (error) {
5738            console.error('SwiftAuth: followTopic failed:', error);
5739            return { success: false, error: error.message };
5740        }
5741    }
5742
5743    /**
5744     * Unfollow a topic
5745     * Uses direct DynamoDB when UserDataManager is ready
5746     */
5747    async function unfollowTopic(topicId) {
5748        if (!topicId) {
5749            return { success: false, error: 'Missing topic ID' };
5750        }
5751
5752        // Direct DynamoDB only - no AJAX fallback for user data
5753        if (!isDirectDynamoEnabled()) {
5754            console.error('SwiftAuth: DynamoDB not initialized - cannot unfollow topic');
5755            return { success: false, error: 'DynamoDB not initialized' };
5756        }
5757
5758        try {
5759            await state.userDataManager.unfollowTopic(topicId);
5760            state.followedTopics = state.followedTopics.filter(t => {
5761                const tId = (typeof t === 'object') ? (t.topicId || t.id) : t;
5762                return tId !== topicId;
5763            });
5764            window.dispatchEvent(new CustomEvent('swiftauth:topics_changed', {
5765                detail: { topics: state.followedTopics }
5766            }));
5767            return { success: true, unfollowed: true };
5768        } catch (error) {
5769            console.error('SwiftAuth: unfollowTopic failed:', error);
5770            return { success: false, error: error.message };
5771        }
5772    }
5773
5774    /**
5775     * Toggle follow state for a topic
5776     */
5777    async function toggleFollowTopic(topicId, topicName) {
5778        if (isTopicFollowed(topicId)) {
5779            return unfollowTopic(topicId);
5780        } else {
5781            return followTopic(topicId, topicName);
5782        }
5783    }
5784
5785    /**
5786     * Update follow topic button UI
5787     * Theme convention:
5788     *   - No 'follow-topic' class = "Following" state (solid blue, checkmark)
5789     *   - With 'follow-topic' class = "Follow" state (outlined, plus icon)
5790     */
5791    function updateFollowTopicButton(btn, isFollowed) {
5792        // Theme class: add 'follow-topic' when NOT following, remove when following
5793        btn.classList.toggle('follow-topic', !isFollowed);
5794        // Plugin class for any custom styling
5795        btn.classList.toggle('swift-topic-following', isFollowed);
5796        btn.setAttribute('data-following', isFollowed ? 'true' : 'false');
5797
5798        // Update button text if it has a text element (theme handles icons via CSS)
5799        const textEl = btn.querySelector('[data-follow-text]');
5800        if (textEl) {
5801            const topicName = btn.getAttribute('data-topic-name') || btn.getAttribute('data-topic');
5802            textEl.textContent = isFollowed ? `Following ${topicName}` : `Follow ${topicName}`;
5803        }
5804    }
5805
5806    /**
5807     * Initialize follow topic button handlers
5808     */
5809    function initFollowTopicButtons() {
5810        const buttons = document.querySelectorAll('[data-topic]');
5811        const isArticlePage = document.querySelector('.p402_premium') !== null;
5812        const isProfilePage = document.querySelector('#swift-profile-root') !== null;
5813
5814        buttons.forEach(btn => {
5815            const topicId = btn.getAttribute('data-topic');
5816            const topicName = btn.getAttribute('data-topic-name') || topicId;
5817
5818            // Set initial state
5819            const isFollowed = isTopicFollowed(topicId);
5820            updateFollowTopicButton(btn, isFollowed);
5821
5822            btn.addEventListener('click', async (e) => {
5823                e.preventDefault();
5824
5825                if (state.authLevel !== 'full') {
5826                    // Redirect to login
5827                    window.location.href = '/authentication/';
5828                    return;
5829                }
5830
5831                // Disable button during request
5832                btn.disabled = true;
5833                const wasFollowed = isTopicFollowed(topicId);
5834
5835                try {
5836                    const result = await toggleFollowTopic(topicId, topicName);
5837
5838                    if (result.success) {
5839                        updateFollowTopicButton(btn, !wasFollowed);
5840                    }
5841                } catch (err) {
5842                    console.error('SwiftAuth: Toggle follow topic failed', err);
5843                } finally {
5844                    btn.disabled = false;
5845                }
5846            });
5847        });
5848
5849        // Reveal topic buttons on article pages after state has been set
5850        // (Theme hides them with [data-topic] { display: none; } until data is loaded)
5851        if (isArticlePage && !isProfilePage) {
5852            buttons.forEach(btn => {
5853                btn.style.display = 'inline-flex';
5854            });
5855        }
5856    }
5857
5858    /**
5859     * Initialize follow buttons on staff profile pages
5860     * Detects [data-swift-follow-author] containers and renders follow buttons
5861     */
5862    function initAuthorFollowContainers() {
5863        const containers = document.querySelectorAll('[data-swift-follow-author]');
5864        if (!containers.length) return;
5865
5866        // Derive author slug from URL: /profile/robert-tann/ → author-robert-tann
5867        const pathParts = window.location.pathname.replace(/\/$/, '').split('/');
5868        const slug = pathParts[pathParts.length - 1];
5869        const topicId = 'author-' + slug;
5870
5871        containers.forEach(container => {
5872            const rawName = container.getAttribute('data-swift-follow-author-name') || slug;
5873
5874            // Build and inject the button
5875            const btn = document.createElement('button');
5876            btn.setAttribute('data-topic', topicId);
5877            btn.setAttribute('data-topic-name', rawName);
5878            btn.setAttribute('type', 'button');
5879            btn.innerHTML = `<span data-follow-text></span>`;
5880            container.appendChild(btn);
5881
5882            // Set initial state using shared utility
5883            updateFollowTopicButton(btn, isTopicFollowed(topicId));
5884
5885            btn.addEventListener('click', async (e) => {
5886                e.preventDefault();
5887                if (state.authLevel !== 'full') {
5888                    window.location.href = '/authentication/';
5889                    return;
5890                }
5891                btn.disabled = true;
5892                const wasFollowed = isTopicFollowed(topicId);
5893                try {
5894                    const result = await toggleFollowTopic(topicId, rawName);
5895                    if (result.success) {
5896                        updateFollowTopicButton(btn, !wasFollowed);
5897                    }
5898                } catch (err) {
5899                    console.error('SwiftAuth: Author follow failed', err);
5900                } finally {
5901                    btn.disabled = false;
5902                }
5903            });
5904        });
5905    }
5906
5907    // =========================================================================
5908    // Social Sign-In (Google & Facebook)
5909    // =========================================================================
5910
5911    let googleClient = null;
5912
5913    /**
5914     * Initialize Google Identity Services
5915     * Call this on page load for the authentication page
5916     */
5917    function initGoogleSignIn() {
5918        if (!config.social?.googleEnabled || !config.social?.googleClientId) {
5919            console.log('SwiftAuth: Google Sign-In not configured');
5920            return;
5921        }
5922
5923        if (typeof google === 'undefined') {
5924            console.log('SwiftAuth: Google Identity Services SDK not loaded yet');
5925            return;
5926        }
5927
5928        // Initialize the Google Sign-In client
5929        googleClient = google.accounts.oauth2.initTokenClient({
5930            client_id: config.social.googleClientId,
5931            scope: 'email profile openid',
5932            callback: handleGoogleResponse,
5933        });
5934
5935        console.log('SwiftAuth: Google Sign-In initialized');
5936    }
5937
5938    /**
5939     * Handle Google Sign-In button click
5940     * Opens Google popup for authentication
5941     */
5942    function signInWithGoogle() {
5943        console.log('SwiftAuth: signInWithGoogle() called');
5944        console.log('SwiftAuth: googleClient:', googleClient);
5945        console.log('SwiftAuth: config.social:', config.social);
5946
5947        if (!googleClient) {
5948            console.log('SwiftAuth: Google client not initialized, falling back to OAuth redirect');
5949            // Fallback to direct Cognito OAuth redirect
5950            initiateOAuthSignIn('Google');
5951            return;
5952        }
5953
5954        console.log('SwiftAuth: Requesting Google access token via popup...');
5955        // Request access token via popup
5956        googleClient.requestAccessToken();
5957    }
5958
5959    /**
5960     * Handle response from Google popup
5961     * After successful Google auth, redirect to Cognito OAuth
5962     */
5963    function handleGoogleResponse(response) {
5964        if (response.error) {
5965            console.error('SwiftAuth: Google Sign-In error', response.error);
5966            showSocialAuthError('Google sign-in failed. Please try again.');
5967            return;
5968        }
5969
5970        console.log('SwiftAuth: Google authentication successful, redirecting to Cognito...');
5971
5972        // Google session is now established in browser
5973        // Redirect to Cognito OAuth - Google will recognize session (instant)
5974        initiateOAuthSignIn('Google');
5975    }
5976
5977    /**
5978     * Handle Facebook Sign-In button click
5979     * Redirects directly to Cognito OAuth which handles the full Facebook flow
5980     */
5981    function signInWithFacebook() {
5982        console.log('SwiftAuth: signInWithFacebook() called');
5983        initiateOAuthSignIn('Facebook');
5984    }
5985
5986    /**
5987     * Show error message for social auth failures
5988     */
5989    function showSocialAuthError(message) {
5990        // Dispatch event for theme to handle
5991        document.dispatchEvent(new CustomEvent('swiftauth:social_error', {
5992            detail: { message }
5993        }));
5994
5995        // Also try to show in any visible error container
5996        const errorContainers = document.querySelectorAll('[data-swift-auth-error], .swift-auth-error');
5997        errorContainers.forEach(container => {
5998            container.textContent = message;
5999            container.style.display = 'block';
6000        });
6001    }
6002
6003    /**
6004     * Redirect to Cognito OAuth endpoint with identity_provider parameter
6005     * Called AFTER provider popup authentication succeeds
6006     * Provider will recognize the session, making this redirect instant
6007     */
6008    function initiateOAuthSignIn(provider) {
6009        console.log('SwiftAuth: initiateOAuthSignIn() called with provider:', provider);
6010
6011        let domain = config.cognito?.domain;
6012        const clientId = config.cognito?.clientId;
6013
6014        // Strip https:// or http:// prefix if present (user may have entered full URL)
6015        if (domain) {
6016            domain = domain.replace(/^https?:\/\//, '');
6017        }
6018
6019        console.log('SwiftAuth: Cognito domain:', domain);
6020        console.log('SwiftAuth: Cognito clientId:', clientId);
6021
6022        if (!domain) {
6023            console.error('SwiftAuth: Cognito domain not configured!');
6024            console.error('SwiftAuth: Make sure to set the Cognito Domain in WordPress admin: Settings > Swift Auth > Authentication');
6025            showSocialAuthError('Social sign-in is not configured properly. Please contact the site administrator.');
6026            return;
6027        }
6028
6029        // Use current origin for redirect - supports multiple custom domains
6030        const redirectUri = encodeURIComponent(window.location.origin + '/authentication/');
6031
6032        // Check if we already have a return URL stored (from clicking login on article page)
6033        // If not, store current page (unless it's /authentication/)
6034        const existingReturnUrl = sessionStorage.getItem('swift_auth_return_url');
6035        if (!existingReturnUrl && !window.location.pathname.includes('/authentication')) {
6036            sessionStorage.setItem('swift_auth_return_url', window.location.href);
6037        }
6038
6039        const url = `https://${domain}/oauth2/authorize?` +
6040            `identity_provider=${provider}&` +
6041            `redirect_uri=${redirectUri}&` +
6042            `response_type=code&` +
6043            `client_id=${clientId}&` +
6044            `scope=aws.cognito.signin.user.admin+email+openid+profile`;
6045
6046        console.log(`SwiftAuth: Redirecting to Cognito OAuth URL:`, url);
6047        window.location.href = url;
6048    }
6049
6050    /**
6051     * Handle OAuth callback on /authentication/ page
6052     * Called when user returns from social provider with auth code
6053     */
6054    async function handleOAuthCallback() {
6055        const urlParams = new URLSearchParams(window.location.search);
6056        const code = urlParams.get('code');
6057        const error = urlParams.get('error');
6058        const errorDescription = urlParams.get('error_description');
6059
6060        // Hide auth page forms and show loading indicator
6061        const authPage = document.querySelector('[data-authentication]');
6062        if (authPage) {
6063            authPage.innerHTML = `
6064                <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;text-align:center;">
6065                    <div class="swift-oauth-spinner"></div>
6066                    <p style="margin-top:16px;font-size:1rem;color:#555;">Logging in...</p>
6067                </div>
6068            `;
6069            // Inject spinner styles if not present
6070            if (!document.getElementById('swift-oauth-spinner-style')) {
6071                const style = document.createElement('style');
6072                style.id = 'swift-oauth-spinner-style';
6073                style.textContent = `
6074                    .swift-oauth-spinner {
6075                        width: 36px; height: 36px;
6076                        border: 3px solid #e0e0e0;
6077                        border-top-color: #0074d4;
6078                        border-radius: 50%;
6079                        animation: swift-oauth-spin 0.8s linear infinite;
6080                    }
6081                    @keyframes swift-oauth-spin { to { transform: rotate(360deg); } }
6082                `;
6083                document.head.appendChild(style);
6084            }
6085        }
6086
6087        // Handle OAuth errors
6088        if (error) {
6089            console.error('OAuth error:', error, errorDescription);
6090            window.location.href = '/authentication/?auth_error=' + encodeURIComponent(errorDescription || 'Social sign-in failed');
6091            return;
6092        }
6093
6094        if (!code) {
6095            return; // No code, not an OAuth callback
6096        }
6097
6098        console.log('SwiftAuth: Processing OAuth callback');
6099
6100        try {
6101            // Fetch a fresh nonce BEFORE any AJAX calls. ajaxRequest() also does
6102            // this lazily, but resolving it up front keeps the OAuth flow explicit.
6103            await fetchNonce();
6104
6105            // Strip the authorization code from the URL immediately so it doesn't
6106            // leak via Referer headers, ad network scripts, or analytics.
6107            window.history.replaceState({}, '', '/authentication/');
6108
6109            // Exchange authorization code for tokens
6110            const tokens = await exchangeCodeForTokens(code);
6111
6112            if (!tokens || !tokens.id_token) {
6113                throw new Error('Failed to exchange code for tokens');
6114            }
6115
6116            // Store tokens in localStorage for Cognito JS SDK compatibility
6117            // This allows getCurrentUser() to work on subsequent page loads
6118            storeCognitoTokens(tokens);
6119
6120            // Complete auth flow with WordPress (same as email/password login)
6121            // This is the critical path - creates WordPress session cookie
6122            const response = await ajaxRequest('swift_auth_complete', {
6123                id_token: tokens.id_token,
6124            });
6125
6126            if (response.success) {
6127                state.authLevel = 'full';
6128                state.sub = response.sub;
6129
6130                console.log('SwiftAuth: Social sign-in complete');
6131
6132                // Emit login event for authBridge to trigger hydration
6133                window.dispatchEvent(new CustomEvent('swiftauth:login'));
6134
6135                // Await profile sync before redirect (matching login() flow)
6136                // MUST be awaited so profile data exists when profile page loads
6137                try {
6138                    const dmSuccess = await initUserDataManager(tokens.id_token);
6139                    console.log('SwiftAuth: OAuth initUserDataManager result:', dmSuccess);
6140                    if (dmSuccess) {
6141                        await syncUserProfileFromToken(tokens.id_token);
6142                        // Record login event for dashboard analytics (fire-and-forget)
6143                        if (state.userDataManager) state.userDataManager.recordLogin();
6144                    } else {
6145                        console.warn('SwiftAuth: UserDataManager init failed — profile will not sync to DynamoDB');
6146                        console.warn('SwiftAuth: initError:', state.userDataManager?.initError?.message);
6147                    }
6148                } catch (e) {
6149                    console.log('SwiftAuth: Profile sync failed (non-blocking)', e);
6150                }
6151
6152                // Pinpoint stays fire-and-forget (not needed before redirect)
6153                try {
6154                    const tokenParts = tokens.id_token.split('.');
6155                    const payload = JSON.parse(atob(tokenParts[1]));
6156                    if (payload.email) {
6157                        updatePinpointEndpoint(response.sub, payload.email);
6158                    }
6159                } catch (e) {
6160                    console.log('SwiftAuth: Pinpoint update failed (non-blocking)', e);
6161                }
6162
6163                // Dispatch success event
6164                document.dispatchEvent(new CustomEvent('swiftauth:social_success', {
6165                    detail: { sub: response.sub }
6166                }));
6167
6168                // Redirect back to original page (article) if stored, otherwise homepage
6169                // Check if user needs onboarding (hasn't completed it yet)
6170                if (state.userDataManager && state.userDataManagerReady) {
6171                    try {
6172                        const profile = await state.userDataManager.getProfile();
6173                        if (!profile.onboardingCompleted) {
6174                            console.log('SwiftAuth: New user needs onboarding, redirecting...');
6175                            window.location.href = '/onboarding/';
6176                            return;
6177                        }
6178                    } catch (err) {
6179                        console.warn('SwiftAuth: Could not check profile for onboarding:', err);
6180                        // Continue with normal redirect if profile check fails
6181                    }
6182                }
6183
6184                // Use swift_auth_return_url which is set when user clicks login from article
6185                const redirectUrl = sessionStorage.getItem('swift_auth_return_url');
6186                sessionStorage.removeItem('swift_auth_return_url');
6187
6188                if (redirectUrl && !redirectUrl.includes('/authentication') && !redirectUrl.includes('/logout')) {
6189                    console.log('SwiftAuth: Redirecting to stored return URL:', redirectUrl);
6190                    window.location.href = redirectUrl;
6191                } else {
6192                    console.log('SwiftAuth: No return URL, redirecting to homepage');
6193                    window.location.href = '/';
6194                }
6195            } else {
6196                throw new Error(response.error || 'Authentication failed');
6197            }
6198
6199        } catch (err) {
6200            console.error('SwiftAuth: OAuth callback error', err);
6201            // Redirect to clean auth page so form is visible again with error
6202            window.location.href = '/authentication/?auth_error=' + encodeURIComponent(err.message || 'Sign-in failed. Please try again.');
6203        }
6204    }
6205
6206    /**
6207     * Store OAuth tokens in localStorage for Cognito JS SDK compatibility
6208     * This allows getCurrentUser() to work on subsequent page loads
6209     *
6210     * @param {object} tokens - The tokens from Cognito token endpoint
6211     * @param {string} tokens.id_token - The ID token (JWT)
6212     * @param {string} tokens.access_token - The access token
6213     * @param {string} tokens.refresh_token - The refresh token (optional)
6214     */
6215    function storeCognitoTokens(tokens) {
6216        const clientId = config.cognito?.clientId;
6217        if (!clientId || !tokens.id_token) {
6218            console.error('SwiftAuth: Cannot store tokens - missing clientId or id_token');
6219            return;
6220        }
6221
6222        try {
6223            // Decode the ID token to get the username (sub)
6224            const tokenParts = tokens.id_token.split('.');
6225            const payload = JSON.parse(atob(tokenParts[1]));
6226            const username = payload.sub || payload['cognito:username'];
6227
6228            if (!username) {
6229                console.error('SwiftAuth: Cannot store tokens - no username in token');
6230                return;
6231            }
6232
6233            // Store tokens in the format Cognito JS SDK expects
6234            const keyPrefix = `CognitoIdentityServiceProvider.${clientId}`;
6235
6236            localStorage.setItem(`${keyPrefix}.${username}.idToken`, tokens.id_token);
6237            localStorage.setItem(`${keyPrefix}.${username}.accessToken`, tokens.access_token);
6238            if (tokens.refresh_token) {
6239                localStorage.setItem(`${keyPrefix}.${username}.refreshToken`, tokens.refresh_token);
6240            }
6241            localStorage.setItem(`${keyPrefix}.LastAuthUser`, username);
6242
6243            // Also store clock drift (required by SDK)
6244            localStorage.setItem(`${keyPrefix}.${username}.clockDrift`, '0');
6245
6246            console.log('SwiftAuth: Cognito tokens stored in localStorage for user:', username);
6247
6248        } catch (err) {
6249            console.error('SwiftAuth: Failed to store Cognito tokens:', err.message);
6250        }
6251    }
6252
6253    /**
6254     * Exchange authorization code for tokens via Cognito's token endpoint
6255     */
6256    async function exchangeCodeForTokens(code) {
6257        let domain = config.cognito?.domain;
6258        const clientId = config.cognito?.clientId;
6259        const redirectUri = window.location.origin + '/authentication/';
6260
6261        // Strip https:// or http:// prefix if present
6262        if (domain) {
6263            domain = domain.replace(/^https?:\/\//, '');
6264        }
6265
6266        const tokenUrl = `https://${domain}/oauth2/token`;
6267
6268        const params = new URLSearchParams({
6269            grant_type: 'authorization_code',
6270            client_id: clientId,
6271            code: code,
6272            redirect_uri: redirectUri,
6273        });
6274
6275        const response = await fetch(tokenUrl, {
6276            method: 'POST',
6277            headers: {
6278                'Content-Type': 'application/x-www-form-urlencoded',
6279            },
6280            body: params.toString(),
6281        });
6282
6283        if (!response.ok) {
6284            const errorText = await response.text();
6285            console.error('Token exchange failed:', errorText);
6286            throw new Error('Failed to complete sign-in');
6287        }
6288
6289        const tokens = await response.json();
6290        // Returns: { id_token, access_token, refresh_token, expires_in, token_type }
6291
6292        return tokens;
6293    }
6294
6295    /**
6296     * Check for OAuth callback on page load
6297     * Should be called during initialization on /authentication/ page
6298     */
6299    function checkOAuthCallback() {
6300        const urlParams = new URLSearchParams(window.location.search);
6301        if (urlParams.has('code') || urlParams.has('error')) {
6302            handleOAuthCallback();
6303            return true;
6304        }
6305        return false;
6306    }
6307
6308    /**
6309     * Initialize social sign-in SDKs and button handlers
6310     * Call this when the authentication page loads
6311     */
6312    function initSocialSignIn() {
6313        console.log('SwiftAuth: initSocialSignIn() called');
6314        console.log('SwiftAuth: config.social =', config.social);
6315
6316        // Initialize Google Identity Services
6317        if (config.social?.googleEnabled) {
6318            console.log('SwiftAuth: Google is enabled, checking for SDK...');
6319            if (typeof google !== 'undefined' && google.accounts) {
6320                console.log('SwiftAuth: Google SDK already loaded');
6321                initGoogleSignIn();
6322            } else {
6323                console.log('SwiftAuth: Google SDK not loaded yet, waiting...');
6324                // Wait for Google SDK to load
6325                window.addEventListener('load', () => {
6326                    setTimeout(() => {
6327                        if (typeof google !== 'undefined' && google.accounts) {
6328                            console.log('SwiftAuth: Google SDK loaded after wait');
6329                            initGoogleSignIn();
6330                        } else {
6331                            console.log('SwiftAuth: Google SDK still not available');
6332                        }
6333                    }, 100);
6334                });
6335            }
6336        } else {
6337            console.log('SwiftAuth: Google sign-in is NOT enabled');
6338        }
6339
6340        // Facebook: no SDK needed — Cognito handles the full OAuth flow
6341
6342        // Initialize social sign-in button handlers using event delegation
6343        initSocialSignInButtons();
6344    }
6345
6346    /**
6347     * Initialize social sign-in button click handlers
6348     * Uses data attributes for buttons: [data-swift-google], [data-swift-facebook]
6349     */
6350    function initSocialSignInButtons() {
6351        console.log('SwiftAuth: Setting up social sign-in button handlers...');
6352
6353        // Show/hide social sign-in containers and buttons based on config
6354        // (This also runs in authentication-page.html inline script, but we do it
6355        //  here as well to ensure it works even if inline scripts are stripped)
6356        if (config.social?.googleEnabled || config.social?.facebookEnabled) {
6357            document.querySelectorAll('.swift-social-signin').forEach(el => {
6358                el.style.display = 'block';
6359            });
6360        }
6361        if (!config.social?.googleEnabled) {
6362            document.querySelectorAll('[data-swift-google]').forEach(btn => {
6363                btn.style.display = 'none';
6364            });
6365        }
6366        if (!config.social?.facebookEnabled) {
6367            document.querySelectorAll('[data-swift-facebook]').forEach(btn => {
6368                btn.style.display = 'none';
6369            });
6370        }
6371
6372        // Log what buttons exist on the page
6373        const googleBtns = document.querySelectorAll('[data-swift-google]');
6374        const facebookBtns = document.querySelectorAll('[data-swift-facebook]');
6375        console.log('SwiftAuth: Found Google buttons:', googleBtns.length);
6376        console.log('SwiftAuth: Found Facebook buttons:', facebookBtns.length);
6377
6378        // Direct attachment to existing buttons (more reliable)
6379        googleBtns.forEach((btn, i) => {
6380            console.log('SwiftAuth: Attaching click handler to Google button', i);
6381            btn.addEventListener('click', (e) => {
6382                e.preventDefault();
6383                e.stopPropagation();
6384                console.log('SwiftAuth: Google button clicked (direct handler)!');
6385                signInWithGoogle();
6386            });
6387        });
6388
6389        facebookBtns.forEach((btn, i) => {
6390            console.log('SwiftAuth: Attaching click handler to Facebook button', i);
6391            btn.addEventListener('click', (e) => {
6392                e.preventDefault();
6393                e.stopPropagation();
6394                console.log('SwiftAuth: Facebook button clicked (direct handler)!');
6395                signInWithFacebook();
6396            });
6397        });
6398
6399        // Also use event delegation for dynamically added buttons
6400        document.addEventListener('click', (e) => {
6401            // Google sign-in button
6402            const googleBtn = e.target.closest('[data-swift-google]');
6403            if (googleBtn) {
6404                e.preventDefault();
6405                e.stopPropagation();
6406                console.log('SwiftAuth: Google sign-in button clicked (delegation)!');
6407                console.log('SwiftAuth: Social config:', config.social);
6408                signInWithGoogle();
6409                return;
6410            }
6411
6412            // Facebook sign-in button
6413            const facebookBtn = e.target.closest('[data-swift-facebook]');
6414            if (facebookBtn) {
6415                e.preventDefault();
6416                e.stopPropagation();
6417                console.log('SwiftAuth: Facebook sign-in button clicked (delegation)!');
6418                console.log('SwiftAuth: Social config:', config.social);
6419                signInWithFacebook();
6420                return;
6421            }
6422        });
6423
6424        console.log('SwiftAuth: Social sign-in button handlers initialized');
6425    }
6426
6427    /**
6428     * Check if social sign-in is enabled
6429     */
6430    function isSocialSignInEnabled() {
6431        return config.social?.googleEnabled || config.social?.facebookEnabled;
6432    }
6433
6434    // =========================================================================
6435    // Initialization
6436    // =========================================================================
6437
6438    /**
6439     * Refresh auth status from server
6440     * This handles cached pages where server-rendered config may be stale
6441     */
6442    async function refreshAuthStatus() {
6443        try {
6444            const response = await ajaxRequest('swift_auth_status', {}, 'POST', false);
6445
6446            if (response.success) {
6447                const newAuthLevel = response.auth_level || 'anonymous';
6448                const newSub = response.sub || null;
6449
6450                // Never let the server downgrade a lite-login session.
6451                // The server has no knowledge of localStorage tokens.
6452                if (state.authLevel === 'lite' && newAuthLevel === 'anonymous') {
6453                    return;
6454                }
6455
6456                // Only update if state changed
6457                if (state.authLevel !== newAuthLevel || state.sub !== newSub) {
6458                    console.log('SwiftAuth: Auth state updated from server', {
6459                        old: state.authLevel,
6460                        new: newAuthLevel,
6461                    });
6462
6463                    state.authLevel = newAuthLevel;
6464                    state.sub = newSub;
6465                    state.sessionExpires = response.expires_at || null;
6466
6467                    // Re-update UI with correct state
6468                    updateUI();
6469                }
6470            }
6471        } catch (err) {
6472            console.error('SwiftAuth: Failed to refresh auth status', err);
6473        }
6474    }
6475
6476    /**
6477     * Check local Cognito session immediately on load
6478     * Updates state and UI based on local storage, without hitting server
6479     */
6480    function checkLocalSession() {
6481        if (!window.swiftAuthUserPool) {
6482            return;
6483        }
6484
6485        // If logout() was called before init(), don't re-elevate
6486        if (state.loggedOut) {
6487            console.log('SwiftAuth: Skipping local session check — logout in progress');
6488            return;
6489        }
6490
6491        const cognitoUser = window.swiftAuthUserPool.getCurrentUser();
6492
6493        if (cognitoUser) {
6494            // We have a user in localStorage, assuming logged in for now
6495            // The actual token validity will be checked by initUserDataManagerFromCognitoSession
6496            // But for UI "flash" prevention, this is a good signal
6497            console.log('SwiftAuth: Local Cognito user found:', cognitoUser.getUsername());
6498
6499            // If config says anonymous but we have a user, optimistically set to full
6500            // This fixes the case where cached page says anonymous but user is logged in
6501            if (state.authLevel === 'anonymous') {
6502                console.log('SwiftAuth: Optimistically setting auth level to full based on local session');
6503                state.authLevel = 'full';
6504                state.sub = cognitoUser.getUsername(); // sub is usually the username in Cognito
6505                updateUI();
6506            }
6507        } else {
6508            // No user in localStorage
6509            console.log('SwiftAuth: No local Cognito user found');
6510
6511            // If config says logged in but we have no user, downgrade to anonymous.
6512            // Do NOT downgrade 'lite' — lite-login sessions have no Cognito token by design.
6513            if (state.authLevel === 'full') {
6514                console.log('SwiftAuth: Downgrading auth level to anonymous based on missing local session');
6515                state.authLevel = 'anonymous';
6516                state.sub = null;
6517                updateUI();
6518            }
6519        }
6520    }
6521
6522    /**
6523     * Initialize the plugin
6524     */
6525    async function init() {
6526        console.log('SwiftAuth: init() starting...');
6527
6528        if (state.initialized) {
6529            console.log('SwiftAuth: Already initialized, skipping');
6530            return;
6531        }
6532        state.initialized = true;
6533
6534        // Initialize Cognito
6535        initCognito();
6536
6537        // Check local Cognito session for optimistic auth (no network)
6538        checkLocalSession();
6539
6540        // Check localStorage for an existing magic (lite-login) token.
6541        // This runs before any network calls so the UI is correct immediately.
6542        if (state.authLevel === 'anonymous') {
6543            const magicSession = getMagicToken();
6544            if (magicSession) {
6545                state.authLevel = 'lite';
6546                state.sub = magicSession.payload.sub || null;
6547                console.log('SwiftAuth: Restored lite-login from localStorage');
6548            }
6549        }
6550
6551        // Signal that initialization is complete (optimistic state)
6552        window.SwiftAuth.isInitialized = true;
6553        window.dispatchEvent(new CustomEvent('swiftauth:initialized'));
6554
6555        // Update UI with optimistic state
6556        updateUI();
6557
6558        // Check for OAuth callback FIRST (before other init)
6559        // This handles return from Google/Facebook sign-in
6560        const authPage = document.querySelector('[data-authentication]');
6561        console.log('SwiftAuth: Auth page element found:', !!authPage);
6562
6563        if (authPage) {
6564            // Check if this is an OAuth callback (returning from Cognito)
6565            if (checkOAuthCallback()) {
6566                // OAuth callback is being processed, wait for it
6567                console.log('SwiftAuth: Processing OAuth callback...');
6568                return; // Don't continue init - handleOAuthCallback will handle everything
6569            }
6570
6571            // Show auth_error from failed OAuth redirect if present
6572            const authError = new URLSearchParams(window.location.search).get('auth_error');
6573            if (authError) {
6574                showSocialAuthError(authError);
6575                window.history.replaceState({}, '', '/authentication/');
6576            }
6577
6578            // Initialize social sign-in SDKs (Google Identity Services, Facebook SDK)
6579            console.log('SwiftAuth: Initializing social sign-in on auth page...');
6580            initSocialSignIn();
6581        } else {
6582            console.log('SwiftAuth: Not on auth page, skipping social sign-in init');
6583        }
6584
6585        // Initialize UI handlers
6586        initLoginButtons();
6587        initBookmarkButtons();
6588        initWhyRegisterButtons();
6589        initDeveloperShortcuts();
6590        initSubscribePage();
6591        initManageSubscription();
6592
6593        // ALWAYS initialize social sign-in button handlers (they use event delegation)
6594        // This ensures buttons work even if added dynamically
6595        initSocialSignInButtons();
6596
6597        // === PARALLEL ASYNC FETCHES ===
6598        // Fetch auth status and wall config in parallel.
6599        // These are no longer embedded in HTML (CloudFront cache-safe).
6600        // The nonce is NOT fetched here — it's resolved lazily on the first
6601        // write action (see fetchNonce/ajaxRequest) so anonymous page loads
6602        // don't boot WordPress for a nonce they never use.
6603        //
6604        // Skip the uncached swift_auth_status round-trip when client state is
6605        // already anonymous. checkLocalSession()/getMagicToken() ran above and
6606        // set authLevel to 'full'/'lite' if there is ANY session evidence
6607        // (Cognito localStorage session or magic token). A still-'anonymous'
6608        // visitor has none, so the server can only echo 'anonymous' — booting
6609        // WordPress for that on every pageview is pure waste.
6610        const initialFetches = [fetchWallConfig()];
6611        if (state.authLevel !== 'anonymous') {
6612            initialFetches.push(refreshAuthStatus());
6613        } else {
6614            console.log('SwiftAuth: Anonymous client state — skipping server auth_status check');
6615        }
6616        await Promise.all(initialFetches);
6617
6618        // Redeem ?mlt= magic link token if present (redeemMagicLink fetches the
6619        // nonce itself via ajaxRequest when an mlt token is in the URL).
6620        // redeemMagicLink() strips the param from the URL and updates state.
6621        await redeemMagicLink();
6622
6623        // Debug: log config (after async data is loaded)
6624        console.log('SwiftAuth: Initialized with config', {
6625            authLevel: state.authLevel,
6626            whyRegisterHtml: config.whyRegisterHtml ? '(set, ' + config.whyRegisterHtml.length + ' chars)' : '(empty)',
6627            socialEnabled: isSocialSignInEnabled(),
6628        });
6629
6630        // Update UI with confirmed auth state
6631        updateUI();
6632
6633        // Show profile shimmer IMMEDIATELY if on profile page (before any async work)
6634        // This gives instant visual feedback while UserDataManager initializes
6635        const profileRootEarly = document.getElementById('swift-profile-root');
6636        if (profileRootEarly) {
6637            if (state.authLevel === 'full') {
6638                renderProfileLoading(profileRootEarly);
6639            } else if (state.authLevel === 'anonymous') {
6640                renderProfileLoginRequired(profileRootEarly);
6641            }
6642        }
6643
6644        // Always try to initialize UserDataManager if user might be logged into Cognito
6645        // This handles: 1) anonymous users with Cognito session, 2) logged in users needing DynamoDB
6646        await initUserDataManagerFromCognitoSession();
6647
6648        // === WALL CHECKS (early) ===
6649        // Auth is stable, UserDataManager ready, wall config loaded — check walls
6650
6651        // Resolve subscribe nav btn on all pages using cached entitlement.
6652        // Article pages get overridden below with fresh DynamoDB data from checkPaywall().
6653        if (config.paywall?.enabled) {
6654            resolveSubscribeNavBtn(evaluateEntitlement(getCachedSubscription()).hasActive);
6655        }
6656
6657        const article = document.querySelector('.p402_premium');
6658        if (article) {
6659            // Free content bypasses all walls
6660            const contentCategory = document.querySelector('meta[name="__sync_contentCategory"]');
6661            const isFreeContent = contentCategory && contentCategory.getAttribute('content') === 'Free';
6662
6663            if (isFreeContent) {
6664                console.log('SwiftAuth: Free content — walls bypassed');
6665            } else {
6666                // Regwall is for anonymous users only
6667                if (config.regwall?.enabled && state.authLevel === 'anonymous') {
6668                    checkRegwall();
6669                }
6670                // Paywall runs for all users; lite-login goes to server entitlement check
6671                if (config.paywall?.enabled) {
6672                    await checkPaywall();
6673                }
6674            }
6675
6676            // Record pageview for reading history (authenticated users only)
6677            console.log('[PV-DIAG] Pageview gate check:', {
6678                authLevel: state.authLevel,
6679                dynamoEnabled: isDirectDynamoEnabled(),
6680                hasArticle: !!article,
6681                userDataManagerReady: state.userDataManagerReady,
6682                hasUserDataManager: !!state.userDataManager,
6683                udmIsReady: state.userDataManager?.isReady?.(),
6684                userId: state.userDataManager?.getUserId?.(),
6685                pubID: config.pubID,
6686            });
6687            if (state.authLevel === 'full' && isDirectDynamoEnabled()) {
6688                try {
6689                    const trackHistory = await state.userDataManager.getReadingHistoryPreference();
6690                    console.log('[PV-DIAG] trackReadingHistory preference:', trackHistory);
6691
6692                    const pubID = config.pubID || '';
6693                    const key = pubID + 'readingHistorySetting';
6694                    localStorage.setItem(key, trackHistory.toString());
6695
6696                    if (trackHistory) {
6697                        const meta = getArticleMeta();
6698                        console.log('[PV-DIAG] Article meta:', {
6699                            postId: meta.postId || meta.id,
6700                            url: meta.url,
6701                            title: meta.title ? meta.title.substring(0, 50) : '(none)',
6702                        });
6703                        if (meta.url) {
6704                            console.log('[PV-DIAG] Recording pageview to DynamoDB...');
6705                            try {
6706                                await recordPageview({
6707                                    postId: meta.postId || meta.id,
6708                                    articleUrl: meta.url,
6709                                    articleTitle: meta.title,
6710                                    articleImage: meta.image
6711                                });
6712                                console.log('[PV-DIAG] Pageview recorded successfully');
6713                            } catch (pvErr) {
6714                                console.error('[PV-DIAG] recordPageview THREW:', pvErr);
6715                            }
6716                        } else {
6717                            console.warn('[PV-DIAG] No meta.url — skipping pageview');
6718                        }
6719                    } else {
6720                        console.warn('[PV-DIAG] trackHistory is false — skipping pageview');
6721                    }
6722                } catch (err) {
6723                    console.warn('[PV-DIAG] Reading history preference check failed:', err);
6724                }
6725            } else {
6726                console.warn('[PV-DIAG] Pageview gate FAILED — not recording', {
6727                    reason: state.authLevel !== 'full' ? 'authLevel is ' + state.authLevel : 'DynamoDB not enabled',
6728                });
6729            }
6730        }
6731
6732        // Now that DynamoDB is ready, update bookmark button states
6733        if (isDirectDynamoEnabled()) {
6734            await updateBookmarkButtonStates();
6735        }
6736
6737        // Enhance subscribe page now that UserDataManager is ready (entitlement check + email prefill)
6738        await updateSubscribePage();
6739
6740        // Initialize profile page if on profile page
6741        await initProfilePage();
6742
6743        // Load followed topics for authenticated users (if not already loaded on profile page)
6744        // This is needed for follow buttons on other pages
6745        // OPTIMIZED: Use lightweight topics-only fetch instead of full profile
6746        if (state.authLevel === 'full' && state.followedTopics.length === 0) {
6747            const hasFollowButtons = document.querySelector('[data-topic]');
6748            if (hasFollowButtons && isDirectDynamoEnabled()) {
6749                try {
6750                    const rawTopics = await state.userDataManager.getFollowedTopicsOnly();
6751                    // Filter and set followed topics
6752                    state.followedTopics = (rawTopics || []).filter(topic => {
6753                        if (!topic) return false;
6754                        const topicId = topic.topicId || topic.id || '';
6755                        const topicName = topic.topicName || topic.name || '';
6756                        return topicId || topicName;
6757                    });
6758                } catch (err) {
6759                    console.warn('SwiftAuth: Could not load followed topics', err);
6760                }
6761            }
6762        }
6763
6764        // Initialize follow topic buttons (after topics are loaded)
6765        initFollowTopicButtons();
6766        initAuthorFollowContainers();
6767
6768        // Handle verification code in URL
6769        const urlParams = new URLSearchParams(window.location.search);
6770        const verificationCode = urlParams.get('code');
6771
6772        if (verificationCode && authPage) {
6773            // Auto-fill verification code
6774            const codeInput = authPage.querySelector('[name="verification_code"]');
6775            if (codeInput) {
6776                codeInput.value = verificationCode;
6777            }
6778        }
6779
6780        // Handle forgot password parameters in URL
6781        const forgotPasswordCode = urlParams.get('forgot_password_code');
6782        const forgotPasswordUsername = urlParams.get('username');
6783        const forgotPasswordEmail = urlParams.get('email');
6784
6785        if (forgotPasswordCode && authPage) {
6786            handleForgotPasswordLink(authPage, {
6787                code: forgotPasswordCode,
6788                username: forgotPasswordUsername,
6789                email: forgotPasswordEmail
6790            });
6791        }
6792    }
6793
6794    // =========================================================================
6795    // Subscribe Page
6796    // =========================================================================
6797
6798    /**
6799     * Instant render of subscribe page based on current state (no async)
6800     */
6801    function initSubscribePage() {
6802        const root = document.getElementById('swift-subscribe-root');
6803        if (!root) return;
6804
6805        const stripeConfig = config.stripe;
6806        if (!stripeConfig || !stripeConfig.enabled || !stripeConfig.plans || stripeConfig.plans.length === 0) {
6807            root.innerHTML = '<p>Subscription plans are not currently available.</p>';
6808            return;
6809        }
6810
6811        // Check for success redirect
6812        const urlParams = new URLSearchParams(window.location.search);
6813        if (urlParams.get('success') === 'true') {
6814            // Read return_to from URL param or cookie
6815            let returnTo = urlParams.get('return_to') || '';
6816            if (!returnTo) {
6817                const match = document.cookie.match('(?:^|; )swift_subscribe_return_to=([^;]*)');
6818                if (match) returnTo = decodeURIComponent(match[1]);
6819            }
6820            // Clear the cookie
6821            document.cookie = 'swift_subscribe_return_to=;path=/;max-age=0';
6822
6823            if (returnTo) {
6824                root.innerHTML = `
6825                    <div class="swift-subscribe-success">
6826                        <h2>Thank you for subscribing!</h2>
6827                        <p>Your subscription is being activated. Redirecting you back to your article...</p>
6828                    </div>
6829                `;
6830                setTimeout(() => { window.location.href = returnTo; }, 2000);
6831            } else {
6832                root.innerHTML = `
6833                    <div class="swift-subscribe-success">
6834                        <h2>Thank you for subscribing!</h2>
6835                        <p>Your subscription is being activated. You should have full access within a few moments.</p>
6836                        <p><a href="/" class="swift-btn swift-btn-primary">Go to Homepage</a></p>
6837                    </div>
6838                `;
6839            }
6840            return;
6841        }
6842
6843        // Stash return_to article URL as cookie for use after Stripe redirect
6844        const returnTo = urlParams.get('return_to') || '';
6845        if (returnTo) {
6846            document.cookie = 'swift_subscribe_return_to=' + encodeURIComponent(returnTo) + ';path=/;max-age=3600;SameSite=Lax';
6847        }
6848
6849        // Require full login
6850        if (state.authLevel !== 'full' || !state.sub) {
6851            root.innerHTML = `
6852                <div class="swift-subscribe-login-required">
6853                    <h2>Sign in to subscribe</h2>
6854                    <p>Please sign in or create an account to choose a subscription plan.</p>
6855                    <a href="/authentication/" class="swift-btn swift-btn-primary" id="swift-subscribe-login-btn">Sign In</a>
6856                </div>
6857            `;
6858            const subscribeLoginBtn = document.getElementById('swift-subscribe-login-btn');
6859            if (subscribeLoginBtn) {
6860                subscribeLoginBtn.addEventListener('click', () => {
6861                    setReturnUrl(window.location.href);
6862                });
6863            }
6864            return;
6865        }
6866
6867        // Logged in — try cached subscription for instant render
6868        const cachedSub = getCachedSubscription();
6869        if (cachedSub) {
6870            const evalResult = evaluateEntitlement(cachedSub);
6871            if (evalResult.hasActive) {
6872                renderSubscribeCurrentPlan(root, cachedSub, evalResult);
6873                return;
6874            }
6875        }
6876
6877        // No cached active subscription — render plan cards (email patched later by updateSubscribePage)
6878        const cachedToken = getCachedIdTokenPayload();
6879        renderSubscribePlanCards(root, stripeConfig, cachedToken?.email || '');
6880    }
6881
6882    /**
6883     * Render plan cards into the subscribe root
6884     */
6885    function renderSubscribePlanCards(root, stripeConfig, email) {
6886        email = email || '';
6887
6888        // Check if any plan has ACH pricing — if so, show the payment method toggle
6889        const hasAchOption = stripeConfig.plans.some(p => p.stripePriceIdAch);
6890        let paymentMethod = 'cc'; // default to Credit Card
6891
6892        function getDisplayPrice(plan) {
6893            if (paymentMethod === 'ach') {
6894                return plan.priceAch || plan.price || '';
6895            }
6896            return plan.priceCC || plan.price || '';
6897        }
6898
6899        function getPriceId(plan) {
6900            if (paymentMethod === 'ach' && plan.stripePriceIdAch) {
6901                return plan.stripePriceIdAch;
6902            }
6903            return plan.stripePriceId;
6904        }
6905
6906        function buildPlanCardsHtml() {
6907            return stripeConfig.plans.map((plan, idx) => {
6908                const isFeatured = !!plan.featured;
6909                const cardClass = 'swift-plan-card' + (isFeatured ? ' swift-plan-featured' : '');
6910                const btnClass = 'swift-btn swift-plan-cta ' + (isFeatured ? 'swift-btn-primary' : 'swift-btn-secondary');
6911                const badge = isFeatured ? '<div class="swift-plan-badge">Best Value</div>' : '<div class="swift-plan-badge"></div>';
6912                const displayPrice = getDisplayPrice(plan);
6913                const priceHtml = displayPrice
6914                    ? '<div class="swift-plan-price"><span class="swift-plan-currency">$</span>' + escapeHtml(displayPrice) + '</div>'
6915                    : '';
6916                const periodHtml = plan.period ? '<div class="swift-plan-period">' + escapeHtml(plan.period) + '</div>' : '';
6917
6918                return `
6919                    <div class="${cardClass}">
6920                        ${badge}
6921                        <h3 class="swift-plan-label">${escapeHtml(plan.label)}</h3>
6922                        ${priceHtml}
6923                        ${periodHtml}
6924                        <button class="${btnClass}" data-plan-index="${idx}">Subscribe</button>
6925                    </div>
6926                `;
6927            }).join('');
6928        }
6929
6930        const subTitle = config.paywall?.subscribePageTitle || 'Choose your plan';
6931        const subSubtitle = config.paywall?.subscribePageSubtitle || '';
6932
6933        const toggleHtml = hasAchOption ? `
6934            <div class="swift-payment-step">
6935                <div class="swift-step-header">
6936                    <span class="swift-step-label">Step 1 &mdash; How would you like to pay?</span>
6937                </div>
6938                <div class="swift-payment-options">
6939                    <div class="swift-payment-option" data-method="ach">
6940                        <span class="swift-radio-dot"></span>
6941                        <span class="swift-payment-option-name">Bank transfer (ACH)</span>
6942                        <span class="swift-fee-badge swift-fee-none">NO FEE</span>
6943                    </div>
6944                    <div class="swift-payment-option swift-payment-option-active" data-method="cc">
6945                        <span class="swift-radio-dot"></span>
6946                        <span class="swift-payment-option-name">Credit or debit card</span>
6947                        <span class="swift-fee-badge swift-fee-paid">+2% FEE</span>
6948                    </div>
6949                </div>
6950            </div>
6951        ` : '';
6952
6953        root.innerHTML = `
6954            <div class="swift-subscribe-header">
6955                <h2 class="swift-subscribe-title">${subTitle}</h2>
6956                ${subSubtitle ? `<p class="swift-subscribe-subtitle">${subSubtitle}</p>` : ''}
6957            </div>
6958            ${toggleHtml}
6959            ${hasAchOption ? '<p class="swift-plan-step-label">Step 2 &mdash; Pick a plan</p>' : ''}
6960            <div class="swift-plan-grid">
6961                ${buildPlanCardsHtml()}
6962            </div>
6963            <p class="swift-stripe-secured">Payments are secured by Stripe</p>
6964        `;
6965
6966        // Toggle handler
6967        if (hasAchOption) {
6968            root.querySelectorAll('.swift-payment-option').forEach(option => {
6969                option.addEventListener('click', () => {
6970                    paymentMethod = option.dataset.method;
6971                    root.querySelectorAll('.swift-payment-option').forEach(o => o.classList.remove('swift-payment-option-active'));
6972                    option.classList.add('swift-payment-option-active');
6973                    const grid = root.querySelector('.swift-plan-grid');
6974                    if (grid) grid.innerHTML = buildPlanCardsHtml();
6975                    bindCheckoutHandlers();
6976                });
6977            });
6978        }
6979
6980        function bindCheckoutHandlers() {
6981            root.querySelectorAll('[data-plan-index]').forEach(btn => {
6982                btn.addEventListener('click', async () => {
6983                    const plan = stripeConfig.plans[btn.dataset.planIndex];
6984                    if (!plan) return;
6985
6986                    btn.disabled = true;
6987                    btn.textContent = 'Redirecting...';
6988
6989                    try {
6990                        let recaptchaToken = '';
6991                        const rcSiteKey = window.swiftAuthConfig && window.swiftAuthConfig.recaptcha && window.swiftAuthConfig.recaptcha.siteKey;
6992                        if (rcSiteKey && typeof grecaptcha !== 'undefined') {
6993                            try {
6994                                recaptchaToken = await grecaptcha.execute(rcSiteKey, { action: 'checkout' });
6995                            } catch (rcErr) {
6996                                console.warn('reCAPTCHA token failed:', rcErr);
6997                            }
6998                        }
6999
7000                        let returnTo = '';
7001                        const cookieMatch = document.cookie.match('(?:^|; )swift_subscribe_return_to=([^;]*)');
7002                        if (cookieMatch) returnTo = decodeURIComponent(cookieMatch[1]);
7003
7004                        let successUrl = window.location.origin + '/subscribe/?success=true';
7005                        if (returnTo) successUrl += '&return_to=' + encodeURIComponent(returnTo);
7006
7007                        const conversion = getConversionSource();
7008                        const checkoutData = {
7009                            price_id: getPriceId(plan),
7010                            success_url: successUrl,
7011                            cancel_url: window.location.href,
7012                            identity_pool_id: state.userDataManager ? state.userDataManager.userPoolSub : '',
7013                            conversion_source: conversion.source,
7014                            conversion_url: conversion.url,
7015                        };
7016                        if (recaptchaToken) {
7017                            checkoutData.recaptcha_token = recaptchaToken;
7018                        }
7019                        const result = await ajaxRequest('swift_create_checkout', checkoutData);
7020                        if (result.success && result.checkout_url) {
7021                            window.location.href = result.checkout_url;
7022                        } else {
7023                            btn.textContent = 'Subscribe';
7024                            btn.disabled = false;
7025                            alert(result.error || 'Could not start checkout. Please try again.');
7026                        }
7027                    } catch (err) {
7028                        btn.textContent = 'Subscribe';
7029                        btn.disabled = false;
7030                        alert('Could not start checkout. Please try again.');
7031                    }
7032                });
7033            });
7034        }
7035
7036        // Initial bind
7037        bindCheckoutHandlers();
7038    }
7039
7040    /**
7041     * Render "Your Subscription" view for active subscribers
7042     * Reusable by both initSubscribePage (cached) and updateSubscribePage (DynamoDB)
7043     */
7044    function renderSubscribeCurrentPlan(root, entitlement, evalResult) {
7045        const formatDate = (dateVal) => {
7046            if (!dateVal) return 'N/A';
7047            const date = new Date(typeof dateVal === 'number' ? dateVal * 1000 : dateVal);
7048            return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
7049        };
7050
7051        const getTierInfo = (tier) => {
7052            if (tier === 'subscriber') return { class: 'success', label: 'Subscriber' };
7053            if (tier === 'employee') return { class: 'info', label: 'Employee' };
7054            if (tier === 'gratis') return { class: 'secondary', label: 'Complimentary' };
7055            if (tier === 'trial') return { class: 'warning', label: 'Trial' };
7056            return { class: 'default', label: 'Free' };
7057        };
7058
7059        const tierInfo = getTierInfo(evalResult.tier);
7060
7061        root.innerHTML = `
7062            <div class="swift-subscribe-current">
7063                <div class="swift-profile-section">
7064                    <div class="swift-section-header">
7065                        <h3>Your Subscription</h3>
7066                        <span class="swift-tier-badge swift-tier-${tierInfo.class}">${tierInfo.label}</span>
7067                    </div>
7068                    <div class="swift-section-content">
7069                        <div class="swift-subscription-status swift-status-active">
7070                            <p class="swift-status-message">Your subscription is <strong>active</strong>.</p>
7071                            ${evalResult.endDate ? `<p class="swift-status-detail">${evalResult.autoRenew ? 'Renews' : 'Expires'}: ${formatDate(evalResult.endDate)}</p>` : ''}
7072                        </div>
7073                        <div class="swift-subscription-actions">
7074                            <button class="swift-btn swift-btn-primary swift-subscribe-manage-btn">Manage Subscription</button>
7075                        </div>
7076                    </div>
7077                </div>
7078            </div>
7079        `;
7080
7081        const manageBtn = root.querySelector('.swift-subscribe-manage-btn');
7082        if (manageBtn) {
7083            manageBtn.addEventListener('click', async () => {
7084                manageBtn.disabled = true;
7085                manageBtn.textContent = 'Opening...';
7086                try {
7087                    const result = await ajaxRequest('swift_checkout_portal', {
7088                        return_url: window.location.href,
7089                    });
7090                    if (result.success && result.portal_url) {
7091                        window.location.href = result.portal_url;
7092                    } else {
7093                        manageBtn.textContent = 'Manage Subscription';
7094                        manageBtn.disabled = false;
7095                        alert(result.error || 'Could not open subscription portal. Please try again.');
7096                    }
7097                } catch (err) {
7098                    manageBtn.textContent = 'Manage Subscription';
7099                    manageBtn.disabled = false;
7100                    alert('Could not open subscription portal. Please try again.');
7101                }
7102            });
7103        }
7104    }
7105
7106    /**
7107     * Deferred enhancement: check entitlement + patch email into plan links
7108     * Runs after UserDataManager is ready. Corrects stale cache if needed.
7109     */
7110    async function updateSubscribePage() {
7111        const root = document.getElementById('swift-subscribe-root');
7112        if (!root) return;
7113
7114        const stripeConfig = config.stripe;
7115        if (!stripeConfig || !stripeConfig.enabled) return;
7116
7117        // Only enhance for logged-in users who saw plan cards
7118        if (state.authLevel !== 'full' || !state.sub) return;
7119
7120        // Check for existing active subscription from DynamoDB
7121        if (state.userDataManager) {
7122            try {
7123                const entitlement = await state.userDataManager.getSubscriptionOnly();
7124                const evalResult = evaluateEntitlement(entitlement);
7125
7126                if (evalResult.hasActive) {
7127                    // Active subscription — render (or re-render) current plan view
7128                    renderSubscribeCurrentPlan(root, entitlement, evalResult);
7129                    return;
7130                }
7131
7132                // DynamoDB says no active entitlement — if cache showed active, correct it
7133                const showingCurrentPlan = root.querySelector('.swift-subscribe-current');
7134                if (showingCurrentPlan) {
7135                    // Stale cache correction: re-render as plan cards
7136                    console.log('SwiftAuth: Cached subscription was stale, showing plan cards');
7137                }
7138
7139                // Fall through to render/update plan cards
7140            } catch (err) {
7141                console.error('SwiftAuth: Subscribe page entitlement check error:', err);
7142            }
7143        }
7144
7145        // No active subscription — render plan cards with email
7146        let email = '';
7147        try {
7148            const idToken = await getIdToken();
7149            if (idToken) {
7150                const payload = JSON.parse(atob(idToken.split('.')[1]));
7151                email = payload.email || '';
7152            }
7153        } catch (e) {
7154            // Ignore
7155        }
7156
7157        // Re-render plan cards with email (handles both fresh render and stale cache correction)
7158        renderSubscribePlanCards(root, stripeConfig, email);
7159    }
7160
7161    function initManageSubscription() {
7162        const root = document.getElementById('swift-manage-subscription-root');
7163        if (!root) return;
7164
7165        if (state.authLevel !== 'full' || !state.sub) {
7166            root.innerHTML = '<p>Please <a href="/authentication/" data-swift-login>sign in</a> to manage your subscription.</p>';
7167            return;
7168        }
7169
7170        root.innerHTML = `
7171            <button class="swift-btn swift-btn-primary" id="swift-open-portal-btn">Open Subscription Portal</button>
7172        `;
7173
7174        const btn = document.getElementById('swift-open-portal-btn');
7175        btn.addEventListener('click', async () => {
7176            btn.disabled = true;
7177            btn.textContent = 'Opening...';
7178            try {
7179                const result = await ajaxRequest('swift_checkout_portal', {
7180                    return_url: window.location.href,
7181                });
7182                if (result.success && result.portal_url) {
7183                    window.location.href = result.portal_url;
7184                } else {
7185                    btn.textContent = 'Open Subscription Portal';
7186                    btn.disabled = false;
7187                    alert(result.error || 'Could not open subscription portal. Please try again.');
7188                }
7189            } catch (err) {
7190                btn.textContent = 'Open Subscription Portal';
7191                btn.disabled = false;
7192                alert('Could not open subscription portal. Please try again.');
7193            }
7194        });
7195    }
7196
7197    function escapeHtml(str) {
7198        const div = document.createElement('div');
7199        div.textContent = str;
7200        return div.innerHTML;
7201    }
7202
7203    // =========================================================================
7204    // Public API
7205    // =========================================================================
7206
7207    window.SwiftAuth = {
7208        // Auth methods
7209        login,
7210        register,
7211        confirmRegistration,
7212        forgotPassword,
7213        confirmPassword,
7214        resetPasswordAndLogin,
7215        changePassword,
7216        logout,
7217        refreshAuthStatus,
7218        checkCognitoSession,
7219
7220        // Social sign-in methods
7221        signInWithGoogle,
7222        signInWithFacebook,
7223        isSocialSignInEnabled,
7224        initSocialSignIn,
7225
7226        // Bookmark methods
7227        addBookmark,
7228        removeBookmark,
7229        toggleBookmark,
7230        checkBookmark,
7231        getBookmarks,
7232
7233        // Profile methods
7234        getProfile,
7235        updateProfile,
7236        initProfilePage,
7237
7238        // Topic methods
7239        followTopic,
7240        unfollowTopic,
7241        toggleFollowTopic,
7242        isTopicFollowed,
7243
7244        // Reading history methods
7245        recordPageview,
7246        clearReadingHistory,
7247        removeReadingHistoryItem,
7248
7249        // State
7250        getState: () => ({ ...state }),
7251        getConfig: () => ({ ...config }),
7252        isDirectDynamoEnabled,
7253        getForgotPasswordParams: () => state.forgotPasswordParams,
7254        getEntitlementStatus: () => evaluateEntitlement(getCachedSubscription()),
7255
7256        // UI
7257        updateUI,
7258        hideRegwall,
7259        unlockContent,
7260        showWhyRegisterModal,
7261        hideWhyRegisterModal,
7262        handleForgotPasswordLink,
7263
7264        // Utilities
7265        getArticleUrl,
7266        getArticleMeta,
7267        isPaywallSite,
7268
7269        // Return URL for post-login redirect
7270        setReturnUrl,
7271        getReturnUrl,
7272        clearReturnUrl,
7273
7274        // Stripe/Subscription
7275        initSubscribePage,
7276        initManageSubscription,
7277
7278        // Developer tools
7279        getSessionCount,
7280        setSessionCount,
7281
7282        // Direct DynamoDB access (for advanced use)
7283        getUserDataManager: () => state.userDataManager,
7284
7285        // Diagnostics (for debugging DynamoDB issues)
7286        getDynamoDBDiagnostics,
7287    };
7288
7289    // Initialize on DOM ready
7290    console.log('SwiftAuth: Script loaded, document.readyState =', document.readyState);
7291
7292    if (document.readyState === 'loading') {
7293        console.log('SwiftAuth: Waiting for DOMContentLoaded...');
7294        document.addEventListener('DOMContentLoaded', init);
7295    } else {
7296        console.log('SwiftAuth: DOM already ready, calling init() immediately');
7297        init();
7298    }
7299
7300    console.log('SwiftAuth: window.SwiftAuth is now available');
7301
7302    // Initialize analytics if available
7303    if (window.SwiftAnalytics && config.analytics) {
7304        window.SwiftAnalytics.init(config.analytics);
7305    }
7306
7307})();

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.