commit 3240d701bd8244908caf8e21a0c5725ac22acd43
parent 3a4b5f6a30d0ca1b7890b7d4784704352943150d
Author: Sebastian <sebasjm@taler-systems.com>
Date: Wed, 13 May 2026 00:32:04 -0300
fix #11385
Diffstat:
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),