nostream/resources/invoices.html
Ricardo Arturo Cabral Mejía 52aac39875
feat: implement nodeless payments processor (#305)
* chore: hide powered by zebedee if payment processor is not

* chore: add nodeless as payments processor to settings

* fix: bad content type on zebedee callback req handler

* chore(release): 1.23.0 [skip ci]

# [1.23.0](https://github.com/Cameri/nostream/compare/v1.22.6...v1.23.0) (2023-05-12)

### Bug Fixes

* add SECRET as env variable ([#298](https://github.com/Cameri/nostream/issues/298)) ([58a1254](58a12546f0))
* invoice auto marked as paid ([be6d6f1](be6d6f1454))
* issues with invoices ([#271](https://github.com/Cameri/nostream/issues/271)) ([e1561e7](e1561e78fd))

### Features

* add LNURL processor ([#202](https://github.com/Cameri/nostream/issues/202)) ([f237400](f23740073f))
* allow lightning zap receipts on paid relays ([#303](https://github.com/Cameri/nostream/issues/303)) ([14bc96f](14bc96f516))

* feat: implement nodeless payments processor

* docs: add accepting payments section

* chore: validate nodeless webhook secret

* chore: hide powered-by-zebedee for non-zebedee processors

---------

Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
2023-05-15 08:07:28 -07:00

263 lines
9.4 KiB
HTML

<!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 d-none" id="powered-by-zebedee">
<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 processor = "{{processor}}"
var timeout
var paid = false
var fallbackTimeout
var now = Math.floor(Date.now()/1000)
console.log('invoice id', reference)
console.log('pubkey', pubkey)
console.log('bolt11', invoice)
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;
console.log('invoice status', status)
if (status === 'expired') {
hide('pending')
show('expired')
return
} else if (status !== 'completed') {
fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime())
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')
var subscription = ['REQ', 'payment', { kinds: [402], '#p': [pubkey], since: now - 60 }]
socket.send(JSON.stringify(subscription))
}
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': {
const event = message[2]
if (
event.pubkey === relayPubkey
&& event.kind === 402
) {
const pubkeyTag = event.tags.find((t) => t[0] === 'p' && t[1] === pubkey)
const invoiceTag = event.tags.find((t) => t[0] === 'bolt11' && t[1] === invoice)
if (pubkeyTag && invoiceTag) {
paid = true
if (expiresAt) 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')
}
}
}
if (expiresAt) {
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')
})
if (processor === 'zebedee') {
document.getElementById('powered-by-zebedee').classList.remove('d-none')
}
</script>
</body>
</html>