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);