payment-button.js (6289B)
1 /** 2 * JavaScript for GNU Taler Turnstile payment button functionality. 3 */ 4 5 (function($) { 6 'use strict'; 7 8 /** 9 * Convert HTTP(S) payment URL to Taler URI format 10 * 11 * @param {string} paymentUrl - The HTTP(S) payment URL 12 * @param {string} sessionId - The hashed session ID 13 * @returns {string} The Taler 'pay' URI including session ID 14 */ 15 function convertToTalerUri(paymentUrl, sessionId) { 16 try { 17 var url = new URL(paymentUrl); 18 var protocol = url.protocol; // 'https:' or 'http:' 19 var host = url.host; // includes port if specified 20 var pathname = url.pathname; // e.g., '/something/orders/12345' 21 22 // Extract the path components, removing '/orders/' part 23 // Expected input: [/instance/$ID]/orders/$ORDER_ID 24 // Expected output: [/instance/$ID]/$ORDER_ID 25 var pathParts = pathname.split('/').filter(function(part) { 26 return part.length > 0; 27 }); 28 29 // Find 'orders' in the path and reconstruct without it 30 var ordersIndex = pathParts.indexOf('orders'); 31 var talerPath = ''; 32 33 if (ordersIndex !== -1 && ordersIndex < pathParts.length - 1) { 34 // Get parts before 'orders' and after 'orders' 35 var beforeOrders = pathParts.slice(0, ordersIndex); 36 var afterOrders = pathParts.slice(ordersIndex + 1); 37 talerPath = beforeOrders.concat(afterOrders).join('/'); 38 } else { 39 console.error('Error converting to Taler URI: "/orders/" not found'); 40 return paymentUrl; 41 } 42 43 if (protocol === 'https:') { 44 return 'taler://pay/' + host + '/' + talerPath + 45 '/' + encodeURIComponent(sessionId); 46 } else if (protocol === 'http:') { 47 return 'taler+http://pay/' + host + '/' + talerPath + 48 '/' + encodeURIComponent(sessionId); 49 } 50 51 console.error('Error converting to Taler URI: unsupported protocol'); 52 return paymentUrl; 53 } catch (e) { 54 console.error('Error converting to Taler URI:', e); 55 return paymentUrl; 56 } 57 } 58 59 /** 60 * Long-poll the payment URL to check if payment was completed 61 * 62 * @param {string} paymentUrl - The payment URL to poll 63 * @param {string} sessionId - The session ID 64 */ 65 function pollPaymentStatus(paymentUrl, sessionId) { 66 var separator = paymentUrl.indexOf('?') !== -1 ? '&' : '?'; 67 var timeoutMs = 30000; 68 var pollUrl = paymentUrl + separator + 'timeout_ms=' + timeoutMs + 69 '&session_id=' + encodeURIComponent(sessionId); 70 var startTime = Date.now(); // in milliseconds since Epoch 71 72 $.ajax({ 73 url: pollUrl, 74 method: 'GET', 75 timeout: timeoutMs + 5000, // Slightly longer than server timeout 76 headers: { 77 'Accept': 'application/json' 78 }, 79 success: function(data, textStatus, xhr) { 80 if (xhr.status === 200 || xhr.status === 202) { 81 console.log('Payment completed! Reloading page...'); 82 window.location.reload(); 83 } else if (xhr.status === 402) { 84 console.log('Payment still pending, continuing to poll...'); 85 var endTime = Date.now(); 86 // Prevent looping faster than the long-poll timeout 87 // (useful in case a bug causes the 402 to be returned 88 // immediately instead of long-polling properly). 89 var delay = (startTime + timeoutMs > endTime) 90 ? (startTime + timeoutMs - endTime) 91 : 0; 92 setTimeout(function() { 93 pollPaymentStatus(paymentUrl, sessionId); 94 }, delay); 95 } 96 }, 97 error: function(xhr, textStatus, errorThrown) { 98 if (xhr.status === 402) { 99 console.log('Payment still required (402), continuing to poll...'); 100 pollPaymentStatus(paymentUrl, sessionId); 101 } else if (textStatus === 'timeout') { 102 console.log('Poll timeout, retrying...'); 103 var endTime = Date.now(); 104 // Prevent looping faster than the long-poll timeout 105 var delay = (startTime + timeoutMs > endTime) 106 ? (startTime + timeoutMs - endTime) 107 : 0; 108 setTimeout(function() { 109 pollPaymentStatus(paymentUrl, sessionId); 110 }, delay); 111 } else { 112 // Other errors - wait a bit before retrying 113 console.log('Poll error: ' + textStatus + ', retrying in 5 seconds...'); 114 setTimeout(function() { 115 pollPaymentStatus(paymentUrl, sessionId); 116 }, 5000); 117 } 118 } 119 }); 120 } 121 122 /** 123 * Initialize payment button functionality 124 */ 125 $(document).ready(function() { 126 $('.taler-turnstile-qr-code-container').each(function() { 127 var $container = $(this); 128 var paymentUrl = $container.data('payment-url'); 129 var sessionId = $container.data('session-id'); 130 131 if (paymentUrl && typeof QRCode !== 'undefined') { 132 $container.empty(); 133 var talerUri = convertToTalerUri(paymentUrl, sessionId); 134 new QRCode($container[0], { 135 text: talerUri, 136 width: 200, 137 height: 200, 138 colorDark: '#000000', 139 colorLight: '#ffffff', 140 correctLevel: QRCode.CorrectLevel.M 141 }); 142 143 console.log('Starting payment status polling for: ' + paymentUrl); 144 pollPaymentStatus(paymentUrl, sessionId); 145 } else if (!window.QRCode) { 146 console.error('QRCode library not loaded'); 147 } 148 }); 149 150 }); 151 152 })(jQuery);