commit 45c74b122f874bec620760cd758449f6b7ff7939
parent db3e05a1ecf6eb174be9f12fad49e184f10eeee7
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 20 Oct 2025 16:59:31 +0200
get QR code payments to work
Diffstat:
4 files changed, 62 insertions(+), 17 deletions(-)
diff --git a/js/payment-button.js b/js/payment-button.js
@@ -9,9 +9,9 @@
/**
* Long-poll the payment URL to check if payment was completed.
*/
- function pollPaymentStatus(paymentUrl) {
- // FIXME: do we need to also add session_id here!?
- var pollUrl = paymentUrl + (paymentUrl.indexOf('?') !== -1 ? '&' : '?') + 'timeout_ms=30000';
+ function pollPaymentStatus(paymentUrl, sessionId) {
+ var separator = paymentUrl.indexOf('?') !== -1 ? '&' : '?';
+ var pollUrl = paymentUrl + separator + 'timeout_ms=30000&session_id=' + encodeURIComponent(sessionId);
$.ajax({
url: pollUrl,
@@ -26,23 +26,25 @@
console.log('Payment completed! Reloading page...');
window.location.reload();
} else if (xhr.status === 402) {
+ // FIXME: if long-polling failed and we got an answer
+ // too quickly, still wait a bit!
console.log('Payment still pending, continuing to poll...');
- pollPaymentStatus(paymentUrl);
+ pollPaymentStatus(paymentUrl, sessionId);
}
},
error: function(xhr, textStatus, errorThrown) {
// Check if this is a 402 Payment Required response
if (xhr.status === 402) {
console.log('Payment still required (402), continuing to poll...');
- pollPaymentStatus(paymentUrl);
+ pollPaymentStatus(paymentUrl, sessionId);
} else if (textStatus === 'timeout') {
console.log('Poll timeout, retrying...');
- pollPaymentStatus(paymentUrl);
+ pollPaymentStatus(paymentUrl, sessionId);
} else {
// Other errors - wait a bit before retrying to avoid hammering the server
console.log('Poll error: ' + textStatus + ', retrying in 5 seconds...');
setTimeout(function() {
- pollPaymentStatus(paymentUrl);
+ pollPaymentStatus(paymentUrl, sessionId);
}, 5000);
}
}
@@ -53,9 +55,10 @@
* Convert HTTP(S) payment URL to Taler URI format.
*
* @param {string} paymentUrl - The HTTP(S) payment URL
- * @returns {string} The Taler URI
+ * @param {string} sessionId - The hashed session ID
+ * @returns {string} The Taler 'pay' URI including session ID
*/
- function convertToTalerUri(paymentUrl) {
+ function convertToTalerUri(paymentUrl, sessionId) {
try {
var url = new URL(paymentUrl);
var protocol = url.protocol; // 'https:' or 'http:'
@@ -84,9 +87,12 @@
}
if (protocol === 'https:') {
- return 'taler://pay/' + host + '/' + talerPath;
+ return 'taler://pay/' + host + '/' + talerPath +
+ '/' + encodeURIComponent(sessionId);
} else if (protocol === 'http:') {
- return 'taler+http://pay/' + host + '/' + talerPath;
+ return 'taler+http://pay/' +
+ host + '/' + talerPath +
+ '/' + encodeURIComponent(sessionId);
}
console.error('Error converting to Taler URI: unsupported protocol');
@@ -107,12 +113,13 @@
buttons.forEach(function(button) {
var $button = $(button);
var paymentUrl = $button.attr('href');
+ var sessionId = $button.data('session-id');
// Generate QR code
var $qrContainer = $('.turnstile-qr-code-container', context);
if ($qrContainer.length && paymentUrl && typeof QRCode !== 'undefined') {
$qrContainer.empty();
- var talerUri = convertToTalerUri(paymentUrl);
+ var talerUri = convertToTalerUri(paymentUrl, sessionId);
new QRCode($qrContainer[0], {
text: talerUri,
width: 200,
@@ -126,7 +133,7 @@
// Start polling for payment status
if (paymentUrl) {
console.log('Starting payment status polling for: ' + paymentUrl);
- pollPaymentStatus(paymentUrl);
+ pollPaymentStatus(paymentUrl, sessionId);
}
});
}
diff --git a/src/TalerMerchantApiService.php b/src/TalerMerchantApiService.php
@@ -550,6 +550,10 @@ class TalerMerchantApiService {
$fulfillment_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString();
+ // Get (hashed) session ID
+ $hashed_session_id = $this->getHashedSessionId();
+ $this->logger->debug('Taler session is @session', ['@session' => $hashed_session_id]);
+
// FIXME: after Merchant v1.1 we can use the returned
// the expiration time and then rely on the default already set in
// the merchant backend instead of hard-coding 1 day here!
@@ -564,7 +568,7 @@ class TalerMerchantApiService {
't_s' => $order_expiration,
],
],
- 'session_id' => session_id(),
+ 'session_id' => $hashed_session_id,
'create_token' => FALSE,
];
@@ -627,6 +631,7 @@ class TalerMerchantApiService {
'payment_url' => $backend_url . 'orders/' . $order_id,
'order_expiration' => $order_expiration,
'paid' => FALSE,
+ 'session_id' => $hashed_session_id,
];
}
catch (RequestException $e) {
@@ -662,4 +667,31 @@ class TalerMerchantApiService {
return $translations;
}
+
+ /**
+ * Generate a hashed session identifier for payment tracking.
+ *
+ * This creates a deterministic hash from the PHP session ID that can be
+ * safely shared with the client and merchant backend as the
+ * Taler "session_id".
+ *
+ * @return string
+ * Base64-encoded SHA-256 hash of the session ID (URL-safe).
+ */
+ private function getHashedSessionId(): string {
+ $raw_session_id = session_id();
+ if (empty($raw_session_id)) {
+ // If no session exists, start one
+ if (session_status() === PHP_SESSION_NONE) {
+ session_start();
+ $raw_session_id = session_id();
+ }
+ }
+
+ $hash = hash('sha256', $raw_session_id, true);
+ // Encode as URL-safe base64: replace +/ with -_ and remove padding
+ return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
+ }
+
+
}
\ No newline at end of file
diff --git a/templates/turnstile-payment-button.html.twig b/templates/turnstile-payment-button.html.twig
@@ -6,7 +6,9 @@
<div class="turnstile-payment-actions">
<div class="turnstile-payment-qr">
- <div class="turnstile-qr-code-container"></div>
+ <div class="turnstile-qr-code-container"
+ data-order-id="{{ order_id }}"
+ data-session-id="{{ session_id }}"></div>
<p class="turnstile-qr-help">{{ 'Scan with your GNU Taler wallet'|t }}</p>
</div>
@@ -14,7 +16,10 @@
<span>{{ 'or'|t }}</span>
</div>
- <a href="{{ payment_url }}" class="button button--primary turnstile-pay-button" data-order-id="{{ order_id }}">
+ <a href="{{ payment_url }}"
+ class="button button--primary turnstile-pay-button"
+ data-order-id="{{ order_id }}"
+ data-session-id="{{ session_id }}">
{{ 'Open GNU Taler payment Web page'|t }}
</a>
</div>
diff --git a/turnstile.module b/turnstile.module
@@ -161,6 +161,7 @@ function turnstile_entity_view_alter(array &$build, EntityInterface $entity, Ent
$pay_button = [
'#theme' => 'turnstile_payment_button',
'#order_id' => $order_info['order_id'],
+ '#session_id' => $order_info['session_id'],
'#payment_url' => $order_info['payment_url'],
'#node_title' => $node->getTitle(),
'#attached' => [
@@ -274,9 +275,9 @@ function turnstile_theme() {
'turnstile_payment_button' => [
'variables' => [
'order_id' => NULL,
+ 'session_id' => NULL,
'payment_url' => NULL,
'node_title' => NULL,
- 'price' => NULL,
],
'template' => 'turnstile-payment-button',
],