mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-18 13:51:53 +01:00
* fix: dont crash when SECRET is not set * docs: add semisol to contributors * docs: improve readme * docs: add payment info to readme * docs: add zebedee_api_key to configuration.md * fix: confirm_invoice unit var * chore: remove unused code * chore: improve error logging for payments * chore: use instead of changeme * chore: fix typo * chore: improve get invoice status ctrl * fix: csp bug * chore: remove rate limits * chore: improve invoice page logging * chore: prevent root with start_local * chore: revert to redis 4.5.1
247 lines
8.8 KiB
HTML
247 lines
8.8 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">
|
|
<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
|
|
|
|
console.log('invoice id', reference)
|
|
|
|
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
|
|
} else {
|
|
console.log('invoice status', status)
|
|
}
|
|
|
|
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> |