/** * Payment Widget Hydration for MkDocs * * Automatically converts payment block placeholders into interactive widgets * when landing pages with payment blocks are exported to MkDocs. * * Reads configuration from window.PAYMENT_API_URL and window.APP_URL * (set by env-config.js hook). No hardcoded URLs. * * Supports: * - Donate blocks: [id^="cm-donate-"] * - Product blocks: [id^="cm-product-"] */ (function() { 'use strict'; var PAYMENT_API_URL = window.PAYMENT_API_URL || ''; var APP_URL = window.APP_URL || ''; /** * Check API health before hydrating */ function checkApiHealth(callback) { if (!PAYMENT_API_URL) { callback(false); return; } var xhr = new XMLHttpRequest(); xhr.open('GET', PAYMENT_API_URL + '/api/payments/config', true); xhr.timeout = 5000; xhr.onload = function() { callback(xhr.status >= 200 && xhr.status < 400); }; xhr.onerror = function() { callback(false); }; xhr.ontimeout = function() { callback(false); }; xhr.send(); } /** * Hydrate a donate block */ function hydrateDonateBlock(el) { if (el.getAttribute('data-hydrated') === 'true') return; var submitBtn = el.querySelector('[data-role="submit"]'); if (!submitBtn) return; // Legacy block with inline script — skip el.setAttribute('data-hydrated', 'true'); var emailInput = el.querySelector('[data-role="email"]'); var nameInput = el.querySelector('[data-role="name"]'); var anonBox = el.querySelector('[data-role="anonymous"]'); var errDiv = el.querySelector('[data-role="error"]'); var formDiv = el.querySelector('[data-role="form"]'); var amountsDiv = el.querySelector('[data-role="amounts"]'); var customWrap = el.querySelector('[data-role="custom-wrap"]'); var customInput = el.querySelector('[data-role="custom-input"]'); var preselected = parseInt(el.getAttribute('data-preselected') || '0', 10); var showPicker = el.getAttribute('data-show-picker') === 'true'; var selectedCents = preselected || 0; function showErr(msg) { if (errDiv) { errDiv.textContent = msg; errDiv.style.display = 'block'; } } function hideErr() { if (errDiv) { errDiv.style.display = 'none'; } } // Amount button logic if (showPicker && amountsDiv) { var amtBtns = amountsDiv.querySelectorAll('.cm-amt-btn'); amtBtns.forEach(function(btn) { btn.addEventListener('click', function() { var cents = btn.getAttribute('data-cents'); // Deactivate all amtBtns.forEach(function(b) { b.classList.remove('cm-amt-active'); }); btn.classList.add('cm-amt-active'); if (cents === 'custom') { selectedCents = 0; if (customWrap) customWrap.style.display = 'block'; if (customInput) customInput.focus(); submitBtn.textContent = 'Donate'; } else { selectedCents = parseInt(cents, 10); if (customWrap) customWrap.style.display = 'none'; submitBtn.textContent = 'Donate $' + (selectedCents / 100).toFixed(0); } if (formDiv) formDiv.style.display = 'block'; }); }); if (customInput) { customInput.addEventListener('input', function() { var v = parseFloat(customInput.value); if (v > 0) { selectedCents = Math.round(v * 100); submitBtn.textContent = 'Donate $' + v.toFixed(2); } else { selectedCents = 0; submitBtn.textContent = 'Donate'; } }); } } // Submit handler submitBtn.addEventListener('click', function() { hideErr(); var email = (emailInput && emailInput.value || '').trim(); if (!email || email.indexOf('@') < 1) { showErr('Please enter a valid email address.'); return; } if (!selectedCents || selectedCents < 100) { showErr('Please select a donation amount.'); return; } submitBtn.disabled = true; submitBtn.textContent = 'Processing...'; fetch(PAYMENT_API_URL + '/api/payments/donate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amountCents: selectedCents, email: email, name: (nameInput && nameInput.value || '').trim() || undefined, isAnonymous: !!(anonBox && anonBox.checked), }), }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.url) { window.location.href = data.url; } else { showErr(data.error && data.error.message || 'Something went wrong.'); submitBtn.disabled = false; submitBtn.textContent = 'Donate'; } }) .catch(function() { showErr('Connection error. Please try again.'); submitBtn.disabled = false; submitBtn.textContent = 'Donate'; }); }); } /** * Hydrate a product block */ function hydrateProductBlock(el) { if (el.getAttribute('data-hydrated') === 'true') return; var submitBtn = el.querySelector('[data-role="submit"]'); if (!submitBtn) return; // Legacy or sold-out — skip el.setAttribute('data-hydrated', 'true'); var emailInput = el.querySelector('[data-role="email"]'); var nameInput = el.querySelector('[data-role="name"]'); var errDiv = el.querySelector('[data-role="error"]'); var productId = el.getAttribute('data-product-id') || ''; function showErr(msg) { if (errDiv) { errDiv.textContent = msg; errDiv.style.display = 'block'; } } function hideErr() { if (errDiv) { errDiv.style.display = 'none'; } } submitBtn.addEventListener('click', function() { hideErr(); var email = (emailInput && emailInput.value || '').trim(); if (!email || email.indexOf('@') < 1) { showErr('Please enter a valid email address.'); return; } submitBtn.disabled = true; submitBtn.textContent = 'Processing...'; fetch(PAYMENT_API_URL + '/api/payments/purchase', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId: productId, buyerEmail: email, buyerName: (nameInput && nameInput.value || '').trim() || undefined, }), }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.url) { window.location.href = data.url; } else { showErr(data.error && data.error.message || 'Something went wrong.'); submitBtn.disabled = false; submitBtn.textContent = 'Buy Now'; } }) .catch(function() { showErr('Connection error. Please try again.'); submitBtn.disabled = false; submitBtn.textContent = 'Buy Now'; }); }); } /** * Show fallback links when API is unreachable */ function showFallbacks() { var fallbacks = document.querySelectorAll('.cm-payment-fallback'); fallbacks.forEach(function(f) { f.style.display = 'block'; }); // Hide interactive forms document.querySelectorAll('[id^="cm-donate-"] [data-role="form"], [id^="cm-donate-"] [data-role="amounts"]').forEach(function(el) { el.style.display = 'none'; }); document.querySelectorAll('[id^="cm-product-"] [data-role="form"]').forEach(function(el) { el.style.display = 'none'; }); } /** * Initialize all payment widgets */ function initAll() { var donateBlocks = document.querySelectorAll('[id^="cm-donate-"]'); var productBlocks = document.querySelectorAll('[id^="cm-product-"]'); if (donateBlocks.length === 0 && productBlocks.length === 0) return; checkApiHealth(function(healthy) { if (healthy) { donateBlocks.forEach(hydrateDonateBlock); productBlocks.forEach(hydrateProductBlock); } else { console.warn('[Payment Widgets] API unreachable, showing fallback links'); showFallbacks(); } }); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAll); } else { initAll(); } // Re-initialize on MkDocs navigation (for SPA-style navigation) if (typeof window.document$ !== 'undefined') { window.document$.subscribe(function() { setTimeout(initAll, 100); }); } })();