paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

commit 3240d701bd8244908caf8e21a0c5725ac22acd43
parent 3a4b5f6a30d0ca1b7890b7d4784704352943150d
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed, 13 May 2026 00:32:04 -0300

fix #11385

Diffstat:
Mcontrib/paywall.en.must | 156++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/backend/paivana-httpd_templates.c | 6++++++
2 files changed, 117 insertions(+), 45 deletions(-)

diff --git a/contrib/paywall.en.must b/contrib/paywall.en.must @@ -17,7 +17,7 @@ const MERCHANT_BACKEND = '{{ merchant_backend }}'; const MERCHANT_TEMPLATE_ID = '{{ template_id }}'; const MAX_PICKUP_DELAY = {{ max_pickup_delay }}; // in seconds - const TEMPLATE_SUMMARY = '{{ sumary }}'; + const TEMPLATE_SUMMARY = '{{ summary }}'; const TEMPLATE_CHOICES = '{{{ choices }}}'; const POLL_WAIT_MS = 30000; </script> @@ -84,6 +84,38 @@ const hash = await sha256b64(buf); return `${curTime}-${hash}`; } + async function confirmPayment(order_id, linkEl, errorEl) { + linkEl.textContent = I18N_PAYMENT_CONFIRMED_LOADING; + linkEl.href = '#'; + try { + const res = await fetch(`${origin}/.well-known/paivana`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + redirect: "manual", + body: JSON.stringify({ order_id, nonce, cur_time: { t_s: expTime }, website }), + }); + if (res.status >= 400) { + linkEl.textContent = I18N_PAYMENT_CONFIRMED_PROBLEM; + errorEl.textContent = JSON.stringify(await res.json()); + } + const dest = res.redirected ? res.url : website; + await waitMs(400); + location.href = dest; + } catch (e) { + console.warn('[paivana] Error trying to confirm payment:', e); + errorEl.textContent = I18N_PAYMENT_CONFIRMED_ERROR; + } + } + + function toggleDescription(el) { + for (const a of el.getElementsByClassName("arrow")) { + a.classList.toggle("upside-down") + } + for (const a of el.getElementsByClassName("price-list")) { + a.classList.toggle("hidden") + } + } + </script> <script> async function main() { @@ -108,7 +140,8 @@ const paivanaId = await makePaivanaId(expTime, nonceBuf, website); - const talerUri = [ + // finally we can compute the talerURI and polling URL + const TALER_URI = [ `taler${suffix}://pay-template/`, merchantHost, merchantPath, @@ -118,44 +151,7 @@ `&fulfillment_url=${encodeURIComponent(website)}` ].join(''); - const errorMessage = document.getElementById('error-message'); - const talerLink = document.getElementById('talerlink'); - talerLink.href = talerUri; - - const merchantHostLabel = document.getElementById('merchant-host'); - merchantHostLabel.textContent = merchantHost - - new QRCode(document.getElementById('qrcode'), { - text: talerUri, - width: QR_WIDTH, - height: QR_HEIGHT, - correctLevel: QRCode.CorrectLevel.M - }); - - async function confirmPayment(order_id) { - talerLink.textContent = I18N_PAYMENT_CONFIRMED_LOADING; - talerLink.href = '#'; - try { - const res = await fetch(`${origin}/.well-known/paivana`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - redirect: "manual", - body: JSON.stringify({ order_id, nonce, cur_time: { t_s: expTime }, website }), - }); - if (res.status >= 400) { - talerLink.textContent = I18N_PAYMENT_CONFIRMED_PROBLEM; - errorMessage.textContent = JSON.stringify(await res.json()); - } - const dest = res.redirected ? res.url : website; - await waitMs(400); - location.href = dest; - } catch (e) { - console.warn('[paivana] Error trying to confirm payment:', e); - errorMessage.textContent = I18N_PAYMENT_CONFIRMED_ERROR; - } - } - - const pollUrl = [ + const PAIVANA_POLL_URL = [ merchantBase, "/sessions/", encodeURIComponent(paivanaId), @@ -165,16 +161,38 @@ POLL_WAIT_MS ].join(""); + + // grab all the html element we need + const talerLink = document.getElementById('taler-link'); + const errorMessageLabel = document.getElementById('error-message'); + const qrDiv = document.getElementById('qrcode'); + + // show the taler URI as a link + // because we want the user to be able to use the webex + talerLink.href = TALER_URI; + + // show the qr code + new QRCode(qrDiv, { + text: TALER_URI, + width: QR_WIDTH, + height: QR_HEIGHT, + correctLevel: QRCode.CorrectLevel.M + }); + + // From here we just poll. Whe the request from polling + // returns that the order has been paid we show to the + // proxy that we hold the nonce and it should return + // us a valid cookie. while (true) { const start = performance.now(); try { - const res = await fetch(pollUrl, { cache: 'no-store' }); + const res = await fetch(PAIVANA_POLL_URL, { cache: 'no-store' }); if (res.status === 200) { let info = null; try { info = await res.json() } catch (_) {} console.log("[paivana] Got reponse from backend", res, info); if (info.order_id) { - await confirmPayment(info.order_id); + await confirmPayment(info.order_id, talerLink, errorMessageLabel); } else { talerLink.textContent = I18N_PAYMENT_CONFIRMED_NO_ORDER; await waitMs(3000); @@ -249,6 +267,7 @@ width: 100%; max-width: 400px; margin: auto; + margin-top: 0px; } /* compact header row */ @@ -341,6 +360,22 @@ color: var(--primary); letter-spacing: 0.04em; } + .merchant-host { + font-size: small; + color: gray; + } + .template-summary { + text-align: center; + } + .pay-choices { + cursor: pointer; + } + .upside-down { + rotate: 180deg; + } + .hidden { + display: none; + } /* steps */ .steps { @@ -388,6 +423,8 @@ font-weight: 500; color: var(--text); margin-bottom: 2px; + display: flex; + justify-content: space-between; } .step-desc { font-size: 12px; @@ -515,7 +552,10 @@ var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this <div class="card"> <!-- QR code + amount --> <div class="qr-wrap"> - <p id="merchant-host"></p> + {{#summary}} + <p class="template-summary">{{ summary }}</p> + {{/summary}} + <p class="merchant-host">{{ merchant_backend }}</p> <div class="qr-taler-frame"> <div style="padding: 10px; border-radius: 20px; background-color: white"> @@ -545,15 +585,41 @@ var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this </p> </div> </a> + {{#has_choices}} + <div class="step pay-choices" onclick="toggleDescription(this)"> + <div class="step-num">2</div> + <div class="step-body "> + <p class="step-title"> + <span>Pay {{default_choice.amount}} or see options</span> + <svg name="toggle-arrow" data-accordion-icon="" class="arrow" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m5 15 7-7 7 7"></path></svg> + </p> + <p class="step-desc"> + Your wallet will display the details of the transaction. + <ul class="price-list hidden"> + {{#choices}} + {{#description}} + <li>{{amount}}: {{description}}</li> + {{/description}} + {{^description}} + <li>{{amount}}</li> + {{/description}} + {{/choices}} + </ul> + </p> + </div> + </div> + {{/has_choices}} + {{^has_choices}} <div class="step"> <div class="step-num">2</div> <div class="step-body"> - <p class="step-title">Pay</p> + <p class="step-title">Pay {{default_choice.amount}}</p> <p class="step-desc"> Your wallet will display the details of the transaction. </p> </div> </div> + {{/has_choices}} <div class="step"> <div class="step-num">3</div> <div class="step-body"> @@ -575,7 +641,7 @@ var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this <div class="footer"> <p id="error-message"> </p> <div class="button-cta"> - <a target="_blank" id="talerlink" href="" class="cta">Pay now</a> + <a target="_blank" id="taler-link" href="" class="cta">Pay now</a> </div> </div> diff --git a/src/backend/paivana-httpd_templates.c b/src/backend/paivana-httpd_templates.c @@ -287,6 +287,12 @@ load_paywall (struct MHD_Connection *conn, GNUNET_JSON_pack_array_incref ( "choices", t->choices), + GNUNET_JSON_pack_bool ( + "has_choices", + 1 < json_array_size(t->choices)), + GNUNET_JSON_pack_object_steal ( + "default_choice", + json_array_get(t->choices, 0)), GNUNET_JSON_pack_uint64 ( "max_pickup_delay", t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU),