PageSourceSearch

https://chimpmatic.com/wp-content/themes/dev/assets/js/mercurial-dashboard-v2.js

js chimpmatic.com collected 2026-09-25 03:59:39 UTC 23,477 bytes, 642 lines download raw bytes

1/**
2 * Mercurial Dashboard JavaScript
3 *
4 * Handles interactive features:
5 * - Tab switching
6 * - Copy license key
7 * - Site deactivation (admin only)
8 * - Inline Stripe payment form (2026-01-23)
9 * - Delete payment method (2026-01-23)
10 *
11 * @since 2026-01-23
12 */
13
14(function () {
15    'use strict';
16
17    const SELECTORS = {
18        dashboard: '.mercurial-dashboard',
19        tab: '[data-mercurial-action="switch-tab"]',
20        panel: '.mercurial-tab-panel',
21        copyBtn: '[data-mercurial-action="copy-key"]',
22        deactivateBtn: '[data-mercurial-action="deactivate-site"]',
23        siteCard: '.mercurial-site-card',
24        activationsCount: '.mercurial-activations-count',
25        activationsBar: '.mercurial-activations-bar-fill',
26        addPaymentForm: '#mercurial-add-payment-form',
27        stripeElement: '#mercurial-stripe-element',
28        stripeErrors: '#mercurial-stripe-errors',
29        submitPayment: '#mercurial-submit-payment',
30        paymentTable: '.mercurial-payment-table tbody',
31    };
32
33    // Stripe state
34    let stripeInstance = null;
35    let stripeElements = null;
36    let cardElement = null;
37    let stripeParams = null;
38
39    function init() {
40        const dashboard = document.querySelector(SELECTORS.dashboard);
41        if (!dashboard) return;
42
43        dashboard.addEventListener('click', handleClick);
44    }
45
46    function handleClick(event) {
47        const target = event.target.closest('[data-mercurial-action]');
48        if (!target) return;
49
50        const action = target.dataset.mercurialAction;
51
52        // Tracking is isolated: a failure here must never break the dashboard.
53        try {
54            trackAction(target, action);
55        } catch (e) {}
56
57        switch (action) {
58            case 'switch-tab':
59                switchTab(target);
60                break;
61            case 'copy-key':
62                copyKey(target);
63                break;
64            case 'deactivate-site':
65                event.preventDefault();
66                deactivateSite(target);
67                break;
68            case 'toggle-add-payment':
69                event.preventDefault();
70                toggleAddPaymentForm();
71                break;
72            case 'close-add-payment':
73                event.preventDefault();
74                closeAddPaymentForm();
75                break;
76            case 'submit-payment':
77                event.preventDefault();
78                submitPaymentMethod(target);
79                break;
80            case 'delete-payment':
81                event.preventDefault();
82                deletePaymentMethod(target);
83                break;
84        }
85    }
86
87    // GA4 tracking: map every dashboard action to an analytics event so
88    // post-purchase behaviour (activations, downloads, renewals) is measurable.
89    const TRACK_EVENTS = {
90        'switch-tab':        { name: 'account_tab_view',      param: t => ({ tab: t.dataset.mercurialTab }) },
91        'copy-key':          { name: 'copy_license_key' },
92        'download':          { name: 'download_plugin',       param: t => ({ product: t.dataset.mercurialProduct, version: t.dataset.mercurialVersion }) },
93        'download-invoice':  { name: 'download_invoice',      param: t => ({ order_id: t.dataset.mercurialOrderId }) },
94        'view-subscription': { name: 'manage_subscription',   param: t => ({ subscription_id: t.dataset.mercurialSubscriptionId }) },
95        'reactivate':        { name: 'reactivate_license',    param: t => ({ subscription_id: t.dataset.mercurialSubscriptionId }) },
96        'deactivate-site':   { name: 'deactivate_site',       param: t => ({ site_url: t.dataset.mercurialSiteUrl }) },
97        'help-click':        { name: 'help_click',            param: t => ({ label: t.dataset.mercurialHelp }) },
98        'browse-products':   { name: 'browse_products',       param: t => ({ source: t.dataset.mercurialSource }) },
99        'submit-payment':    { name: 'add_payment_info' },
100        'delete-payment':    { name: 'remove_payment_method', param: t => ({ payment_id: t.dataset.mercurialPaymentId }) },
101        'save-account':      { name: 'update_account' },
102        // toggle-add-payment / close-add-payment are UI state only, not tracked.
103    };
104
105    function trackAction(target, action) {
106        const map = TRACK_EVENTS[action];
107        if (!map) {
108            return;
109        }
110
111        // Sites report through Cloudflare Zaraz (edge-side GA4) or load gtag
112        // directly. Support both; no-op when neither is present.
113        const zaraz = (window.zaraz && typeof window.zaraz.track === 'function') ? window.zaraz : null;
114        const hasGtag = typeof window.gtag === 'function';
115        if (!zaraz && !hasGtag) {
116            return;
117        }
118
119        const params = map.param ? map.param(target) : {};
120
121        // Enrich with the license context from the enclosing card, when present.
122        const card = target.closest('.mercurial-license-card');
123        if (card && card.dataset.mercurialLicenseId) {
124            params.license_id = card.dataset.mercurialLicenseId;
125        }
126
127        // Drop empty values so GA4 does not record blank parameters.
128        Object.keys(params).forEach(key => {
129            if (params[key] === undefined || params[key] === '') {
130                delete params[key];
131            }
132        });
133
134        // Prefer Zaraz (zaraz.track -> "All Tracks" trigger -> GA4); else gtag.
135        if (zaraz) {
136            zaraz.track(map.name, params);
137        } else {
138            window.gtag('event', map.name, params);
139        }
140    }
141
142    function switchTab(tabButton) {
143        const tabName = tabButton.dataset.mercurialTab;
144
145        // Update tab buttons
146        document.querySelectorAll(SELECTORS.tab).forEach(tab => {
147            tab.classList.toggle('is-active', tab.dataset.mercurialTab === tabName);
148        });
149
150        // Update panels
151        document.querySelectorAll(SELECTORS.panel).forEach(panel => {
152            const isTarget = panel.id === `mercurial-panel-${tabName}`;
153            panel.hidden = !isTarget;
154        });
155
156        // Update URL without reload
157        const url = new URL(window.location);
158        url.searchParams.set('tab', tabName);
159        window.history.replaceState({}, '', url);
160    }
161
162    function copyKey(button) {
163        const targetSelector = button.dataset.mercurialTarget;
164        const keyElement = document.querySelector(targetSelector);
165
166        if (!keyElement) return;
167
168        const text = keyElement.textContent.trim();
169
170        navigator.clipboard.writeText(text).then(() => {
171            const originalText = button.innerHTML;
172            button.classList.add('is-copied');
173            button.innerHTML = `
174                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
175                    <polyline points="20 6 9 17 4 12"></polyline>
176                </svg>
177                Copied!
178            `;
179
180            setTimeout(() => {
181                button.classList.remove('is-copied');
182                button.innerHTML = originalText;
183            }, 2000);
184        });
185    }
186
187    async function deactivateSite(button) {
188        const instance = button.dataset.mercurialInstance;
189        const siteUrl = button.dataset.mercurialSiteUrl;
190
191        if (!instance) {
192            console.error('Missing instance ID');
193            return;
194        }
195
196        // State: Deactivating...
197        button.disabled = true;
198        button.textContent = 'Deactivating...';
199        button.classList.add('is-loading');
200
201        try {
202            const response = await fetch('/wp-json/mercurial/v1/deactivate-site', {
203                method: 'POST',
204                headers: {
205                    'Content-Type': 'application/json',
206                    'X-WP-Nonce': mercurialDashboard.nonce,
207                },
208                body: JSON.stringify({ instance }),
209            });
210
211            const data = await response.json();
212
213            if (data.success) {
214                // State: Deactivated!
215                button.textContent = 'Deactivated!';
216                button.classList.remove('is-loading');
217                button.classList.add('is-success');
218
219                const siteCard = button.closest(SELECTORS.siteCard);
220                if (siteCard) {
221                    // Store reference before card removal
222                    const licenseCard = siteCard.closest('.mercurial-license-card');
223
224                    setTimeout(() => {
225                        siteCard.style.opacity = '0';
226                        siteCard.style.transform = 'translateX(-20px)';
227
228                        setTimeout(() => {
229                            siteCard.remove();
230                            updateActivationsCountForCard(licenseCard);
231                        }, 300);
232                    }, 800);
233                }
234            } else {
235                alert(data.message || 'Failed to deactivate site.');
236                button.disabled = false;
237                button.textContent = 'Deactivate';
238                button.classList.remove('is-loading');
239            }
240        } catch (error) {
241            console.error('Deactivation error:', error);
242            alert('An error occurred. Please try again.');
243            button.disabled = false;
244            button.textContent = 'Deactivate';
245            button.classList.remove('is-loading');
246        }
247    }
248
249    function updateActivationsCountForCard(licenseCard) {
250        if (!licenseCard) return;
251
252        const countEl = licenseCard.querySelector(SELECTORS.activationsCount);
253        const barEl = licenseCard.querySelector(SELECTORS.activationsBar);
254
255        if (!countEl) return;
256
257        const total = parseInt(countEl.dataset.mercurialTotal, 10);
258        const currentSites = licenseCard.querySelectorAll(SELECTORS.siteCard).length;
259
260        // Update text
261        countEl.innerHTML = `<strong>${currentSites}</strong> of <strong>${total}</strong> activations used`;
262
263        // Update progress bar
264        if (barEl) {
265            const percentage = total > 0 ? (currentSites / total) * 100 : 0;
266            barEl.style.width = `${percentage}%`;
267        }
268
269        // Update empty slots message
270        const siteList = licenseCard.querySelector('.mercurial-site-list');
271        let emptySlot = siteList.querySelector('.mercurial-empty-slot');
272        const emptySlots = total - currentSites;
273
274        if (emptySlots > 0) {
275            if (!emptySlot) {
276                emptySlot = document.createElement('div');
277                emptySlot.className = 'mercurial-empty-slot';
278                siteList.appendChild(emptySlot);
279            }
280            emptySlot.textContent = `+ ${emptySlots} activation slot${emptySlots > 1 ? 's' : ''} available`;
281        }
282    }
283
284    // =========================================================================
285    // INLINE STRIPE PAYMENT FORM (2026-01-23)
286    // =========================================================================
287
288    async function toggleAddPaymentForm() {
289        const form = document.querySelector(SELECTORS.addPaymentForm);
290        if (!form) return;
291
292        if (form.hidden) {
293            form.hidden = false;
294            form.classList.add('is-opening');
295
296            // Initialize Stripe if not already done
297            if (!stripeInstance) {
298                await initializeStripe();
299            }
300
301            setTimeout(() => form.classList.remove('is-opening'), 300);
302        } else {
303            closeAddPaymentForm();
304        }
305    }
306
307    function closeAddPaymentForm() {
308        const form = document.querySelector(SELECTORS.addPaymentForm);
309        if (!form) return;
310
311        form.classList.add('is-closing');
312        setTimeout(() => {
313            form.hidden = true;
314            form.classList.remove('is-closing');
315            clearStripeErrors();
316        }, 200);
317    }
318
319    async function initializeStripe() {
320        const elementContainer = document.querySelector(SELECTORS.stripeElement);
321        if (!elementContainer) return;
322
323        // Show loading state
324        elementContainer.innerHTML = '<div class="mercurial-stripe-loading">Loading payment form...</div>';
325
326        try {
327            // Fetch Stripe params from our REST endpoint
328            const response = await fetch('/wp-json/mercurial/v1/stripe-params', {
329                headers: {
330                    'X-WP-Nonce': mercurialDashboard.nonce,
331                },
332            });
333
334            const result = await response.json();
335
336            if (!result.success) {
337                throw new Error(result.message || 'Failed to load payment form');
338            }
339
340            stripeParams = result.data;
341
342            // Load Stripe.js if not already loaded
343            if (typeof Stripe === 'undefined') {
344                await loadStripeScript();
345            }
346
347            // Initialize Stripe
348            stripeInstance = Stripe(stripeParams.publishableKey, {
349                locale: stripeParams.locale || 'auto',
350            });
351
352            // Create Elements with light theme matching Mercurial Dashboard
353            stripeElements = stripeInstance.elements({
354                appearance: {
355                    theme: 'stripe',
356                    variables: {
357                        colorPrimary: '#84cc16',
358                        colorBackground: '#ffffff',
359                        colorText: '#1a212b',
360                        colorDanger: '#dc2626',
361                        fontFamily: 'Inter, system-ui, sans-serif',
362                        spacingUnit: '4px',
363                        borderRadius: '6px',
364                    },
365                    rules: {
366                        '.Input': {
367                            backgroundColor: '#ffffff',
368                            border: '1px solid #dde2e7',
369                        },
370                        '.Input:focus': {
371                            border: '1px solid #84cc16',
372                            boxShadow: '0 0 0 1px #84cc16',
373                        },
374                        '.Label': {
375                            color: '#415966',
376                        },
377                    },
378                },
379            });
380
381            // Clear container and create Card Element
382            elementContainer.innerHTML = '';
383            cardElement = stripeElements.create('card', {
384                hidePostalCode: true,
385                style: {
386                    base: {
387                        fontSize: '16px',
388                        color: '#1a212b',
389                        '::placeholder': {
390                            color: '#8a97a4',
391                        },
392                    },
393                    invalid: {
394                        color: '#dc2626',
395                    },
396                },
397            });
398
399            cardElement.mount(elementContainer);
400
401            // Handle card errors
402            cardElement.on('change', (event) => {
403                if (event.error) {
404                    showStripeError(event.error.message);
405                } else {
406                    clearStripeErrors();
407                }
408            });
409
410        } catch (error) {
411            console.error('Stripe initialization error:', error);
412            elementContainer.innerHTML = `<div class="mercurial-stripe-error">Failed to load payment form. Please refresh the page.</div>`;
413        }
414    }
415
416    function loadStripeScript() {
417        return new Promise((resolve, reject) => {
418            if (typeof Stripe !== 'undefined') {
419                resolve();
420                return;
421            }
422
423            const script = document.createElement('script');
424            script.src = 'https://js.stripe.com/v3/';
425            script.async = true;
426            script.onload = resolve;
427            script.onerror = () => reject(new Error('Failed to load Stripe.js'));
428            document.head.appendChild(script);
429        });
430    }
431
432    function showStripeError(message) {
433        const errorsEl = document.querySelector(SELECTORS.stripeErrors);
434        if (errorsEl) {
435            errorsEl.textContent = message;
436            errorsEl.hidden = false;
437        }
438    }
439
440    function clearStripeErrors() {
441        const errorsEl = document.querySelector(SELECTORS.stripeErrors);
442        if (errorsEl) {
443            errorsEl.textContent = '';
444            errorsEl.hidden = true;
445        }
446    }
447
448    async function submitPaymentMethod(button) {
449        if (!stripeInstance || !cardElement || !stripeParams) {
450            showStripeError('Payment form not initialized. Please try again.');
451            return;
452        }
453
454        setButtonLoading(button, true);
455        clearStripeErrors();
456
457        try {
458            const { paymentMethod, error: pmError } = await stripeInstance.createPaymentMethod({
459                type: 'card',
460                card: cardElement,
461            });
462
463            if (pmError) {
464                throw new Error(pmError.message);
465            }
466
467            const formData = new FormData();
468            formData.append('action', 'wc_stripe_create_and_confirm_setup_intent');
469            formData.append('_ajax_nonce', stripeParams.nonce);
470            formData.append('wc-stripe-payment-method', paymentMethod.id);
471            formData.append('wc-stripe-payment-type', 'card');
472
473            const updateSubsCheckbox = document.getElementById('mercurial-update-subscriptions');
474            if (updateSubsCheckbox && updateSubsCheckbox.checked) {
475                formData.append('update_all_subscription_payment_methods', '1');
476            }
477
478            const response = await fetch(stripeParams.ajaxUrl, {
479                method: 'POST',
480                body: formData,
481            });
482
483            const result = await response.json();
484
485            if (!result.success) {
486                throw new Error(result.data?.error?.message || 'Failed to save payment method');
487            }
488
489            if (result.data.status === 'requires_action' && result.data.client_secret) {
490                const { error: confirmError }
490 = await stripeInstance.confirmCardSetup(
491                    result.data.client_secret
492                );
493
494                if (confirmError) {
495                    throw new Error(confirmError.message);
496                }
497            }
498
499            if (updateSubsCheckbox && updateSubsCheckbox.checked) {
500                await fetch('/wp-json/mercurial/v1/update-subscriptions-payment', {
501                    method: 'POST',
502                    headers: {
503                        'Content-Type': 'application/json',
504                        'X-WP-Nonce': stripeParams.restNonce,
505                    },
506                    body: JSON.stringify({ payment_method_id: paymentMethod.id }),
507                });
508            }
509
510            setButtonLoading(button, false);
511            closeAddPaymentForm();
512            showSuccessMessage('Card added successfully!');
513            setTimeout(() => {
514                window.location.reload();
515            }, 1500);
516
517        } catch (error) {
518            showStripeError(error.message || 'An error occurred. Please try again.');
519            setButtonLoading(button, false);
520        }
521    }
522
523    function setButtonLoading(button, loading) {
524        const textEl = button.querySelector('.mercurial-btn-text');
525        const spinnerEl = button.querySelector('.mercurial-btn-spinner');
526
527        if (loading) {
528            button.disabled = true;
529            if (textEl) textEl.hidden = true;
530            if (spinnerEl) spinnerEl.hidden = false;
531        } else {
532            button.disabled = false;
533            if (textEl) textEl.hidden = false;
534            if (spinnerEl) spinnerEl.hidden = true;
535        }
536    }
537
538    function showSuccessMessage(message) {
539        const form = document.querySelector(SELECTORS.addPaymentForm);
540        if (!form) return;
541
542        const successEl = document.createElement('div');
543        successEl.className = 'mercurial-success-message';
544        successEl.innerHTML = `
545            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="24" height="24">
546                <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
547                <polyline points="22 4 12 14.01 9 11.01"></polyline>
548            </svg>
549            <span>${message}</span>
550        `;
551        form.querySelector('.mercurial-add-payment-body').innerHTML = '';
552        form.querySelector('.mercurial-add-payment-body').appendChild(successEl);
553        form.querySelector('.mercurial-add-payment-footer').hidden = true;
554    }
555
556    // =========================================================================
557    // DELETE PAYMENT METHOD (2026-01-23)
558    // =========================================================================
559
560    async function deletePaymentMethod(button) {
561        const paymentId = button.dataset.mercurialPaymentId;
562
563        if (!paymentId) {
564            console.error('Missing payment ID');
565            return;
566        }
567
568        // [Delete] → [Deleting...]
569        button.disabled = true;
570        button.textContent = 'Deleting...';
571        button.classList.add('is-loading');
572
573        try {
574            const response = await fetch('/wp-json/mercurial/v1/delete-payment-method', {
575                method: 'POST',
576                headers: {
577                    'Content-Type': 'application/json',
578                    'X-WP-Nonce': mercurialDashboard.nonce,
579                },
580                body: JSON.stringify({ token_id: parseInt(paymentId, 10) }),
581            });
582
583            const data = await response.json();
584
585            if (data.success) {
586                // [Deleting...] → [Deleted!]
587                button.textContent = 'Deleted!';
588                button.classList.remove('is-loading');
589                button.classList.add('is-deleted');
590
591                // Animate row removal after brief delay
592                const row = button.closest('tr');
593                if (row) {
594                    setTimeout(() => {
595                        row.style.transition = 'opacity 0.3s, transform 0.3s';
596                        row.style.opacity = '0';
597                        row.style.transform = 'translateX(-20px)';
598
599                        setTimeout(() => {
600                            row.remove();
601                            checkEmptyTable();
602                        }, 300);
603                    }, 500);
604                }
605            } else {
606                // Reset on failure
607                button.disabled = false;
608                button.textContent = 'Delete';
609                button.classList.remove('is-loading');
610            }
611        } catch (error) {
612            console.error('Delete payment error:', error);
613            button.disabled = false;
614            button.textContent = 'Delete';
615            button.classList.remove('is-loading');
616        }
617    }
618
619    function checkEmptyTable() {
620        const tbody = document.querySelector(SELECTORS.paymentTable);
621        if (!tbody) return;
622
623        const rows = tbody.querySelectorAll('tr');
624        if (rows.length === 0) {
625            // Replace table with empty state
626            const table = tbody.closest('table');
627            if (table) {
628                const emptyState = document.createElement('p');
629                emptyState.className = 'mercurial-empty-state';
630                emptyState.textContent = 'No payment methods saved.';
631                table.parentNode.replaceChild(emptyState, table);
632            }
633        }
634    }
635
636    // Initialize when DOM is ready
637    if (document.readyState === 'loading') {
638        document.addEventListener('DOMContentLoaded', init);
639    } else {
640        init();
641    }
642})();

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.