<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Invoice Payment - {{name}}</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> <link rel="stylesheet" href="./css/style.css"> <script src="https://unpkg.com/webln@0.2.0/dist/webln.min.js" integrity="sha384-mTReBqbhPO7ljQeIoFaD1NYS2KiYMwFJhUNpdwLj+VIuhhjvHQlZ1XpwzAvd93nQ" crossorigin="anonymous" ></script> </head> <body lang="en"> <main class="container"> <form method="post" action="/invoices"> <div class="row"> <div class="col"> <h1 class="mt-4 mb-4 text-center text-nowrap">{{name}}</h1> </div> </div> <div class="row"> <div class="col text-center"> <p class="pending"> Scan with your preferred Bitcoin Lightning wallet: </p> <p class="paid d-none text-success"> You may now connect to {{relay_url}} </p> <p class="expired d-none text-secondary"> Your invoice expired. Try again! </p> </div> </div> <div class="row justify-content-center"> <div class="card pending col-8 col-lg-4 d-flex flex-column justify-content-center mb-4"> <div class="card-body m-auto"> <div id="invoice" onclick="sendPayment()"></div> </div> <div class="card-body d-flex flex-row justify-content-center"> <div class="input-group input-group-sm w-100 mw-256" onclick="copy()"> <input type="text" name="invoice" class="form-control form-control-sm" id="invoiceInput" value="{{invoice}}" readonly> <span class="input-group-text" id="invoiceAlert">copy</span> </div> </div> <div class="card-body d-flex flex-row justify-content-center"> <div> <div class="spinner-grow spinner-grow-sm" role="status"></div> Waiting for payment... </div> </div> </div> <div class="card paid d-none col-8 col-lg-4 justify-content-center"> <div class="card-body text-center"> <div class="success-checkmark"> <div class="check-icon"> <span class="icon-line line-tip"></span> <span class="icon-line line-long"></span> <div class="icon-circle"></div> <div class="icon-fix"></div> </div> </div> <h2 class="text-success">Payment successful!</h2> <p class="text-secondary">{{amount}} sats received</p> </div> </div> <div class="card expired d-none col-8 col-lg-4 justify-content-center mb-4"> <div class="card-body text-center"> <h2 class="text-danger">Invoice expired!</h2> </div> </div> </div> <div class="row pending d-none"> <div class="col"> <div class="d-flex justify-content-center mb-3"> <button id="sendPaymentBtn" class="btn btn-lg btn-warning d-none" type="submit" onclick="sendPayment()">Pay with wallet</button> </div> </div> </div> <div class="row expired d-none"> <div class="d-flex justify-content-center mb-3"> <input type="hidden" name="pubkey" value="{{pubkey}}" required> <input type="checkbox" class="d-none" name="tosAccepted" value="yes" checked required> <input type="hidden" name="feeSchedule" value="admission" /> <button class="btn btn-lg btn-primary" type="submit">Get another invoice</button> </div> </div> <div class="row"> <div class="d-flex justify-content-center mb-3 mt-4"> <a href="https://zeb.gg/nostr-zbd-quickstart" target="_blank"> <img class="poweredbyzbd-img" src="https://cdn.zebedee.io/an/nostr/poweredbyzbd.png" /> </a> </div> </div> </form> </main> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script> var reference = "{{reference}}" var relayUrl = "{{relay_url}}" var relayPubkey = "{{relay_pubkey}}" var invoice = "{{invoice}}"; var pubkey = "{{pubkey}}" var expiresAt = "{{expires_at}}" var timeout var paid = false var fallbackTimeout function getBackoffTime() { return 5000 + Math.floor(Math.random() * 5000) } async function getInvoiceStatus() { fetch(`/invoices/${reference}/status`).then(async (response) => { const data = await response.json() console.log('data', data) const { status } = data; if (status === 'pending') { fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime()) return } else if (status === 'expired') { hide('pending') show('expired') return } paid = true clearTimeout(timeout) hide('pending') show('paid') }, (error) => { console.error('error fetching status', error) fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime()) }) } fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime) function connect() { var socket = new WebSocket(relayUrl) socket.onopen = () => { console.log('connected') socket.send(JSON.stringify(['REQ', 'payment', { kinds: [4], authors: [relayPubkey], '#c': [reference], limit: 1 }])) } socket.onmessage = (raw) => { const message = JSON.parse(raw.data) console.log('received', message) if (!Array.isArray(message) || message.length < 2 || message[1] !== 'payment') { return } switch (message[0]) { case 'EVENT': { // TODO: validate event const event = message[2] // TODO: validate signature if (event.pubkey === relayPubkey) { paid = true clearTimeout(timeout) hide('pending') show('paid') } } break; } if (!paid && message[0] === 'EOSE' && message[1] === 'payment') { return } if (message.length !== 3 || message[0] !== 'EVENT' || message[1] !== 'payment') { return } } socket.onerror = console.error.bind(console) socket.onclose = () => { console.log('disconnected') setTimeout(connect, 1000) } } function show(className) { return toggle(className, true) } function hide(className) { return toggle(className, false) } function toggle(className, show) { const elements = document.getElementsByClassName(className) for (const elem of elements) { if (show) { elem.classList.remove('d-none') } else { elem.classList.add('d-none') } } } const expiry = (new Date(expiresAt).getTime() - new Date().getTime()) console.log('expiry at', expiresAt, Math.floor(expiry / 1000)) timeout = setTimeout(() => { hide('pending') show('expired') }, expiry) new QRCode(document.getElementById("invoice"), { text: `lightning:${invoice}`, width: 256, height: 256, correctLevel: QRCode.CorrectLevel.M }); function copy() { var elem = document.getElementById('invoiceInput') elem.select() elem.setSelectionRange(0, 999999) navigator.clipboard.writeText(elem.value) document.getElementById('invoiceAlert').innerText = 'copied!' } async function sendPayment() { const webln = await WebLN.requestProvider(); webln.sendPayment(invoice) } connect() sendPayment().catch(() => { document.getElementById('sendPaymentBtn').classList.remove('d-none') }) </script> </body> </html>