252 lines
8.3 KiB
JavaScript
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);
|
|
});
|
|
}
|
|
})();
|