paywall.en.must (5320B)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <title>Payment Required</title> 5 <meta http-equiv="content-type" content="text/html;CHARSET=utf-8"> 6 <!-- FIXME: probably should serve this ourselves... --> 7 <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" 8 integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" 9 crossorigin="anonymous" referrerpolicy="no-referrer"></script> 10 </head> 11 <body> 12 <h1>Payment Required</h1> 13 <p>Scan the QR code or follow the link below with your 14 <a href="https://wallet.taler.net">GNU Taler</a> wallet.</p> 15 16 <div id="qrcode"></div> 17 <p><a id="talerlink" href="#">Generating payment link...</a></p> 18 19 <script> 20 (async () => { 21 22 function waitMs(ms) { 23 return new Promise(resolve => setTimeout(resolve, ms)); 24 } 25 26 // Note: good like this ONLY for short inputs... 27 function toBase64Url(bytes) { 28 let binary = String.fromCharCode(...bytes); 29 return btoa(binary) 30 .replace(/\+/g, '-') 31 .replace(/\//g, '_') 32 .replace(/=+$/, ''); // remove padding 33 } 34 35 async function sha256b64(str) { 36 const buf = await crypto.subtle.digest( 37 'SHA-256', new TextEncoder().encode(str)); 38 return toBase64Url(new Uint8Array(buf)); 39 } 40 41 async function makePaivanaId(curTime, nonce, website) { 42 const hash = await sha256b64(nonce + website + String(curTime)); 43 return `${curTime}-${hash}`; 44 } 45 46 const href = window.location.href; 47 // This is a policy decision: we include the query parameters 48 // in the website to support sites that need them, say if 49 // "?page=42" is being used instead of "/pages/42". 50 const website = href.split('#')[0]; 51 const metaMerchant = '{{ merchant_backend }}'; 52 const metaTemplate = '{{ template_id }}'; 53 const maxPickupDelay = {{ max_pickup_delay }}; // in seconds 54 // cap at 100 years, we don't deal well with 'forever' otherwise 55 const usePickupDelay = Math.min(maxPickupDelay, 60*60*24*356*100); 56 57 // Strip trailing slash so we can append /templateId cleanly. 58 const merchantBase = metaMerchant.replace(/\/$/, ''); 59 const merchantUrl = new URL(merchantBase); 60 const merchantProto = merchantUrl.protocol; 61 const suffix = merchantProto === 'http:' ? '+http' : ''; 62 const merchantHost = merchantUrl.host; 63 const merchantPath = merchantUrl.path; 64 65 const expTime = Math.floor(Date.now() / 1000) + maxPickupDelay; 66 const nonceArr = new Uint8Array(32); 67 crypto.getRandomValues(nonceArr); 68 const nonce = Array.from(nonceArr) 69 .map(b => b.toString(16).padStart(2, '0')).join(''); 70 71 const paivanaId = await makePaivanaId(expTime, nonce, website); 72 73 const talerUri = [ 74 `taler${suffix}://pay-template/`, 75 merchantHost, 76 merchantPath, 77 `/`, 78 metaTemplate, 79 `?session_id=${encodeURIComponent(paivanaId)}`, 80 `&fulfillment_url=${encodeURIComponent(href)}` 81 ].join(''); 82 83 const talerLink = document.getElementById('talerlink'); 84 talerLink.href = talerUri; 85 talerLink.textContent = talerUri; 86 87 new QRCode(document.getElementById('qrcode'), { 88 text: talerUri, 89 width: 200, 90 height: 200, 91 correctLevel: QRCode.CorrectLevel.M 92 }); 93 94 async function confirmPayment(orderId) { 95 talerLink.textContent = 'Payment confirmed! Loading page...'; 96 talerLink.href = '#'; 97 try { 98 const res = await fetch(`${website}/.well-known/paivana`, { 99 method: 'POST', 100 headers: { 'Content-Type': 'application/json' }, 101 body: JSON.stringify({ order_id: orderId, nonce, cur_time: expTime, website }), 102 }); 103 const dest = res.redirected ? res.url : website; 104 // FIXME: 400 just for testing... 105 await waitMs(400); 106 location.href = dest; 107 } catch (e) { 108 console.warn('[paivana] confirm error:', e); 109 talerLink.href = '#'; 110 talerLink.textContent = 'Could not reach the server!'; 111 } 112 } 113 114 const pollBase = `${website}/.well-known/paivana/sessions/` 115 + encodeURIComponent(paivanaId); 116 while (true) { 117 const start = performance.now(); 118 try { 119 const res = await fetch(`${pollBase}?timeout_ms=30000`, { cache: 'no-store' }); 120 if (res.status === 200) { 121 let orderId = null; 122 try { orderId = (await res.json()).order_id ?? null; } catch (_) {} 123 if (orderId) { 124 await confirmPayment(orderId); 125 } else { 126 // FIXME: for testing... 127 await waitMs(400); 128 location.href = website; 129 } 130 return; 131 } 132 } catch (e) { 133 console.warn('[paivana] poll error:', e); 134 talerLink.href = '#'; 135 talerLink.textContent = 'Network error! Retrying...'; 136 } 137 const remMs = Math.round(30_000 - (performance.now() - start)); 138 if (remMs > 0) await waitMs(remMs); 139 } 140 141 })(); 142 </script> 143 </body> 144 </html>