/** * GOA shop pay popup — public only (no merchant secrets). * Flow: POST templates/{id} → taler://pay/… + payto links. * * QR display matches taler-merchant-webui QR_Taler * (@gnu-taler/web-util QR.tsx): animated #0042B3 conic ring + qr-logo.svg. * Uses qrcode-generator (same lib as webui) via global QRCode if present, * else falls back to canvas from qrcode.min.js (davidshimjs). */ (function () { var MERCHANT_HOST = "taler.hacktivism.ch"; /* goa-shop: dedicated instance with fixed-order product templates */ var INSTANCE = "goa-shop"; var SHOP_PAYTO = "payto://x-taler-bank/bank.hacktivism.ch/goa-shop?receiver-name=GOA%20Shop"; var SHOP_PAYTO_HTTPS = "https://bank.hacktivism.ch/webui/"; function introBase() { var b = document.querySelector("base"); return b && b.href ? b.href : "/intro/"; } function logoSrc() { return introBase() + "qr-logo.svg"; } function templateHttps(productId) { return ( "https://" + MERCHANT_HOST + "/instances/" + INSTANCE + "/templates/" + encodeURIComponent(productId) ); } function payTemplateUri(productId) { return ( "taler://pay-template/" + MERCHANT_HOST + "/instances/" + INSTANCE + "/" + encodeURIComponent(productId) ); } function ensureModal() { var m = document.getElementById("goa-pay-modal"); if (m) return m; m = document.createElement("div"); m.id = "goa-pay-modal"; m.className = "goa-pay-modal"; m.setAttribute("role", "dialog"); m.setAttribute("aria-modal", "true"); m.setAttribute("aria-labelledby", "goa-pay-title"); m.hidden = true; m.innerHTML = '
' + ' ' + '

Pay with Taler

' + '

' + '

Preparing payment…

' + '

① Wallet payment

' + '

Scan with the GNU Taler Wallet to pay this product

' + '
' + '
' + ' ' + "
" + ' Open payment link →' + '

' + ' taler:// URI' + ' ·' + ' HTTPS' + "

" + '
' + ' " + ' " + "
" + ' ' + "
"; document.body.appendChild(m); function close() { m.classList.remove("open"); m.hidden = true; // collapse settlement on close var body = document.getElementById("goa-pay-settle-body"); var btn = document.getElementById("goa-pay-settle-toggle"); if (body) body.hidden = true; if (btn) { btn.setAttribute("aria-expanded", "false"); m.classList.remove("settle-open"); } } m.addEventListener("click", function (e) { if (e.target === m) close(); }); document.getElementById("goa-pay-x").onclick = close; document.getElementById("goa-pay-close").onclick = close; document.getElementById("goa-pay-settle-toggle").onclick = function () { var body = document.getElementById("goa-pay-settle-body"); var btn = document.getElementById("goa-pay-settle-toggle"); var open = body.hidden; body.hidden = !open; btn.setAttribute("aria-expanded", open ? "true" : "false"); m.classList.toggle("settle-open", open); if (open) { // paint payto QR when first expanded renderQr(document.getElementById("goa-pay-qr-payto"), SHOP_PAYTO, 140); } }; document.addEventListener("keydown", function (e) { if (e.key === "Escape" && m.classList.contains("open")) close(); }); return m; } /** * Render URI as a real PNG (not a live canvas). * davidshimjs paints canvas then often hides it for an img; if the modal is * still display:none, layout collapses and you only see the blue ring. * We always encode off-DOM and inject a fixed-size image. */ function renderQr(hostEl, text, size) { if (!hostEl) return; hostEl.innerHTML = ""; size = size || 220; if (!text) { hostEl.innerHTML = '

'; return; } if (typeof QRCode === "undefined") { hostEl.innerHTML = '

QR library missing (qrcode.min.js)

'; return; } var level = QRCode.CorrectLevel && QRCode.CorrectLevel.M != null ? QRCode.CorrectLevel.M : QRCode.CorrectLevel && QRCode.CorrectLevel.L != null ? QRCode.CorrectLevel.L : 1; var scratch = document.createElement("div"); scratch.setAttribute("aria-hidden", "true"); scratch.style.cssText = "position:fixed;left:-9999px;top:0;width:" + size + "px;height:" + size + "px;overflow:hidden;opacity:0;pointer-events:none"; document.body.appendChild(scratch); var dataUrl = ""; try { new QRCode(scratch, { text: String(text), width: size, height: size, colorDark: "#000000", colorLight: "#ffffff", correctLevel: level, }); var canvas = scratch.querySelector("canvas"); var libImg = scratch.querySelector("img"); if (canvas && canvas.width > 0) { try { dataUrl = canvas.toDataURL("image/png"); } catch (e1) {} } if (!dataUrl && libImg && libImg.src && libImg.src.indexOf("data:") === 0) { dataUrl = libImg.src; } } catch (err) { dataUrl = ""; } if (scratch.parentNode) scratch.parentNode.removeChild(scratch); if (!dataUrl) { hostEl.innerHTML = '

QR encode failed

'; return; } var img = document.createElement("img"); img.alt = "QR code"; img.width = size; img.height = size; img.src = dataUrl; img.style.display = "block"; img.style.width = size + "px"; img.style.height = size + "px"; img.style.maxWidth = "100%"; img.style.margin = "0 auto"; img.style.background = "#fff"; hostEl.appendChild(img); } function setLogo(imgEl) { if (!imgEl) return; imgEl.src = logoSrc(); imgEl.onerror = function () { imgEl.style.display = "none"; }; imgEl.style.display = ""; } function normalizePayUri(uri) { if (!uri) return ""; return String(uri) .replace(/taler\.hacktivism\.ch:443/g, "taler.hacktivism.ch") .replace(/:443\//g, "/") .replace(/:443\?/g, "?"); } /** Public: template → order → taler_pay_uri (no secrets). */ function createPayUri(productId) { var url = templateHttps(productId); return fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", cache: "no-store", }) .then(function (r) { if (!r.ok) throw new Error("template POST HTTP " + r.status); return r.json(); }) .then(function (created) { var oid = created.order_id; var tok = created.token; if (!oid || !tok) throw new Error("no order_id/token"); var statusUrl = "https://" + MERCHANT_HOST + "/instances/" + INSTANCE + "/orders/" + encodeURIComponent(oid) + "?token=" + encodeURIComponent(tok); return fetch(statusUrl, { cache: "no-store" }).then(function (r) { return r.text().then(function (t) { var pay = ""; try { var d = JSON.parse(t); pay = d.taler_pay_uri || ""; } catch (e) {} if (!pay) { pay = "taler://pay/" + MERCHANT_HOST + "/instances/" + INSTANCE + "/" + oid + "/?c=" + tok; } return { taler_pay_uri: normalizePayUri(pay), order_id: oid, token: tok, status_https: statusUrl.replace( "taler.hacktivism.ch:443", "taler.hacktivism.ch" ), template_https: url, }; }); }); }); } function showPay(productId, name, amount) { var m = ensureModal(); if (document.body && document.body.getAttribute("data-shop-theme") === "bank") { m.classList.add("bank-theme"); } else { m.classList.remove("bank-theme"); } document.getElementById("goa-pay-title").textContent = name || productId; document.getElementById("goa-pay-amount").textContent = amount || ""; var status = document.getElementById("goa-pay-status"); status.textContent = "Preparing payment…"; status.className = "goa-pay-hint"; var open = document.getElementById("goa-pay-open"); open.removeAttribute("href"); open.classList.add("disabled"); open.onclick = null; var payLink = document.getElementById("goa-pay-uri"); payLink.removeAttribute("href"); payLink.classList.add("disabled"); payLink.textContent = "taler:// URI"; document.getElementById("goa-pay-uri-https").href = templateHttps(productId); document.getElementById("goa-pay-uri-https").textContent = "HTTPS"; var paytoLink = document.getElementById("goa-pay-payto"); paytoLink.href = SHOP_PAYTO; paytoLink.textContent = "payto:// URI"; document.getElementById("goa-pay-payto-https").href = SHOP_PAYTO_HTTPS; document.getElementById("goa-pay-payto-https").textContent = "HTTPS"; setLogo(document.getElementById("goa-pay-qr-pay-logo")); // Reset settlement panel (collapsed until user expands) var settleBody = document.getElementById("goa-pay-settle-body"); var settleBtn = document.getElementById("goa-pay-settle-toggle"); if (settleBody) settleBody.hidden = true; if (settleBtn) settleBtn.setAttribute("aria-expanded", "false"); m.classList.remove("settle-open"); var paytoHost = document.getElementById("goa-pay-qr-payto"); if (paytoHost) paytoHost.innerHTML = ""; // Open first so layout exists, then paint wallet QR only m.hidden = false; m.classList.add("open"); renderQr( document.getElementById("goa-pay-qr-pay"), payTemplateUri(productId), 220 ); createPayUri(productId) .then(function (info) { var pay = info.taler_pay_uri; open.href = pay; open.classList.remove("disabled"); open.textContent = "Open payment link →"; payLink.href = pay; payLink.classList.remove("disabled"); payLink.textContent = "taler:// URI"; var uh = document.getElementById("goa-pay-uri-https"); uh.href = info.status_https || info.template_https; uh.textContent = "HTTPS"; // Live unpaid taler://pay (fresh order each open) renderQr(document.getElementById("goa-pay-qr-pay"), pay, 220); status.textContent = "Ready — scan QR or open in wallet"; }) .catch(function (err) { status.textContent = "Payment setup failed: " + (err && err.message ? err.message : err); status.className = "goa-pay-hint err"; open.textContent = "Retry"; open.onclick = function (e) { e.preventDefault(); showPay(productId, name, amount); }; }); } function bind() { document.querySelectorAll("[data-product]").forEach(function (el) { el.addEventListener("click", function (e) { e.preventDefault(); var id = el.getAttribute("data-product"); var nameEl = el.querySelector(".name"); var priceEl = el.querySelector(".price"); showPay( id, nameEl ? nameEl.textContent.trim() : id, priceEl ? priceEl.textContent.trim() : "" ); }); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bind); } else { bind(); } })();