turnstile

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

payment-button.js (6672B)


      1 /**
      2  * @file payment-button.js
      3  * @brief JavaScript for GNU Taler Turnstile payment button functionality.
      4  * @author Christian Grothoff
      5  * @license LGPLv3+
      6  */
      7 
      8 (function ($, Drupal, once) {
      9   'use strict';
     10 
     11   /**
     12    * Long-poll the payment URL to check if payment was completed.
     13    */
     14   function pollPaymentStatus(paymentUrl, sessionId) {
     15     var separator = paymentUrl.indexOf('?') !== -1 ? '&' : '?';
     16     const timeout_ms = 30000;
     17     var pollUrl = paymentUrl + separator + 'timeout_ms=' + timeout_ms + '&session_id=' + encodeURIComponent(sessionId);
     18     const start_time = Date.now(); // in milliseconds since Epoch
     19 
     20     $.ajax({
     21       url: pollUrl,
     22       method: 'GET',
     23       timeout: timeout_ms + 5000, // Slightly longer than server timeout
     24       headers: {
     25         'Accept': 'application/json'
     26       },
     27       success: function(data, textStatus, xhr) {
     28         // Check if we got 20x (payment completed)
     29         if ( (xhr.status === 200) || (xhr.status === 202) ) {
     30           console.log('Payment completed! Reloading page...');
     31           window.location.reload();
     32         } else if (xhr.status === 402) {
     33           console.log('Payment still pending, continuing to poll...');
     34           const end_time = Date.now();
     35           // Prevent looping faster than the long-poll timeout
     36           // (useful in case a bug causes the 402 to be returned
     37           // immediately instead of long-polling properly).
     38           const delay = (start_time + timeout_ms > end_time)
     39                   ? (start_time + timeout_ms - end_time)
     40                   : 0;
     41           setTimeout(function() {
     42              pollPaymentStatus(paymentUrl, sessionId);
     43           }, delay);
     44         }
     45       },
     46       error: function(xhr, textStatus, errorThrown) {
     47         // Check if this is a 402 Payment Required response
     48         if (xhr.status === 402) {
     49           console.log('Payment still required (402), continuing to poll...');
     50           pollPaymentStatus(paymentUrl, sessionId);
     51         } else if (textStatus === 'timeout') {
     52           console.log('Poll timeout, retrying...');
     53           const end_time = Date.now();
     54           // Prevent looping faster than the long-poll timeout
     55           // (useful in case a bug causes a timeout to be returned
     56           // faster than what we wanted)
     57           const delay = (start_time + timeout_ms > end_time)
     58                   ? (start_time + timeout_ms - end_time)
     59                   : 0;
     60           setTimeout(function() {
     61              pollPaymentStatus(paymentUrl, sessionId);
     62           }, delay);
     63         } else {
     64           // Other errors - wait a bit before retrying to avoid hammering the server,
     65           // But do not wait the full long-polling period to remain responsive
     66           console.log('Poll error: ' + textStatus + ', retrying in 5 seconds...');
     67           setTimeout(function() {
     68               pollPaymentStatus(paymentUrl, sessionId);
     69           }, 5000);
     70         }
     71       }
     72     });
     73   }
     74 
     75   /**
     76    * Convert HTTP(S) payment URL to Taler URI format.
     77    *
     78    * @param {string} paymentUrl - The HTTP(S) payment URL
     79    * @param {string} sessionId - The hashed session ID
     80    * @returns {string} The Taler 'pay' URI including session ID
     81    */
     82   function convertToTalerUri(paymentUrl, sessionId) {
     83     try {
     84       var url = new URL(paymentUrl);
     85       var protocol = url.protocol; // 'https:' or 'http:'
     86       var host = url.host; // includes port if specified
     87       var pathname = url.pathname; // e.g., '/something/orders/12345'
     88 
     89       // Extract the path components, removing '/orders/' part
     90       // Expected input: [/instance/$ID]/orders/$ORDER_ID
     91       // Expected output: [/instance/$ID]/$ORDER_ID
     92       var pathParts = pathname.split('/').filter(function(part) {
     93         return part.length > 0;
     94       });
     95 
     96       // Find 'orders' in the path and reconstruct without it
     97       var ordersIndex = pathParts.indexOf('orders');
     98       var talerPath = '';
     99 
    100       if (ordersIndex !== -1 && ordersIndex < pathParts.length - 1) {
    101         // Get parts before 'orders' and after 'orders'
    102         var beforeOrders = pathParts.slice(0, ordersIndex);
    103         var afterOrders = pathParts.slice(ordersIndex + 1);
    104         talerPath = beforeOrders.concat(afterOrders).join('/');
    105       } else {
    106         console.error('Error converting to Taler URI: "/orders/" not found');
    107         return paymentUrl;
    108       }
    109 
    110       if (protocol === 'https:') {
    111         return 'taler://pay/' + host + '/' + talerPath +
    112                '/' + encodeURIComponent(sessionId);
    113       } else if (protocol === 'http:') {
    114         return 'taler+http://pay/' +
    115               host + '/' + talerPath +
    116               '/' + encodeURIComponent(sessionId);
    117       }
    118 
    119       console.error('Error converting to Taler URI: unsupported protocol');
    120       return paymentUrl;
    121     } catch (e) {
    122       console.error('Error converting to Taler URI:', e);
    123       return paymentUrl;
    124     }
    125   }
    126 
    127 
    128   /**
    129    * Attach payment button behavior.
    130    */
    131   Drupal.behaviors.talerTurnstilePaymentButton = {
    132     attach: function (context, settings) {
    133       // Do taler presence detection exactly once.
    134       once('taler-support', 'html').forEach(() => {
    135         // Detect presence of taler support in the browser.
    136         window.talerCallback = (res) => {
    137           console.log("talerCallback", res);
    138           if (res.present) {
    139             const els = $('.show-if-taler-supported');
    140             els.removeClass("hidden");
    141           } else {
    142             $('.show-if-taler-supported').addClass("hidden");
    143           }
    144         };
    145         // Add taler-support meta tag
    146         let meta = document.createElement('meta');
    147         meta.name = "taler-support";
    148         meta.content = "api,callback";
    149         document.getElementsByTagName('head')[0].appendChild(meta);
    150       });
    151       var qrContainers = once('taler-turnstile-qr-generation', '.taler-turnstile-qr-code-container', context);
    152       qrContainers.forEach(function(qrContainer) {
    153         var $qrContainer = $(qrContainer);
    154         var paymentUrl = $qrContainer.data('payment-url');
    155         var sessionId = $qrContainer.data('session-id');
    156         if (paymentUrl && typeof QRCode !== 'undefined') {
    157           $qrContainer.empty();
    158           var talerUri = convertToTalerUri(paymentUrl, sessionId);
    159           new QRCode($qrContainer[0], {
    160             text: talerUri,
    161             width: 200,
    162             height: 200,
    163             colorDark: '#000000',
    164             colorLight: '#ffffff',
    165             correctLevel: QRCode.CorrectLevel.M
    166           });
    167         }
    168 
    169         if (paymentUrl) {
    170           console.log('Starting payment status polling for: ' + paymentUrl);
    171           pollPaymentStatus(paymentUrl, sessionId);
    172         }
    173       });
    174     }
    175   };
    176 
    177 })(jQuery, Drupal, once);