changemaker.lite/mkdocs/site/assets/js/payment-widgets.js
2026-02-18 17:15:31 -07:00

252 lines
8.3 KiB
JavaScript

/**
* 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);
});
}
})();