turnstile

Drupal paywall plugin
Log | Files | Refs | README | LICENSE

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:
Mjs/payment-button.js | 33++++++++++++++++++++-------------
Msrc/TalerMerchantApiService.php | 34+++++++++++++++++++++++++++++++++-
Mtemplates/turnstile-payment-button.html.twig | 9+++++++--
Mturnstile.module | 3++-
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', ],