Browse Source

apply prettier to everything.

Login
fiatjaf 5 years ago
parent
commit
4730500ed7
  1. 2
      .gitignore
  2. 12
      .prettierrc
  3. 2
      Makefile
  4. 2
      lnbits/core/static/js/extensions.js
  5. 12
      lnbits/core/static/js/index.js
  6. 339
      lnbits/core/static/js/wallet.js
  7. 40
      lnbits/extensions/amilk/templates/amilk/_api_docs.html
  8. 444
      lnbits/extensions/amilk/templates/amilk/index.html
  9. 125
      lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html
  10. 1422
      lnbits/extensions/diagonalley/templates/diagonalley/index.html
  11. 4
      lnbits/extensions/diagonalley/templates/diagonalley/stall.html
  12. 607
      lnbits/extensions/events/templates/events/display.html
  13. 815
      lnbits/extensions/events/templates/events/index.html
  14. 790
      lnbits/extensions/events/templates/events/registration.html
  15. 415
      lnbits/extensions/events/templates/events/ticket.html
  16. 107
      lnbits/extensions/example/templates/example/index.html
  17. 20
      lnbits/extensions/paywall/templates/paywall/_api_docs.html
  18. 199
      lnbits/extensions/paywall/templates/paywall/display.html
  19. 444
      lnbits/extensions/paywall/templates/paywall/index.html
  20. 65
      lnbits/extensions/tpos/templates/tpos/_api_docs.html
  21. 19
      lnbits/extensions/tpos/templates/tpos/_tpos.html
  22. 596
      lnbits/extensions/tpos/templates/tpos/index.html
  23. 454
      lnbits/extensions/tpos/templates/tpos/tpos.html
  24. 200
      lnbits/extensions/usermanager/templates/usermanager/_api_docs.html
  25. 777
      lnbits/extensions/usermanager/templates/usermanager/index.html
  26. 104
      lnbits/extensions/withdraw/templates/withdraw/_api_docs.html
  27. 31
      lnbits/extensions/withdraw/templates/withdraw/_lnurl.html
  28. 103
      lnbits/extensions/withdraw/templates/withdraw/display.html
  29. 440
      lnbits/extensions/withdraw/templates/withdraw/index.html
  30. 115
      lnbits/extensions/withdraw/templates/withdraw/print_qr.html
  31. 244
      lnbits/static/js/base.js
  32. 65
      lnbits/static/js/components.js
  33. 5
      package.json

2
.gitignore

@ -27,3 +27,5 @@ venv
.pyre* .pyre*
__bundle__ __bundle__
node_modules

12
.prettierrc

@ -0,0 +1,12 @@
{
"semi": false,
"arrowParens": "avoid",
"insertPragma": false,
"printWidth": 80,
"proseWrap": "preserve",
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"jsxBracketSameLine": false,
"bracketSpacing": false
}

2
Makefile

@ -0,0 +1,2 @@
prettier:
./node_modules/.bin/prettier --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**

2
lnbits/core/static/js/extensions.js

@ -1,4 +1,4 @@
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin] mixins: [windowMixin]
}); })

12
lnbits/core/static/js/index.js

@ -3,23 +3,23 @@ new Vue({
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
disclaimerDialog: { disclaimerDialog: {
show: false, show: false,
data: {} data: {}
}, },
walletName: '' walletName: ''
}; }
}, },
methods: { methods: {
createWallet: function () { createWallet: function () {
LNbits.href.createWallet(this.walletName); LNbits.href.createWallet(this.walletName)
}, },
processing: function () { processing: function () {
this.$q.notify({ this.$q.notify({
timeout: 0, timeout: 0,
message: 'Processing...', message: 'Processing...',
icon: null icon: null
}); })
} }
} }
}); })

339
lnbits/core/static/js/wallet.js

@ -1,39 +1,49 @@
Vue.component(VueQrcode.name, VueQrcode); Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader); Vue.use(VueQrcodeReader)
function generateChart(canvas, payments) { function generateChart(canvas, payments) {
var txs = []; var txs = []
var n = 0; var n = 0
var data = { var data = {
labels: [], labels: [],
income: [], income: [],
outcome: [], outcome: [],
cumulative: [] cumulative: []
}; }
_.each(payments.slice(0).sort(function (a, b) { _.each(
return a.time - b.time; payments.slice(0).sort(function (a, b) {
}), function (tx) { return a.time - b.time
txs.push({ }),
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'), function (tx) {
sat: tx.sat, txs.push({
}); hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
}); sat: tx.sat
})
}
)
_.each(_.groupBy(txs, 'hour'), function (value, day) { _.each(_.groupBy(txs, 'hour'), function (value, day) {
var income = _.reduce(value, function(memo, tx) { var income = _.reduce(
return (tx.sat >= 0) ? memo + tx.sat : memo; value,
}, 0); function (memo, tx) {
var outcome = _.reduce(value, function(memo, tx) { return tx.sat >= 0 ? memo + tx.sat : memo
return (tx.sat < 0) ? memo + Math.abs(tx.sat) : memo; },
}, 0); 0
n = n + income - outcome; )
data.labels.push(day); var outcome = _.reduce(
data.income.push(income); value,
data.outcome.push(outcome); function (memo, tx) {
data.cumulative.push(n); return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo
}); },
0
)
n = n + income - outcome
data.labels.push(day)
data.income.push(income)
data.outcome.push(outcome)
data.cumulative.push(n)
})
new Chart(canvas.getContext('2d'), { new Chart(canvas.getContext('2d'), {
type: 'bar', type: 'bar',
@ -44,7 +54,7 @@ function generateChart(canvas, payments) {
data: data.cumulative, data: data.cumulative,
type: 'line', type: 'line',
label: 'balance', label: 'balance',
backgroundColor: '#673ab7', // deep-purple backgroundColor: '#673ab7', // deep-purple
borderColor: '#673ab7', borderColor: '#673ab7',
borderWidth: 4, borderWidth: 4,
pointRadius: 3, pointRadius: 3,
@ -55,14 +65,14 @@ function generateChart(canvas, payments) {
type: 'bar', type: 'bar',
label: 'in', label: 'in',
barPercentage: 0.75, barPercentage: 0.75,
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
}, },
{ {
data: data.outcome, data: data.outcome,
type: 'bar', type: 'bar',
label: 'out', label: 'out',
barPercentage: 0.75, barPercentage: 0.75,
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
} }
] ]
}, },
@ -72,18 +82,20 @@ function generateChart(canvas, payments) {
}, },
tooltips: { tooltips: {
mode: 'index', mode: 'index',
intersect:false intersect: false
}, },
scales: { scales: {
xAxes: [{ xAxes: [
type: 'time', {
display: true, type: 'time',
offset: true, display: true,
time: { offset: true,
minUnit: 'hour', time: {
stepSize: 3 minUnit: 'hour',
stepSize: 3
}
} }
}], ]
}, },
// performance tweaks // performance tweaks
animation: { animation: {
@ -95,10 +107,9 @@ function generateChart(canvas, payments) {
} }
} }
} }
}); })
} }
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
@ -128,8 +139,20 @@ new Vue({
paymentsTable: { paymentsTable: {
columns: [ columns: [
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'}, {name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true}, {
{name: 'sat', align: 'right', label: 'Amount (sat)', field: 'sat', sortable: true} name: 'date',
align: 'left',
label: 'Date',
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: 'Amount (sat)',
field: 'sat',
sortable: true
}
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10
@ -143,46 +166,50 @@ new Vue({
show: false, show: false,
location: window.location location: window.location
} }
}; }
}, },
computed: { computed: {
filteredPayments: function () { filteredPayments: function () {
var q = this.paymentsTable.filter; var q = this.paymentsTable.filter
if (!q || q == '') return this.payments; if (!q || q == '') return this.payments
return LNbits.utils.search(this.payments, q); return LNbits.utils.search(this.payments, q)
}, },
balance: function () { balance: function () {
if (this.payments.length) { if (this.payments.length) {
return _.pluck(this.payments, 'amount').reduce(function (a, b) { return a + b; }, 0) / 1000; return (
_.pluck(this.payments, 'amount').reduce(function (a, b) {
return a + b
}, 0) / 1000
)
} }
return this.g.wallet.sat; return this.g.wallet.sat
}, },
fbalance: function () { fbalance: function () {
return LNbits.utils.formatSat(this.balance) return LNbits.utils.formatSat(this.balance)
}, },
canPay: function () { canPay: function () {
if (!this.send.invoice) return false; if (!this.send.invoice) return false
return this.send.invoice.sat <= this.balance; return this.send.invoice.sat <= this.balance
}, },
pendingPaymentsExist: function () { pendingPaymentsExist: function () {
return (this.payments) return this.payments
? _.where(this.payments, {pending: 1}).length > 0 ? _.where(this.payments, {pending: 1}).length > 0
: false; : false
} }
}, },
methods: { methods: {
closeCamera: function () { closeCamera: function () {
this.sendCamera.show = false; this.sendCamera.show = false
}, },
showCamera: function () { showCamera: function () {
this.sendCamera.show = true; this.sendCamera.show = true
}, },
showChart: function () { showChart: function () {
this.paymentsChart.show = true; this.paymentsChart.show = true
this.$nextTick(function () { this.$nextTick(function () {
generateChart(this.$refs.canvas, this.payments); generateChart(this.$refs.canvas, this.payments)
}); })
}, },
showReceiveDialog: function () { showReceiveDialog: function () {
this.receive = { this.receive = {
@ -194,7 +221,7 @@ new Vue({
memo: '' memo: ''
}, },
paymentChecker: null paymentChecker: null
}; }
}, },
showSendDialog: function () { showSendDialog: function () {
this.send = { this.send = {
@ -204,57 +231,64 @@ new Vue({
bolt11: '' bolt11: ''
}, },
paymentChecker: null paymentChecker: null
}; }
}, },
closeReceiveDialog: function () { closeReceiveDialog: function () {
var checker = this.receive.paymentChecker; var checker = this.receive.paymentChecker
setTimeout(function () { setTimeout(function () {
clearInterval(checker); clearInterval(checker)
}, 10000); }, 10000)
}, },
closeSendDialog: function () { closeSendDialog: function () {
this.sendCamera.show = false; this.sendCamera.show = false
var checker = this.send.paymentChecker; var checker = this.send.paymentChecker
setTimeout(function () { setTimeout(function () {
clearInterval(checker); clearInterval(checker)
}, 1000); }, 1000)
}, },
createInvoice: function () { createInvoice: function () {
var self = this; var self = this
this.receive.status = 'loading'; this.receive.status = 'loading'
LNbits.api.createInvoice(this.g.wallet, this.receive.data.amount, this.receive.data.memo) LNbits.api
.createInvoice(
this.g.wallet,
this.receive.data.amount,
this.receive.data.memo
)
.then(function (response) { .then(function (response) {
self.receive.status = 'success'; self.receive.status = 'success'
self.receive.paymentReq = response.data.payment_request; self.receive.paymentReq = response.data.payment_request
self.receive.paymentChecker = setInterval(function () { self.receive.paymentChecker = setInterval(function () {
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (response) { LNbits.api
if (response.data.paid) { .getPayment(self.g.wallet, response.data.checking_id)
self.fetchPayments(); .then(function (response) {
self.receive.show = false; if (response.data.paid) {
clearInterval(self.receive.paymentChecker); self.fetchPayments()
} self.receive.show = false
}); clearInterval(self.receive.paymentChecker)
}, 2000); }
})
}).catch(function (error) { }, 2000)
LNbits.utils.notifyApiError(error); })
self.receive.status = 'pending'; .catch(function (error) {
}); LNbits.utils.notifyApiError(error)
self.receive.status = 'pending'
})
}, },
decodeQR: function (res) { decodeQR: function (res) {
this.send.data.bolt11 = res; this.send.data.bolt11 = res
this.decodeInvoice(); this.decodeInvoice()
this.sendCamera.show = false; this.sendCamera.show = false
}, },
decodeInvoice: function () { decodeInvoice: function () {
if (this.send.data.bolt11.startsWith('lightning:')) { if (this.send.data.bolt11.startsWith('lightning:')) {
this.send.data.bolt11 = this.send.data.bolt11.slice(10); this.send.data.bolt11 = this.send.data.bolt11.slice(10)
} }
let invoice; let invoice
try { try {
invoice = decode(this.send.data.bolt11); invoice = decode(this.send.data.bolt11)
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
timeout: 3000, timeout: 3000,
@ -262,101 +296,120 @@ new Vue({
message: error + '.', message: error + '.',
caption: '400 BAD REQUEST', caption: '400 BAD REQUEST',
icon: null icon: null
}); })
return; return
} }
let cleanInvoice = { let cleanInvoice = {
msat: invoice.human_readable_part.amount, msat: invoice.human_readable_part.amount,
sat: invoice.human_readable_part.amount / 1000, sat: invoice.human_readable_part.amount / 1000,
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000) fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
}; }
_.each(invoice.data.tags, function (tag) { _.each(invoice.data.tags, function (tag) {
if (_.isObject(tag) && _.has(tag, 'description')) { if (_.isObject(tag) && _.has(tag, 'description')) {
if (tag.description == 'payment_hash') { cleanInvoice.hash = tag.value; } if (tag.description == 'payment_hash') {
else if (tag.description == 'description') { cleanInvoice.description = tag.value; } cleanInvoice.hash = tag.value
else if (tag.description == 'expiry') { } else if (tag.description == 'description') {
var expireDate = new Date((invoice.data.time_stamp + tag.value) * 1000); cleanInvoice.description = tag.value
cleanInvoice.expireDate = Quasar.utils.date.formatDate(expireDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ'); } else if (tag.description == 'expiry') {
cleanInvoice.expired = false; // TODO var expireDate = new Date(
(invoice.data.time_stamp + tag.value) * 1000
)
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
expireDate,
'YYYY-MM-DDTHH:mm:ss.SSSZ'
)
cleanInvoice.expired = false // TODO
} }
} }
}); })
this.send.invoice = Object.freeze(cleanInvoice); this.send.invoice = Object.freeze(cleanInvoice)
}, },
payInvoice: function () { payInvoice: function () {
var self = this; var self = this
dismissPaymentMsg = this.$q.notify({ dismissPaymentMsg = this.$q.notify({
timeout: 0, timeout: 0,
message: 'Processing payment...', message: 'Processing payment...',
icon: null icon: null
}); })
LNbits.api.payInvoice(this.g.wallet, this.send.data.bolt11).then(function (response) { LNbits.api
self.send.paymentChecker = setInterval(function () { .payInvoice(this.g.wallet, this.send.data.bolt11)
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (res) { .then(function (response) {
if (res.data.paid) { self.send.paymentChecker = setInterval(function () {
self.send.show = false; LNbits.api
clearInterval(self.send.paymentChecker); .getPayment(self.g.wallet, response.data.checking_id)
dismissPaymentMsg(); .then(function (res) {
self.fetchPayments(); if (res.data.paid) {
} self.send.show = false
}); clearInterval(self.send.paymentChecker)
}, 2000); dismissPaymentMsg()
}).catch(function (error) { self.fetchPayments()
dismissPaymentMsg(); }
LNbits.utils.notifyApiError(error); })
}); }, 2000)
})
.catch(function (error) {
dismissPaymentMsg()
LNbits.utils.notifyApiError(error)
})
}, },
deleteWallet: function (walletId, user) { deleteWallet: function (walletId, user) {
LNbits.utils.confirmDialog( LNbits.utils
'Are you sure you want to delete this wallet?' .confirmDialog('Are you sure you want to delete this wallet?')
).onOk(function () { .onOk(function () {
LNbits.href.deleteWallet(walletId, user); LNbits.href.deleteWallet(walletId, user)
}); })
}, },
fetchPayments: function (checkPending) { fetchPayments: function (checkPending) {
var self = this; var self = this
return LNbits.api.getPayments(this.g.wallet, checkPending).then(function (response) { return LNbits.api
self.payments = response.data.map(function (obj) { .getPayments(this.g.wallet, checkPending)
return LNbits.map.payment(obj); .then(function (response) {
}).sort(function (a, b) { self.payments = response.data
return b.time - a.time; .map(function (obj) {
}); return LNbits.map.payment(obj)
}); })
.sort(function (a, b) {
return b.time - a.time
})
})
}, },
checkPendingPayments: function () { checkPendingPayments: function () {
var dismissMsg = this.$q.notify({ var dismissMsg = this.$q.notify({
timeout: 0, timeout: 0,
message: 'Checking pending transactions...', message: 'Checking pending transactions...',
icon: null icon: null
}); })
this.fetchPayments(true).then(function () { this.fetchPayments(true).then(function () {
dismissMsg(); dismissMsg()
}); })
}, },
exportCSV: function () { exportCSV: function () {
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments); LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments)
} }
}, },
watch: { watch: {
'payments': function () { payments: function () {
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance]); EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance])
} }
}, },
created: function () { created: function () {
this.fetchPayments(); this.fetchPayments()
setTimeout(this.checkPendingPayments(), 1200); setTimeout(this.checkPendingPayments(), 1200)
}, },
mounted: function () { mounted: function () {
if (this.$refs.disclaimer && !this.$q.localStorage.getItem('lnbits.disclaimerShown')) { if (
this.disclaimerDialog.show = true; this.$refs.disclaimer &&
this.$q.localStorage.set('lnbits.disclaimerShown', true); !this.$q.localStorage.getItem('lnbits.disclaimerShown')
) {
this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true)
} }
} }
}); })

40
lnbits/extensions/amilk/templates/amilk/_api_docs.html

@ -1,16 +1,24 @@
<q-expansion-item
<q-expansion-item group="extras"
group="extras" icon="swap_vertical_circle"
icon="swap_vertical_circle" label="Info"
label="Info" :content-inset-level="0.5"
:content-inset-level="0.5" >
> <q-card>
<q-card> <q-card-section>
<q-card-section> <h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5> <p>
<p>Milking faucets with software, known as "assmilking", seems at first to be black-hat, although in fact there might be some unexplored use cases. An LNURL withdraw gives someone the right to pull funds, which can be done over time. An LNURL withdraw could be used outside of just faucets, to provide money streaming and repeat payments.<br/>Paste or scan an LNURL withdraw, enter the amount for the AMilk to pull and the frequency for it to be pulled.<br/> Milking faucets with software, known as "assmilking", seems at first to
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p> be black-hat, although in fact there might be some unexplored use cases.
</q-card> An LNURL withdraw gives someone the right to pull funds, which can be
</q-card-section> done over time. An LNURL withdraw could be used outside of just faucets,
to provide money streaming and repeat payments.<br />Paste or scan an
</q-card-section></q-expansion-item> LNURL withdraw, enter the amount for the AMilk to pull and the frequency
for it to be pulled.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>

444
lnbits/extensions/amilk/templates/amilk/index.html

@ -1,230 +1,252 @@
{% extends "base.html" %} {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% from "macros.jinja" import window_vars with context %} <div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
{% block page %} <q-card-section>
<div class="row q-col-gutter-md"> <q-btn unelevated color="deep-purple" @click="amilkDialog.show = true"
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> >New AMilk</q-btn
<q-card> >
<q-card-section> </q-card-section>
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true">New AMilk</q-btn> </q-card>
</q-card-section>
</q-card> <q-card>
<q-card-section>
<q-card> <div class="row items-center no-wrap q-mb-md">
<q-card-section> <div class="col">
<div class="row items-center no-wrap q-mb-md"> <h5 class="text-subtitle1 q-my-none">AMilks</h5>
<div class="col">
<h5 class="text-subtitle1 q-my-none">AMilks</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div> </div>
<q-table dense flat <div class="col-auto">
:data="amilks" <q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
row-key="id" </div>
:columns="amilksTable.columns" </div>
:pagination.sync="amilksTable.pagination"> <q-table
{% raw %} dense
<template v-slot:header="props"> flat
<q-tr :props="props"> :data="amilks"
row-key="id"
<q-th :columns="amilksTable.columns"
v-for="col in props.cols" :pagination.sync="amilksTable.pagination"
:key="col.name" >
:props="props" {% raw %}
> <template v-slot:header="props">
{{ col.label }} <q-tr :props="props">
</q-th> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-tr> </q-th>
</template> </q-tr>
<template v-slot:body="props"> </template>
<q-tr :props="props"> <template v-slot:body="props">
<q-tr :props="props">
<q-td <q-td v-for="col in props.cols" :key="col.name" :props="props">
v-for="col in props.cols" {{ col.value }}
:key="col.name" </q-td>
:props="props" <q-td auto-width>
<q-btn
> flat
{{ col.value }} dense
</q-td> size="xs"
<q-td auto-width> @click="deleteAMilk(props.row.id)"
<q-btn flat dense size="xs" @click="deleteAMilk(props.row.id)" icon="cancel" color="pink"></q-btn> icon="cancel"
</q-td> color="pink"
</q-tr> ></q-btn>
</template> </q-td>
{% endraw %} </q-tr>
</q-table> </template>
</q-card-section> {% endraw %}
</q-card> </q-table>
</div> </q-card-section>
</q-card>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Assistant Faucet Milker Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "amilk/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="amilkDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createAMilk" class="q-gutter-md">
<q-select filled dense emit-value v-model="amilkDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="amilkDialog.data.lnurl"
type="url"
label="LNURL Withdraw"></q-input>
<q-input filled dense
v-model.number="amilkDialog.data.amount"
type="number"
label="Amount *"></q-input>
<q-input filled dense
v-model.trim="amilkDialog.data.atime"
type="number"
label="Hit frequency (secs)"
placeholder="Frequency to be hit"></q-input>
<q-btn unelevated
color="deep-purple"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
type="submit">Create amilk</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div> </div>
{% endblock %}
{% block scripts %} <div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
{{ window_vars(user) }} <q-card>
<script> <q-card-section>
var mapAMilk = function (obj) { <h6 class="text-subtitle1 q-my-none">
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm'); LNbits Assistant Faucet Milker Extension
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount); </h6>
obj.wall = ['/amilk/', obj.id].join(''); </q-card-section>
return obj; <q-card-section class="q-pa-none">
} <q-separator></q-separator>
<q-list>
{% include "amilk/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
new Vue({ <q-dialog v-model="amilkDialog.show" position="top">
el: '#vue', <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
mixins: [windowMixin], <q-form @submit="createAMilk" class="q-gutter-md">
data: function () { <q-select
return { filled
amilks: [], dense
amilksTable: { emit-value
columns: [ v-model="amilkDialog.data.wallet"
{name: 'id', align: 'left', label: 'ID', field: 'id'}, :options="g.user.walletOptions"
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'}, label="Wallet *"
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'}, >
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'} </q-select>
], <q-input
pagination: { filled
rowsPerPage: 10 dense
} v-model.trim="amilkDialog.data.lnurl"
}, type="url"
amilkDialog: { label="LNURL Withdraw"
show: false, ></q-input>
data: {} <q-input
filled
dense
v-model.number="amilkDialog.data.amount"
type="number"
label="Amount *"
></q-input>
<q-input
filled
dense
v-model.trim="amilkDialog.data.atime"
type="number"
label="Hit frequency (secs)"
placeholder="Frequency to be hit"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
type="submit"
>Create amilk</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapAMilk = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.wall = ['/amilk/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
amilks: [],
amilksTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
],
pagination: {
rowsPerPage: 10
} }
}; },
}, amilkDialog: {
methods: { show: false,
data: {}
}
}
getAMilks: function () { },
var self = this; methods: {
getAMilks: function () {
var self = this
LNbits.api.request( LNbits.api
.request(
'GET', 'GET',
'/amilk/api/v1/amilk?all_wallets', '/amilk/api/v1/amilk?all_wallets',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
).then(function (response) { )
.then(function (response) {
self.amilks = response.data.map(function (obj) { self.amilks = response.data.map(function (obj) {
response.data.forEach(MILK); response.data.forEach(MILK)
function MILK(item){ function MILK(item) {
window.setInterval(function () {
window.setInterval(function(){ LNbits.api
.request(
'GET',
LNbits.api.request( '/amilk/api/v1/amilk/milk/' + item.id,
'GET', 'Lorem'
'/amilk/api/v1/amilk/milk/' + item.id, )
"Lorem" .then(function (response) {
).then(function (response) { self.amilks = response.data.map(function (obj) {
self.amilks = response.data.map(function (obj) { return mapAMilk(obj)
return mapAMilk(obj); })
}); })
}); }, item.atime * 1000)
},
item.atime*1000);
} }
return mapAMilk(obj); return mapAMilk(obj)
}); })
}); })
}, },
createAMilk: function () { createAMilk: function () {
var data = { var data = {
lnurl: this.amilkDialog.data.lnurl, lnurl: this.amilkDialog.data.lnurl,
atime: parseInt(this.amilkDialog.data.atime), atime: parseInt(this.amilkDialog.data.atime),
amount: this.amilkDialog.data.amount amount: this.amilkDialog.data.amount
}; }
var self = this; var self = this
console.log(this.amilkDialog.data.wallet); console.log(this.amilkDialog.data.wallet)
LNbits.api.request( LNbits.api
.request(
'POST', 'POST',
'/amilk/api/v1/amilk', '/amilk/api/v1/amilk',
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet}).inkey, _.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet})
.inkey,
data data
).then(function (response) { )
self.amilks.push(mapAMilk(response.data)); .then(function (response) {
self.amilkDialog.show = false; self.amilks.push(mapAMilk(response.data))
self.amilkDialog.data = {}; self.amilkDialog.show = false
}).catch(function (error) { self.amilkDialog.data = {}
LNbits.utils.notifyApiError(error); })
}); .catch(function (error) {
}, LNbits.utils.notifyApiError(error)
deleteAMilk: function (amilkId) { })
var self = this;
var amilk = _.findWhere(this.amilks, {id: amilkId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this AMilk link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/amilk/api/v1/amilks/' + amilkId,
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
).then(function (response) {
self.amilks = _.reject(self.amilks, function (obj) { return obj.id == amilkId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportCSV: function () {
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks);
}
}, },
created: function () { deleteAMilk: function (amilkId) {
if (this.g.user.wallets.length) { var self = this
this.getAMilks(); var amilk = _.findWhere(this.amilks, {id: amilkId})
}
LNbits.utils
.confirmDialog('Are you sure you want to delete this AMilk link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/amilk/api/v1/amilks/' + amilkId,
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
)
.then(function (response) {
self.amilks = _.reject(self.amilks, function (obj) {
return obj.id == amilkId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks)
} }
}); },
</script> created: function () {
if (this.g.user.wallets.length) {
this.getAMilks()
}
}
})
</script>
{% endblock %} {% endblock %}

125
lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html

@ -1,20 +1,29 @@
<q-expansion-item
<q-expansion-item group="extras"
group="extras" icon="swap_vertical_circle"
icon="swap_vertical_circle" label="Info"
label="Info" :content-inset-level="0.5"
:content-inset-level="0.5" >
> <q-card>
<q-card> <q-card-section>
<q-card-section> <h5 class="text-subtitle1 q-my-none">
<h5 class="text-subtitle1 q-my-none">Diagon Alley: Decentralised Market-Stalls</h5> Diagon Alley: Decentralised Market-Stalls
<p>Make a list of products to sell, point your list of products at a public indexer. Buyers browse your products on the indexer, and pay you directly. Ratings are managed by the indexer. Your stall can be listed in multiple indexers, even over TOR, if you wish to be anonymous.<br/> </h5>
More information on the <a href="https://github.com/lnbits/Diagon-Alley">Diagon Alley Protocol</a><br/> <p>
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p> Make a list of products to sell, point your list of products at a public
</q-card> indexer. Buyers browse your products on the indexer, and pay you
</q-card-section> directly. Ratings are managed by the indexer. Your stall can be listed
in multiple indexers, even over TOR, if you wish to be anonymous.<br />
</q-card-section> More information on the
<a href="https://github.com/lnbits/Diagon-Alley"
>Diagon Alley Protocol</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item <q-expansion-item
group="extras" group="extras"
@ -22,43 +31,91 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-expansion-item
<q-expansion-item group="api" dense expand-separator label="Get prodcuts, categorised by wallet"> group="api"
dense
expand-separator
label="Get prodcuts, categorised by wallet"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">GET</span> /api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code> <code
><span class="text-light-blue">GET</span>
/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>Product JSON list</code> <code>Product JSON list</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code> <code
>curl -X GET {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code
>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get invoice for product"> <q-expansion-item
group="api"
dense
expand-separator
label="Get invoice for product"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-green">POST</span> /api/v1/diagonalley/stall/order/&lt;indexer_id&gt;</code> <code
><span class="text-light-green">POST</span>
/api/v1/diagonalley/stall/order/&lt;indexer_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"id": &lt;string&gt;, "address": &lt;string&gt;, "shippingzone": &lt;integer&gt;, "email": &lt;string&gt;, "quantity": &lt;integer&gt;}</code> <code
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> >{"id": &lt;string&gt;, "address": &lt;string&gt;, "shippingzone":
<code>{"checking_id": &lt;string&gt;,"payment_request": &lt;string&gt;}</code> &lt;integer&gt;, "email": &lt;string&gt;, "quantity":
&lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/order/&lt;indexer_id&gt; -d '{"id": &lt;product_id&&gt;, "email": &lt;customer_email&gt;, "address": &lt;customer_address&gt;, "quantity": 2, "shippingzone": 1}' -H "Content-type: application/json" <code
</code> >curl -X POST {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/order/&lt;indexer_id&gt; -d
'{"id": &lt;product_id&&gt;, "email": &lt;customer_email&gt;,
"address": &lt;customer_address&gt;, "quantity": 2, "shippingzone":
1}' -H "Content-type: application/json"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Check a product has been shipped" <q-expansion-item
class="q-mb-md"> group="api"
dense
expand-separator
label="Check a product has been shipped"
class="q-mb-md"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">GET</span> /diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;</code> <code
><span class="text-light-blue">GET</span>
/diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200 OK (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"shipped": &lt;boolean&gt;}</code> <code>{"shipped": &lt;boolean&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt; -H "Content-type: application/json"</code> <code
>curl -X GET {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;
-H "Content-type: application/json"</code
>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

1422
lnbits/extensions/diagonalley/templates/diagonalley/index.html

File diff suppressed because it is too large

4
lnbits/extensions/diagonalley/templates/diagonalley/stall.html

@ -1 +1,3 @@
<script>console.log("{{ stall }}")</script> <script>
console.log('{{ stall }}')
</script>

607
lnbits/extensions/events/templates/events/display.html

@ -1,4 +1,4 @@
<!-- @format --> <!-- @format -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -202,204 +202,183 @@
type="text/javascript" type="text/javascript"
></script> ></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background: #1f2234;
}
<style> body {
color: #fff;
}
//GOOFY CSS HACK TO GO DARK .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background: #1f2234;
border-left-color: #8964a9;
}
.skin-blue .wrapper { .skin-blue .main-header .navbar {
background: background-color: #2e507d;
#1f2234; }
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed { .content-wrapper,
background: none; .right-side {
background-color: rgba(0, 0, 0, 0); background-color: #1f2234;
background-color: }
#3e355a; .skin-blue .main-header .logo {
} background-color: #1f2234;
color: #fff;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a { .skin-blue .sidebar-menu > li.header {
color: #4b646f;
background: #1f2234;
}
.skin-blue .wrapper,
.skin-blue .main-sidebar,
.skin-blue .left-side {
background: #1f2234;
}
background: none; .skin-blue .sidebar-menu > li > .treeview-menu {
} margin: 0 1px;
</style> background: #1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover,
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background: #3e355a;
border-left-color: #8964a9;
}
.skin-blue .main-header .logo:hover {
background: #3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color: #3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red,
.callout.callout-danger,
.alert-danger,
.alert-error,
.label-danger,
.modal-danger .modal-body {
background-color: #1f2234 !important;
}
.alert-danger,
.alert-error {
border-color: #fff;
border: 1px solid #fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover,
.skin-blue .main-header .navbar .nav > li > a:active,
.skin-blue .main-header .navbar .nav > li > a:focus,
.skin-blue .main-header .navbar .nav .open > a,
.skin-blue .main-header .navbar .nav .open > a:hover,
.skin-blue .main-header .navbar .nav .open > a:focus {
color: #f6f6f6;
background-color: #3e355a;
}
.bg-aqua,
.callout.callout-info,
.alert-info,
.label-info,
.modal-info .modal-body {
background-color: #3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n + 1) {
background-color: #333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover,
a:active,
a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color: #333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color: #333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color: #333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color: #3e355a;
}
.skin-blue .sidebar-menu > li > a:hover,
.skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head> </head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open"> <body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper"> <div class="wrapper">
@ -421,7 +400,6 @@ background-color:
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less--> <!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu"> <li class="dropdown messages-menu">
{% block messages %}{% endblock %} {% block messages %}{% endblock %}
</li> </li>
</ul> </ul>
@ -429,15 +407,13 @@ background-color:
</nav> </nav>
</header> </header>
<aside class="main-sidebar"> <aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less --> <!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;"> <section class="sidebar" style="height: auto;"></section>
</section>
<!-- /.sidebar --> <!-- /.sidebar -->
</aside> </aside>
<!-- Right side column. Contains the navbar and content of the page --> <!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper"> <div class="content-wrapper">
<!-- Content Header (Page header) --> <!-- Content Header (Page header) -->
<section class="content-header"> <section class="content-header">
@ -445,126 +421,147 @@ background-color:
LNBits Events LNBits Events
<small>Lightning powered tickets</small> <small>Lightning powered tickets</small>
</h1> </h1>
</section> </section>
<!-- Main content --> <!-- Main content -->
<section class="content"><br/><br/> <section class="content">
<center><h1 style="font-size:500%">{{ nme }}</h1></center> <br /><br />
<center><h2 style="width:55%;word-wrap: break-word;" >{{ descr }}</h2></center> <center><h1 style="font-size: 500%;">{{ nme }}</h1></center>
<div id="theform"> <center>
<br/><br/><br/> <h2 style="width: 55%; word-wrap: break-word;">{{ descr }}</h2>
<center> </center>
<div id="theform">
<form role="form"> <br /><br /><br />
<center>
<div class="form-group" style="width:300px;"> <form role="form">
<div class="form-group" style="width: 300px;">
<input id="Nam" type="text" class="form-control" placeholder="Name"></input> <input
<input id="Ema" type="text" class="form-control" placeholder="Email"></input> id="Nam"
</div> type="text"
<button onclick="submitforticket()" type="button" class="btn btn-info">Go to payment</button><p style="color:red;" id="error"></p> class="form-control"
</form> placeholder="Name"
</div> />
</center> <input
id="Ema"
<center><br/><br/> <div id="qrcode" style="width: 340px;"></div><br/><br/> type="text"
<div style="width:55%;word-wrap: break-word;" id="qrcodetxt"></div> <br/></center> class="form-control"
placeholder="Email"
</section><!-- /.content --> />
</div><!-- /.content-wrapper --> </div>
<button
onclick="submitforticket()"
type="button"
class="btn btn-info"
>
Go to payment
</button>
<p style="color: red;" id="error"></p>
</form>
</center>
</div>
<center>
<br /><br />
<div id="qrcode" style="width: 340px;"></div>
<br /><br />
<div
style="width: 55%; word-wrap: break-word;"
id="qrcodetxt"
></div>
<br />
</center>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</div> </div>
</body> </body>
<script> <script>
function postAjax(url, data, thekey, success) {
var params =
function postAjax(url, data, thekey, success) { typeof data == 'string'
var params = ? data
typeof data == 'string' : Object.keys(data)
? data .map(function (k) {
: Object.keys(data) return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
.map(function(k) { })
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) .join('&')
}) var xhr = window.XMLHttpRequest
.join('&') ? new XMLHttpRequest()
var xhr = window.XMLHttpRequest : new ActiveXObject('Microsoft.XMLHTTP')
? new XMLHttpRequest() xhr.open('POST', url)
: new ActiveXObject('Microsoft.XMLHTTP') xhr.onreadystatechange = function () {
xhr.open('POST', url) if (xhr.readyState > 3 && xhr.status == 200) {
xhr.onreadystatechange = function() { success(xhr.responseText)
if (xhr.readyState > 3 && xhr.status == 200) { }
success(xhr.responseText) }
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(params)
return xhr
} }
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(params)
return xhr
}
function getAjax(url, thekey, success) { function getAjax(url, thekey, success) {
var xhr = window.XMLHttpRequest var xhr = window.XMLHttpRequest
? new XMLHttpRequest() ? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP') : new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('GET', url, true) xhr.open('GET', url, true)
xhr.onreadystatechange = function() { xhr.onreadystatechange = function () {
if (xhr.readyState > 3 && xhr.status == 200) { if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText) success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
return xhr
}
function submitforticket(){
nam = document.getElementById('Nam').value
ema = document.getElementById('Ema').value
postAjax(
"{{ url_for('events.api_getticket') }}?ema=" + ema,
JSON.stringify({"unireg": "{{wave }}", "name": nam}),
"filla",
function(data) {
theinvoice = JSON.parse(data).pay_req
thehash = JSON.parse(data).payment_hash
new QRCode(document.getElementById('qrcode'), {
text: theinvoice,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
document.getElementById('theform').innerHTML = ""
document.getElementById("qrcode").style.backgroundColor = "white";
document.getElementById("qrcode").style.padding = "20px";
document.getElementById('qrcodetxt').innerHTML = theinvoice + "<br/><br/>"
var refreshId = setInterval(function(){
getAjax('/api/v1/invoice/' + thehash, "{{wave}}", function(datab) {
console.log(JSON.parse(datab).PAID)
if (JSON.parse(datab).PAID == 'TRUE') {
location.replace("{{ url_for('events.ticket') }}?hash="+thehash + "&unireg={{wave}}")
clearInterval(refreshId)
} }
})}, 3000); }
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
})
}
xhr.send()
return xhr
}
function submitforticket() {
nam = document.getElementById('Nam').value
ema = document.getElementById('Ema').value
postAjax(
"{{ url_for('events.api_getticket') }}?ema=" + ema,
JSON.stringify({unireg: '{{wave }}', name: nam}),
'filla',
function (data) {
theinvoice = JSON.parse(data).pay_req
thehash = JSON.parse(data).payment_hash
new QRCode(document.getElementById('qrcode'), {
text: theinvoice,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
document.getElementById('theform').innerHTML = ''
document.getElementById('qrcode').style.backgroundColor = 'white'
document.getElementById('qrcode').style.padding = '20px'
document.getElementById('qrcodetxt').innerHTML =
theinvoice + '<br/><br/>'
var refreshId = setInterval(function () {
getAjax('/api/v1/invoice/' + thehash, '{{wave}}', function (datab) {
console.log(JSON.parse(datab).PAID)
if (JSON.parse(datab).PAID == 'TRUE') {
location.replace(
"{{ url_for('events.ticket') }}?hash=" +
thehash +
'&unireg={{wave}}'
)
clearInterval(refreshId)
}
})
}, 3000)
}
)
}
</script> </script>
</html> </html>

815
lnbits/extensions/events/templates/events/index.html

@ -10,45 +10,43 @@
<li class="header"><b>Instant wallet, bookmark to save</b></li> <li class="header"><b>Instant wallet, bookmark to save</b></li>
<li></li> <li></li>
</ul> </ul>
{% endblock %} {% endblock %} {% block menuitems %}
<li class="treeview">
{% block menuitems %} <a href="#">
<li class="treeview"> <i class="fa fa-bitcoin"></i> <span>Wallets</span>
<a href="#"> <i class="fa fa-angle-left pull-right"></i>
<i class="fa fa-bitcoin"></i> <span>Wallets</span> </a>
<i class="fa fa-angle-left pull-right"></i> <ul class="treeview-menu">
</a> {% for w in user_wallets %}
<ul class="treeview-menu"> <li>
{% for w in user_wallets %} <a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"
<li> ><i class="fa fa-bolt"></i> {{ w.name }}</a
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"><i class="fa fa-bolt"></i> {{ w.name }}</a> >
</li> </li>
{% endfor %} {% endfor %}
<li><a onclick="sidebarmake()">Add a wallet +</a></li> <li><a onclick="sidebarmake()">Add a wallet +</a></li>
<div id="sidebarmake"></div> <div id="sidebarmake"></div>
</ul> </ul>
</li> </li>
<li class="active treeview"> <li class="active treeview">
<a href="#"> <a href="#">
<i class="fa fa-th"></i> <span>Extensions</span> <i class="fa fa-th"></i> <span>Extensions</span>
<i class="fa fa-angle-left pull-right"></i> <i class="fa fa-angle-left pull-right"></i>
</a> </a>
<ul class="treeview-menu"> <ul class="treeview-menu">
{% for extension in EXTENSIONS %} {% for extension in EXTENSIONS %} {% if extension.code in user_ext %}
{% if extension.code in user_ext %} <li>
<li> <a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"><i class="fa fa-plus"></i> {{ extension.name }}</a> ><i class="fa fa-plus"></i> {{ extension.name }}</a
</li> >
{% endif %} </li>
{% endfor %} {% endif %} {% endfor %}
<li> <li>
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li> <a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a>
</ul> </li>
</li> </ul>
{% endblock %} </li>
{% endblock %} {% block body %}
{% block body %}
<!-- Right side column. Contains the navbar and content of the page --> <!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper"> <div class="content-wrapper">
<!-- Content Header (Page header) --> <!-- Content Header (Page header) -->
@ -56,127 +54,150 @@
<h1> <h1>
Events Events
<small>bitcoin tickets</small> <small>bitcoin tickets</small>
</h1> </h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li> <li>
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a> <a href="{{ url_for('wallet') }}?usr={{ user }}"
><i class="fa fa-dashboard"></i> Home</a
>
</li> </li>
<li> <li>
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a> <a href="{{ url_for('core.extensions') }}?usr={{ user }}"
><li class="fa fa-dashboard">Extensions</li></a
>
</li> </li>
<li> <li>
<i class="active" class="fa fa-dashboard">Lightning tickets</i> <i class="active" class="fa fa-dashboard">Lightning tickets</i>
</li> </li>
</ol> </ol>
<br /><br /> <br /><br />
</section> </section>
<style> <style>
.datepicker-days{ .datepicker-days {
background-color: #1f2234;
background-color: #1f2234; }
} </style>
</style>
<!-- Main content --> <!-- Main content -->
<section class="content"> <section class="content">
<!-- Small boxes (Stat box) --> <!-- Small boxes (Stat box) -->
<div class="row"> <div class="row">
<div class="col-md-6">
<!-- general form elements -->
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title">Make a ticket wave</h3>
</div>
<!-- /.box-header -->
<div class="col-md-6"> <!-- form start -->
<!-- general form elements --> <form role="form">
<div class="box box-primary"> <div class="box-body">
<div class="form-group">
<label for="exampleInputEmail1">Ticket title</label>
<div class="box-header"> <input
<h3 class="box-title"> Make a ticket wave</h3> id="tit"
</div><!-- /.box-header --> type="text"
pattern="^[A-Za-z]+$"
class="form-control"
<!-- form start --> />
<form role="form">
<div class="box-body">
<div class="form-group">
<label for="exampleInputEmail1">Ticket title</label>
<input id="tit" type="text" pattern="^[A-Za-z]+$" class="form-control" >
</div>
<div class="form-group">
<label>Description of event</label>
<textarea id="descr" class="form-control" rows="2"></textarea>
</div>
<!-- select -->
<div class="form-group">
<label>Select a wallet</label>
<select id="wal" class="form-control">
<option></option>
{% for w in user_wallets %}
<option>{{w.name}}-{{w.id}}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="nooftickets">No. of tickets</label>
<input id="notickets" type="number" class="form-control" placeholder="10" max="86400"></input>
</div>
<div class="form-group">
<label>Close date:</label>
<div class="form-group">
<input type="text" class="form-control" id="datepicker"></input>
</div>
<div class="form-group">
<label for="prpertick">Price per ticket</label>
<input id="prtickets" type="number" class="form-control" placeholder="10"></input>
</div>
</div><!-- /.box-body -->
<div class="box-footer">
<button onclick="postev()" type="button" class="btn btn-info">Create Wave</button><p style="color:red;" id="error"></p>
</div>
</form>
</div></div></div>
<div class="col-md-6">
<!-- general form elements -->
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title">Select a link</h3>
</div><!-- /.box-header -->
<form role="form">
<div class="box-body">
<div class="form-group">
<select class="form-control" id="waveselect" onchange="drawwithdraw()">
<option value="none" selected>
Select an Option
</option>
{% for w in user_ev %}
<option id="{{w.uni}}" value="{{w.tit}}-{{w.unireg}}-{{w.uni}}">{{w.tit}}-{{w.unireg}}-{{w.uni}}</option>
{% endfor %}
</select>
</div>
<center> <br/><div id="qrcode" style="width:340px" ></div><br/><div style="width:75%;word-wrap: break-word;" id="qrcodetxt" ></div></center>
</div>
</form>
</div><!-- /.box -->
</div> </div>
<div class="form-group">
<label>Description of event</label>
<textarea id="descr" class="form-control" rows="2"></textarea>
</div>
<!-- select -->
<div class="form-group">
<label>Select a wallet</label>
<select id="wal" class="form-control">
<option></option>
{% for w in user_wallets %}
<option>{{w.name}}-{{w.id}}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="nooftickets">No. of tickets</label>
<input
id="notickets"
type="number"
class="form-control"
placeholder="10"
max="86400"
/>
</div>
</div>
<div class="form-group">
<label>Close date:</label>
<div class="form-group">
<input type="text" class="form-control" id="datepicker" />
</div>
<div class="form-group">
<label for="prpertick">Price per ticket</label>
<input
id="prtickets"
type="number"
class="form-control"
placeholder="10"
/>
</div>
</div>
<div class="box-footer">
<button onclick="postev()" type="button" class="btn btn-info">
Create Wave
</button>
<p style="color: red;" id="error"></p>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<!-- general form elements -->
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title">Select a link</h3>
</div>
<!-- /.box-header -->
<form role="form">
<div class="box-body">
<div class="form-group">
<select
class="form-control"
id="waveselect"
onchange="drawwithdraw()"
>
<option value="none" selected>
Select an Option
</option>
{% for w in user_ev %}
<option id="{{w.uni}}" value="{{w.tit}}-{{w.unireg}}-{{w.uni}}"
>{{w.tit}}-{{w.unireg}}-{{w.uni}}</option
>
{% endfor %}
</select>
</div>
<center>
<br />
<div id="qrcode" style="width: 340px;"></div>
<br />
<div
style="width: 75%; word-wrap: break-word;"
id="qrcodetxt"
></div>
</center>
</div>
</form>
</div>
<!-- /.box -->
</div> </div>
<div class="row"> <div class="row">
@ -187,16 +208,19 @@
</div> </div>
<!-- /.box-header --> <!-- /.box-header -->
<div class="box-body no-padding"> <div class="box-body no-padding">
<table id="pagnation" class="table table-bswearing anchorordered table-striped"> <table
id="pagnation"
class="table table-bswearing anchorordered table-striped"
>
<tr> <tr>
<th>Title</th> <th>Title</th>
<th style="width:15%">Amt</th> <th style="width: 15%;">Amt</th>
<th style="width:15%">Sold</th> <th style="width: 15%;">Sold</th>
<th style="width:15%">Closing</th> <th style="width: 15%;">Closing</th>
<th style="width:15%">Price</th> <th style="width: 15%;">Price</th>
<th style="width:15%">Wallet</th> <th style="width: 15%;">Wallet</th>
<th style="width:10%">Edit</th> <th style="width: 10%;">Edit</th>
<th style="width:10%">Del</th> <th style="width: 10%;">Del</th>
</tr> </tr>
<tbody id="ticketwaves"></tbody> <tbody id="ticketwaves"></tbody>
</table> </table>
@ -207,274 +231,271 @@
</div> </div>
</div> </div>
<div id="editlink"></div>
<div id="editlink"></div>
<!-- /.content --> <!-- /.content -->
</section> </section>
<script> <script>
//Date picker //Date picker
$('#datepicker').datepicker({ $('#datepicker').datepicker({
autoclose: true autoclose: true
}) })
window.user = {{ user | tojson | safe }}
window.user_wallets = {{ user_wallets | tojson | safe }}
window.user_ext = {{ user_ext | tojson | safe }}
window.user_ev = {{ user_ev | tojson | safe }}
const user_ev = window.user_ev
console.log(user_ev)
function drawChart(user_ev) {
var transactionsHTML = ''
for (var i = 0; i < user_ev.length; i++) {
var ev = user_ev[i]
transactionsHTML =
"<tr><td style='width: 50%'>" +
ev.tit +
'</td><td>' +
ev.notickets +
'</td><td>' +
ev.sold +
'</td><td>' +
ev.cldate +
'</td><td>' +
ev.prtick +
'</td><td>' +
"<a href='{{ url_for('wallet') }}?usr="+ ev.usr +"'>" + ev.wal.substring(0, 4) + "...</a>" +
'</td><td>' +
"<i onclick='editlink("+ i +")'' class='fa fa-edit'></i>" +
'</td><td>' +
"<b><a style='color:red;' href='" + "{{ url_for('events.index') }}?del=" + ev.uni + "&usr=" + ev.usr +"'>" + "<i class='fa fa-trash'></i>" + "</a></b>" +
'</td></tr>' +
transactionsHTML
document.getElementById('ticketwaves').innerHTML = transactionsHTML
}
}
if (user_ev.length) {
drawChart(user_ev)
}
function postev(){
wal = document.getElementById('wal').value
tit = document.getElementById('tit').value
cldate = document.getElementById('datepicker').value
notickets = document.getElementById('notickets').value
prtickets = document.getElementById('prtickets').value
descr = document.getElementById('descr').value
if (tit == "") {
document.getElementById("error").innerHTML = "Only use letters in title"
return amt
}
if (wal == "") {
document.getElementById("error").innerHTML = "No wallet selected"
return amt
}
if (cldate == "") {
document.getElementById("error").innerHTML = "No date selected"
return amt
}
if (isNaN(notickets) || notickets < 1) {
document.getElementById("error").innerHTML = "Must be more than 1"
return amt
}
if (isNaN(prtickets) || prtickets < 10) {
document.getElementById("error").innerHTML = "Must be higher 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": notickets,"cldate": cldate, "prtickets": prtickets, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
window.user = {{ user | tojson | safe }} }
window.user_wallets = {{ user_wallets | tojson | safe }}
window.user_ext = {{ user_ext | tojson | safe }}
window.user_ev = {{ user_ev | tojson | safe }}
const user_ev = window.user_ev
console.log(user_ev)
function editlink(evnum){
evdetails = user_ev[evnum]
console.log(evdetails.descr)
wallpick = ""
checkbox = ""
if (evdetails.uniq == 1){
checkbox = "checked"}
document.getElementById('editlink').innerHTML = "<div class='row'>"+
"<div class='col-md-6'>"+
" <!-- general form elements -->"+
"<div class='box box-primary' style='min-height: 300px;'>"+
"<div class='box-header'>"+
"<h3 class='box-title'> Edit: <i id='unid'>" + evdetails.tit + "-" + evdetails.uni + "-" + evdetails.unireg + "</i> </h3>"+
"<div class='box-tools pull-right'>" +
"<button class='btn btn-box-tool' data-widget='remove'><i class='fa fa-times'></i></button>" +
"</div>" +
" </div><!-- /.box-header -->"+
" <!-- form start -->"+
"<form role='form'>"+
"<div class='box-body'>"+
"<div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Link title</label>"+
"<input id='edittit' type='text' class='form-control' value='"+
evdetails.tit +
"'> </div>"+
" </div>"+
"<div class='col-sm-1 col-md-8'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Description of event</label>"+
"<textarea id='editdescr' type='textarea' rows='1' class='form-control'>"+evdetails.descr+"</textarea> </div>"+
" </div>"+
" <div class='col-sm-4 col-md-4'>"+
" <!-- select -->"+
" <div class='form-group'>"+
" <label>Select a wallet</label>"+
"<select id='editwal' class='form-control'>"+
" <option>" + evdetails.walnme + "-" + evdetails.wal + "</option>"+
" {% for w in user_wallets %}"+
" <option>{{w.name}}-{{w.id}}</option>"+
" {% endfor %}"+
" </select>"+
" </div>"+
" </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
" <label for='exampleInputPassword1'>No of tickets:</label>"+
" <input id='editnooftickets' type='number' class='form-control' placeholder='0' max='86400' value='"+
evdetails.notickets +
"'>"+
"</div> </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Price per ticket:</label>"+
" <input id='editprtick' type='number' class='form-control' placeholder='1' value='"+
evdetails.prtick +
"'>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
" <div class='input-group date'>"+
" <label for='exampleInputEmail1'>Close date:</label>"+
" <input id='datepicker2' type='text' class='form-control' placeholder='1' value='"+
evdetails.cldate +
"'>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
"</div><!-- /.box-body -->"+
" </div><br/>"+
" <div class='box-footer'>"+
" <div class='col-sm-3 col-md-4'>"+
"<button onclick='editlinkcont()' type='button' style='margin: 24px;' class='btn btn-info'>Edit link(s)</button>"+
"</div>"+
"<p style='color:red;' id='error2'>.</p>"+
" </div></form></div><!-- /.box --></div></div>"
function drawChart(user_ev) {
var transactionsHTML = ''
for (var i = 0; i < user_ev.length; i++) { }
var ev = user_ev[i]
transactionsHTML = //Date picker
"<tr><td style='width: 50%'>" + $('#datepicker2').datepicker({
ev.tit + autoclose: true
'</td><td>' + })
ev.notickets +
'</td><td>' + function editlinkcont(){
ev.sold +
'</td><td>' + unid = document.getElementById('unid').innerHTML
ev.cldate + wal = document.getElementById('editwal').value
'</td><td>' + tit = document.getElementById('edittit').value
ev.prtick + nooftickets = document.getElementById('editnooftickets').value
'</td><td>' + prtick = document.getElementById('editprtick').value
"<a href='{{ url_for('wallet') }}?usr="+ ev.usr +"'>" + ev.wal.substring(0, 4) + "...</a>" + cldate = document.getElementById('datepicker2').value
'</td><td>' + descr = document.getElementById('editdescr').value
"<i onclick='editlink("+ i +")'' class='fa fa-edit'></i>" + uni = unid.split("-")[1]
'</td><td>' +
"<b><a style='color:red;' href='" + "{{ url_for('events.index') }}?del=" + ev.uni + "&usr=" + ev.usr +"'>" + "<i class='fa fa-trash'></i>" + "</a></b>" + if (tit == "") {
'</td></tr>' + document.getElementById("error2").innerHTML = "Only use letters in title"
transactionsHTML return amt
document.getElementById('ticketwaves').innerHTML = transactionsHTML }
if (wal == "") {
document.getElementById("error2").innerHTML = "No wallet selected"
return amt
}
if (isNaN(nooftickets) || nooftickets < 1 || nooftickets > 1000000) {
document.getElementById("error2").innerHTML = "No. of tickets must be between 1 - 1000000"
return amt
}
if (isNaN(prtick) || prtick < 10 ) {
document.getElementById("error2").innerHTML = "Ticket pricket must be higher than 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": nooftickets,"cldate": cldate, "prtickets": prtick, "id": unid, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
} }
}
if (user_ev.length) {
drawChart(user_ev)
}
function postev(){
wal = document.getElementById('wal').value
tit = document.getElementById('tit').value
cldate = document.getElementById('datepicker').value
notickets = document.getElementById('notickets').value
prtickets = document.getElementById('prtickets').value
descr = document.getElementById('descr').value
if (tit == "") {
document.getElementById("error").innerHTML = "Only use letters in title"
return amt
}
if (wal == "") {
document.getElementById("error").innerHTML = "No wallet selected"
return amt
}
if (cldate == "") {
document.getElementById("error").innerHTML = "No date selected"
return amt
}
if (isNaN(notickets) || notickets < 1) {
document.getElementById("error").innerHTML = "Must be more than 1"
return amt
}
if (isNaN(prtickets) || prtickets < 10) {
document.getElementById("error").innerHTML = "Must be higher 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": notickets,"cldate": cldate, "prtickets": prtickets, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
}
function editlink(evnum){
evdetails = user_ev[evnum]
console.log(evdetails.descr)
wallpick = ""
checkbox = ""
if (evdetails.uniq == 1){
checkbox = "checked"}
document.getElementById('editlink').innerHTML = "<div class='row'>"+
"<div class='col-md-6'>"+
" <!-- general form elements -->"+
"<div class='box box-primary' style='min-height: 300px;'>"+
"<div class='box-header'>"+
"<h3 class='box-title'> Edit: <i id='unid'>" + evdetails.tit + "-" + evdetails.uni + "-" + evdetails.unireg + "</i> </h3>"+
"<div class='box-tools pull-right'>" +
"<button class='btn btn-box-tool' data-widget='remove'><i class='fa fa-times'></i></button>" +
"</div>" +
" </div><!-- /.box-header -->"+
" <!-- form start -->"+
"<form role='form'>"+
"<div class='box-body'>"+
"<div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Link title</label>"+
"<input id='edittit' type='text' class='form-control' value='"+
evdetails.tit +
"'></input> </div>"+
" </div>"+
"<div class='col-sm-1 col-md-8'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Description of event</label>"+
"<textarea id='editdescr' type='textarea' rows='1' class='form-control'>"+evdetails.descr+"</textarea> </div>"+
" </div>"+
" <div class='col-sm-4 col-md-4'>"+
" <!-- select -->"+
" <div class='form-group'>"+
" <label>Select a wallet</label>"+
"<select id='editwal' class='form-control'>"+
" <option>" + evdetails.walnme + "-" + evdetails.wal + "</option>"+
" {% for w in user_wallets %}"+
" <option>{{w.name}}-{{w.id}}</option>"+
" {% endfor %}"+
" </select>"+
" </div>"+
" </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
" <label for='exampleInputPassword1'>No of tickets:</label>"+
" <input id='editnooftickets' type='number' class='form-control' placeholder='0' max='86400' value='"+
evdetails.notickets +
"'></input>"+
"</div> </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Price per ticket:</label>"+
" <input id='editprtick' type='number' class='form-control' placeholder='1' value='"+
evdetails.prtick +
"'></input>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
" <div class='input-group date'>"+
" <label for='exampleInputEmail1'>Close date:</label>"+
" <input id='datepicker2' type='text' class='form-control' placeholder='1' value='"+
evdetails.cldate +
"'></input>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
"</div><!-- /.box-body -->"+
" </div><br/>"+
" <div class='box-footer'>"+
" <div class='col-sm-3 col-md-4'>"+
"<button onclick='editlinkcont()' type='button' style='margin: 24px;' class='btn btn-info'>Edit link(s)</button>"+
"</div>"+
"<p style='color:red;' id='error2'>.</p>"+
" </div></form></div><!-- /.box --></div></div>"
}
//Date picker
$('#datepicker2').datepicker({
autoclose: true
})
function editlinkcont(){
unid = document.getElementById('unid').innerHTML
wal = document.getElementById('editwal').value
tit = document.getElementById('edittit').value
nooftickets = document.getElementById('editnooftickets').value
prtick = document.getElementById('editprtick').value
cldate = document.getElementById('datepicker2').value
descr = document.getElementById('editdescr').value
uni = unid.split("-")[1]
if (tit == "") {
document.getElementById("error2").innerHTML = "Only use letters in title"
return amt
}
if (wal == "") {
document.getElementById("error2").innerHTML = "No wallet selected"
return amt
}
if (isNaN(nooftickets) || nooftickets < 1 || nooftickets > 1000000) {
document.getElementById("error2").innerHTML = "No. of tickets must be between 1 - 1000000"
return amt
}
if (isNaN(prtick) || prtick < 10 ) {
document.getElementById("error2").innerHTML = "Ticket pricket must be higher than 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": nooftickets,"cldate": cldate, "prtickets": prtick, "id": unid, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
}
//draws withdraw QR code
function drawwithdraw() {
document.getElementById("qrcode").innerHTML = "";
walname = document.getElementById("waveselect").value
thewave = walname.split("-");
console.log(window.location.hostname + "-" + thewave[1])
toencode = "/events/wave/" + thewave[1]
toreg = "/events/registration/" + thewave[2]
new QRCode(document.getElementById('qrcode'), {
text: toencode,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
//draws withdraw QR code
document.getElementById('qrcodetxt').innerHTML = "<a href='" + toencode + "'><h2>Payment link</a> , " + function drawwithdraw() {
"<a href='" + toreg + "'>Registration</h2></a>"
document.getElementById("qrcode").style.backgroundColor = "white"; document.getElementById("qrcode").innerHTML = "";
document.getElementById("qrcode").style.padding = "20px"; walname = document.getElementById("waveselect").value
} thewave = walname.split("-");
console.log(window.location.hostname + "-" + thewave[1])
toencode = "/events/wave/" + thewave[1]
toreg = "/events/registration/" + thewave[2]
new QRCode(document.getElementById('qrcode'), {
text: toencode,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
document.getElementById('qrcodetxt').innerHTML = "<a href='" + toencode + "'><h2>Payment link</a> , " +
"<a href='" + toreg + "'>Registration</h2></a>"
document.getElementById("qrcode").style.backgroundColor = "white";
document.getElementById("qrcode").style.padding = "20px";
}
</script> </script>
</div> </div>
{% endblock %} {% endblock %}

790
lnbits/extensions/events/templates/events/registration.html

@ -1,4 +1,4 @@
<!-- @format --> <!-- @format -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -202,204 +202,183 @@
type="text/javascript" type="text/javascript"
></script> ></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background: #1f2234;
}
<style> body {
color: #fff;
}
//GOOFY CSS HACK TO GO DARK .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background: #1f2234;
border-left-color: #8964a9;
}
.skin-blue .wrapper { .skin-blue .main-header .navbar {
background: background-color: #2e507d;
#1f2234; }
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed { .content-wrapper,
background: none; .right-side {
background-color: rgba(0, 0, 0, 0); background-color: #1f2234;
background-color: }
#3e355a; .skin-blue .main-header .logo {
} background-color: #1f2234;
color: #fff;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a { .skin-blue .sidebar-menu > li.header {
color: #4b646f;
background: #1f2234;
}
.skin-blue .wrapper,
.skin-blue .main-sidebar,
.skin-blue .left-side {
background: #1f2234;
}
background: none; .skin-blue .sidebar-menu > li > .treeview-menu {
} margin: 0 1px;
</style> background: #1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover,
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background: #3e355a;
border-left-color: #8964a9;
}
.skin-blue .main-header .logo:hover {
background: #3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color: #3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red,
.callout.callout-danger,
.alert-danger,
.alert-error,
.label-danger,
.modal-danger .modal-body {
background-color: #1f2234 !important;
}
.alert-danger,
.alert-error {
border-color: #fff;
border: 1px solid #fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover,
.skin-blue .main-header .navbar .nav > li > a:active,
.skin-blue .main-header .navbar .nav > li > a:focus,
.skin-blue .main-header .navbar .nav .open > a,
.skin-blue .main-header .navbar .nav .open > a:hover,
.skin-blue .main-header .navbar .nav .open > a:focus {
color: #f6f6f6;
background-color: #3e355a;
}
.bg-aqua,
.callout.callout-info,
.alert-info,
.label-info,
.modal-info .modal-body {
background-color: #3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n + 1) {
background-color: #333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover,
a:active,
a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color: #333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color: #333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color: #333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color: #3e355a;
}
.skin-blue .sidebar-menu > li > a:hover,
.skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head> </head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open"> <body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper"> <div class="wrapper">
@ -421,7 +400,6 @@ background-color:
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less--> <!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu"> <li class="dropdown messages-menu">
{% block messages %}{% endblock %} {% block messages %}{% endblock %}
</li> </li>
</ul> </ul>
@ -429,17 +407,15 @@ background-color:
</nav> </nav>
</header> </header>
<aside class="main-sidebar"> <aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less --> <!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;"> <section class="sidebar" style="height: auto;">
<!-- Sidebar user panel --> <!-- Sidebar user panel -->
</section> </section>
<!-- /.sidebar --> <!-- /.sidebar -->
</aside> </aside>
<!-- Right side column. Contains the navbar and content of the page --> <!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper"> <div class="content-wrapper">
<!-- Content Header (Page header) --> <!-- Content Header (Page header) -->
<section class="content-header"> <section class="content-header">
@ -447,244 +423,248 @@ background-color:
LNBits Events LNBits Events
<small>Lightning powered tickets</small> <small>Lightning powered tickets</small>
</h1> </h1>
</section> </section>
<!-- Main content --> <!-- Main content -->
<section class="content"><br/><br/> <section class="content">
<center><h1 style="font-size:500%">{{ user_ev[0][6] }}</h1></center> <br /><br />
<center><h1 style="font-size: 500%;">{{ user_ev[0][6] }}</h1></center>
<br/><br/><br/> <br /><br /><br />
<div class='modal fade sends' tabindex='-1' role='dialog' aria-labelledby='myLargeModalLabel' aria-hidden='true'> <div
<div class='modal-dialog' > class="modal fade sends"
<div id='scantickets' style='padding: 0 10px 0 10px;'> tabindex="-1"
</div></div></div> role="dialog"
aria-labelledby="myLargeModalLabel"
aria-hidden="true"
<center> >
<button <div class="modal-dialog">
onclick="scanQRsend()" <div id="scantickets" style="padding: 0 10px 0 10px;"></div>
class="btn btn-block btn-primary btn-lg" </div>
data-toggle="modal"
data-target=".sends"
style="width:300px;"
>
Scan ticket
</button>
</center>
<div id="scantickets"></div>
<br/><br/><br/>
<div id="qrcodetxt"></div> <br/></center>
<br/><br/><br/>
<center>
<div class="row" style="width:80%;margin-top:80px">
<style>
.ema, button:focus .txt {
display: none;
}
button:focus .ema {
display: block;
}
</style>
<div class="box">
<div class="box-header">
<h3 class="box-title">Attendees<b id="withdraws"></b></h3>
</div>
<!-- /.box-header -->
<div class="box-body no-padding">
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
<tr>
<th style="width:20%">Name</th>
<th style="width:20%">Email</th>
<th style="width:50%">Ticket</th>
<th style="width:10%">Registered</th>
</tr>
<tbody id="ticketwaves"></tbody>
</table>
</div> </div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</center>
</section><!-- /.content --> <center>
</div><!-- /.content-wrapper --> <button
onclick="scanQRsend()"
class="btn btn-block btn-primary btn-lg"
data-toggle="modal"
data-target=".sends"
style="width: 300px;"
>
Scan ticket
</button>
</center>
<div id="scantickets"></div>
<br /><br /><br />
<div id="qrcodetxt"></div>
<br />
<br /><br /><br />
<center>
<div class="row" style="width: 80%; margin-top: 80px;">
<style>
.ema,
button:focus .txt {
display: none;
}
button:focus .ema {
display: block;
}
</style>
<div class="box">
<div class="box-header">
<h3 class="box-title">Attendees<b id="withdraws"></b></h3>
</div>
<!-- /.box-header -->
<div class="box-body no-padding">
<table
id="pagnation"
class="table table-bswearing anchorordered table-striped"
>
<tr>
<th style="width: 20%;">Name</th>
<th style="width: 20%;">Email</th>
<th style="width: 50%;">Ticket</th>
<th style="width: 10%;">Registered</th>
</tr>
<tbody id="ticketwaves"></tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</center>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</div> </div>
</body> </body>
<script> <script>
window.user_ev = {{ user_ev | tojson | safe }} window.user_ev = {{ user_ev | tojson | safe }}
window.user_ev_sold = {{ user_ev_sold | tojson | safe }} window.user_ev_sold = {{ user_ev_sold | tojson | safe }}
console.log(user_ev) console.log(user_ev)
console.log(user_ev_sold) console.log(user_ev_sold)
function drawChart(user_ev_sold) { function drawChart(user_ev_sold) {
var transactionsHTML = '' var transactionsHTML = ''
for (var i = 0; i < user_ev_sold.length; i++) { for (var i = 0; i < user_ev_sold.length; i++) {
var ev = user_ev_sold[i] var ev = user_ev_sold[i]
transactionsHTML = transactionsHTML =
"<tr><td>" + "<tr><td>" +
ev.name + ev.name +
'</td><td>' + '</td><td>' +
'<button style="background-color: #333646;padding: 0;border: none;background: none;" class="lost"><span class="txt">xxxxxx</span>' + '<button style="background-color: #333646;padding: 0;border: none;background: none;" class="lost"><span class="txt">xxxxxx</span>' +
'<span class="ema">' + ev.email + '</span></button>'+ '<span class="ema">' + ev.email + '</span></button>'+
'</td><td>' + '</td><td>' +
ev.hash + ev.hash +
'</td><td>' + '</td><td>' +
ev.reg + ev.reg +
'</td></tr>' + '</td></tr>' +
transactionsHTML transactionsHTML
document.getElementById('ticketwaves').innerHTML = transactionsHTML document.getElementById('ticketwaves').innerHTML = transactionsHTML
} }
} }
if (user_ev_sold.length) { if (user_ev_sold.length) {
drawChart(user_ev_sold) drawChart(user_ev_sold)
} }
function postAjax(url, data, thekey, success) { function postAjax(url, data, thekey, success) {
var params = var params =
typeof data == 'string' typeof data == 'string'
? data ? data
: Object.keys(data) : Object.keys(data)
.map(function(k) { .map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
}) })
.join('&') .join('&')
var xhr = window.XMLHttpRequest var xhr = window.XMLHttpRequest
? new XMLHttpRequest() ? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP') : new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('POST', url) xhr.open('POST', url)
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) { if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText) success(xhr.responseText)
} }
} }
xhr.setRequestHeader('X-Api-Key', thekey) xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(params) xhr.send(params)
return xhr return xhr
}
function getAjax(url, thekey, success) {
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
} }
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
return xhr
}
function scanQRsend() {
document.getElementById('scantickets').innerHTML =
"<div class='modal-content'>"+
"<br/><div id='registered'><div id='loadingMessage'>🎥 Unable to access video stream (please make sure you have a webcam enabled)</div>" +
"<canvas id='canvas' hidden></canvas><div id='output' hidden><div id='outputMessage'></div>" +
"<br/><span id='outputData'></span></div></div><div class='modal-footer'>"+
"<button type='submit' class='btn btn-primary' onclick='cancelsend()'>Cancel</button><br/><br/></div>"
var video = document.createElement('video')
var canvasElement = document.getElementById('canvas')
var canvas = canvasElement.getContext('2d')
var loadingMessage = document.getElementById('loadingMessage')
var outputContainer = document.getElementById('output')
var outputMessage = document.getElementById('outputMessage')
var outputData = document.getElementById('outputData')
// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices
.getUserMedia({video: {facingMode: 'environment'}})
.then(function(stream) {
video.srcObject = stream
video.setAttribute('playsinline', true) // required to tell iOS safari we don't want fullscreen
video.play()
requestAnimationFrame(tick)
})
function tick() {
loadingMessage.innerText = '⌛ Loading video...'
if (video.readyState === video.HAVE_ENOUGH_DATA) {
loadingMessage.hidden = true
canvasElement.hidden = false
outputContainer.hidden = false
canvasElement.height = video.videoHeight
canvasElement.width = video.videoWidth
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height)
var imageData = canvas.getImageData(
0,
0,
canvasElement.width,
canvasElement.height
)
var code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert'
})
if (code) {
thehash = code.data
document.getElementById('registered').innerHTML = "<h1 style='color:green;font-size:300%;'>Registered!</h1>"
outputMessage.hidden = true
outputData.parentElement.hidden = false
outputData.innerText = JSON.stringify(code.data)
getAjax("{{ url_for('events.api_checkticket') }}?thehash=" + thehash, "filla", function(datab) {
if (JSON.parse(datab).status == 'TRUE') {
location.reload()
}
})
} else { function getAjax(url, thekey, success) {
outputMessage.hidden = false var xhr = window.XMLHttpRequest
outputData.parentElement.hidden = true ? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
} }
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
return xhr
} }
requestAnimationFrame(tick)
}
}
function cancelsend() { function scanQRsend() {
location.reload(); document.getElementById('scantickets').innerHTML =
} "<div class='modal-content'>"+
"<br/><div id='registered'><div id='loadingMessage'>🎥 Unable to access video stream (please make sure you have a webcam enabled)</div>" +
"<canvas id='canvas' hidden></canvas><div id='output' hidden><div id='outputMessage'></div>" +
"<br/><span id='outputData'></span></div></div><div class='modal-footer'>"+
"<button type='submit' class='btn btn-primary' onclick='cancelsend()'>Cancel</button><br/><br/></div>"
var video = document.createElement('video')
var canvasElement = document.getElementById('canvas')
var canvas = canvasElement.getContext('2d')
var loadingMessage = document.getElementById('loadingMessage')
var outputContainer = document.getElementById('output')
var outputMessage = document.getElementById('outputMessage')
var outputData = document.getElementById('outputData')
// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices
.getUserMedia({video: {facingMode: 'environment'}})
.then(function(stream) {
video.srcObject = stream
video.setAttribute('playsinline', true) // required to tell iOS safari we don't want fullscreen
video.play()
requestAnimationFrame(tick)
})
function tick() {
loadingMessage.innerText = '⌛ Loading video...'
if (video.readyState === video.HAVE_ENOUGH_DATA) {
loadingMessage.hidden = true
canvasElement.hidden = false
outputContainer.hidden = false
canvasElement.height = video.videoHeight
canvasElement.width = video.videoWidth
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height)
var imageData = canvas.getImageData(
0,
0,
canvasElement.width,
canvasElement.height
)
var code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert'
})
if (code) {
thehash = code.data
document.getElementById('registered').innerHTML = "<h1 style='color:green;font-size:300%;'>Registered!</h1>"
outputMessage.hidden = true
outputData.parentElement.hidden = false
outputData.innerText = JSON.stringify(code.data)
getAjax("{{ url_for('events.api_checkticket') }}?thehash=" + thehash, "filla", function(datab) {
if (JSON.parse(datab).status == 'TRUE') {
location.reload()
}
})
} else {
outputMessage.hidden = false
outputData.parentElement.hidden = true
}
}
requestAnimationFrame(tick)
}
}
function getUrlVars() { function cancelsend() {
var vars = {}; location.reload();
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { }
vars[key] = value;
});
return vars;
}
var name = getUrlVars()["name"];
var thehash = getUrlVars()["thehash"];
console.log(thehash)
if(thehash != null){
document.getElementById('qrcodetxt').innerHTML = "<center><h1>" + name + " is registered!</h1></center>"
}
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = value;
});
return vars;
}
var name = getUrlVars()["name"];
var thehash = getUrlVars()["thehash"];
console.log(thehash)
if(thehash != null){
document.getElementById('qrcodetxt').innerHTML = "<center><h1>" + name + " is registered!</h1></center>"
}
</script> </script>
</html> </html>

415
lnbits/extensions/events/templates/events/ticket.html

@ -1,4 +1,4 @@
<!-- @format --> <!-- @format -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -202,204 +202,183 @@
type="text/javascript" type="text/javascript"
></script> ></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background: #1f2234;
}
<style> body {
color: #fff;
}
//GOOFY CSS HACK TO GO DARK .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background: #1f2234;
border-left-color: #8964a9;
}
.skin-blue .wrapper { .skin-blue .main-header .navbar {
background: background-color: #2e507d;
#1f2234; }
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed { .content-wrapper,
background: none; .right-side {
background-color: rgba(0, 0, 0, 0); background-color: #1f2234;
background-color: }
#3e355a; .skin-blue .main-header .logo {
} background-color: #1f2234;
color: #fff;
}
.skin-blue .sidebar-menu > li.header {
color: #4b646f;
background: #1f2234;
}
.skin-blue .wrapper,
.skin-blue .main-sidebar,
.skin-blue .left-side {
background: #1f2234;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a { .skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background: #1f2234;
}
background: none; .skin-blue .sidebar-menu > li > a {
} border-left: 3px solid transparent;
</style> margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover,
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background: #3e355a;
border-left-color: #8964a9;
}
.skin-blue .main-header .logo:hover {
background: #3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color: #3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red,
.callout.callout-danger,
.alert-danger,
.alert-error,
.label-danger,
.modal-danger .modal-body {
background-color: #1f2234 !important;
}
.alert-danger,
.alert-error {
border-color: #fff;
border: 1px solid #fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover,
.skin-blue .main-header .navbar .nav > li > a:active,
.skin-blue .main-header .navbar .nav > li > a:focus,
.skin-blue .main-header .navbar .nav .open > a,
.skin-blue .main-header .navbar .nav .open > a:hover,
.skin-blue .main-header .navbar .nav .open > a:focus {
color: #f6f6f6;
background-color: #3e355a;
}
.bg-aqua,
.callout.callout-info,
.alert-info,
.label-info,
.modal-info .modal-body {
background-color: #3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n + 1) {
background-color: #333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover,
a:active,
a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color: #333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color: #333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color: #333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color: #3e355a;
}
.skin-blue .sidebar-menu > li > a:hover,
.skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head> </head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open"> <body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper"> <div class="wrapper">
@ -421,7 +400,6 @@ background-color:
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less--> <!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu"> <li class="dropdown messages-menu">
{% block messages %}{% endblock %} {% block messages %}{% endblock %}
</li> </li>
</ul> </ul>
@ -429,15 +407,13 @@ background-color:
</nav> </nav>
</header> </header>
<aside class="main-sidebar"> <aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less --> <!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;"> <section class="sidebar" style="height: auto;"></section>
</section>
<!-- /.sidebar --> <!-- /.sidebar -->
</aside> </aside>
<!-- Right side column. Contains the navbar and content of the page --> <!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper"> <div class="content-wrapper">
<!-- Content Header (Page header) --> <!-- Content Header (Page header) -->
<section class="content-header"> <section class="content-header">
@ -445,30 +421,37 @@ background-color:
LNBits Events LNBits Events
<small>Lightning powered tickets</small> <small>Lightning powered tickets</small>
</h1> </h1>
</section> </section>
<!-- Main content --> <!-- Main content -->
<section class="content"><br/><br/> <section class="content">
<br /><br />
<center><h2 style="width:70%;font-size:400%" >Bookmark/Screenshot this page. <br/>It is your ticket!</h2></center>
<center> <div style="width:340px;background-color:white;padding:20px"id="qrcode"></div></center> <center>
<h2 style="width: 70%; font-size: 400%;">
Bookmark/Screenshot this page. <br />It is your ticket!
</section><!-- /.content --> </h2>
</div><!-- /.content-wrapper --> </center>
<center>
<div
style="width: 340px; background-color: white; padding: 20px;"
id="qrcode"
></div>
</center>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</div> </div>
</body> </body>
<script> <script>
new QRCode(document.getElementById('qrcode'), { new QRCode(document.getElementById('qrcode'), {
text: "{{ticket}}", text: '{{ticket}}',
width: 300, width: 300,
height: 300, height: 300,
colorDark: '#000000', colorDark: '#000000',
colorLight: '#ffffff', colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M correctLevel: QRCode.CorrectLevel.M
}) })
</script> </script>
</html> </html>

107
lnbits/extensions/example/templates/example/index.html

@ -1,54 +1,57 @@
{% extends "base.html" %} {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% from "macros.jinja" import window_vars with context %} <q-card>
<q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5>
{% block page %} <q-list>
<q-card> <q-item
<q-card-section> v-for="tool in tools"
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5> :key="tool.name"
<q-list> tag="a"
<q-item v-for="tool in tools" :key="tool.name" tag="a" :href="tool.url" target="_blank"> :href="tool.url"
{% raw %} <!-- with raw Flask won't try to interpret the Vue moustaches --> target="_blank"
<q-item-section> >
<q-item-label>{{ tool.name }}</q-item-label> {% raw %}
<q-item-label caption>{{ tool.language }}</q-item-label> <!-- with raw Flask won't try to interpret the Vue moustaches -->
</q-item-section> <q-item-section>
{% endraw %} <q-item-label>{{ tool.name }}</q-item-label>
</q-item> <q-item-label caption>{{ tool.language }}</q-item-label>
</q-list> </q-item-section>
<q-separator class="q-my-lg"></q-separator> {% endraw %}
<p>A magical "g" is always available, with info about the user, wallets and extensions:</p> </q-item>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code> </q-list>
</q-card-section> <q-separator class="q-my-lg"></q-separator>
</q-card> <p>
{% endblock %} A magical "g" is always available, with info about the user, wallets and
extensions:
{% block scripts %} </p>
{{ window_vars(user) }} <code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
<script> </q-card-section>
new Vue({ </q-card>
el: '#vue', {% endblock %} {% block scripts %} {{ window_vars(user) }}
mixins: [windowMixin], <script>
data: function () { new Vue({
return { el: '#vue',
tools: [] mixins: [windowMixin],
}; data: function () {
}, return {
created: function () { tools: []
var self = this;
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data;
});
} }
}); },
</script> created: function () {
var self = this
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data
})
}
})
</script>
{% endblock %} {% endblock %}

20
lnbits/extensions/paywall/templates/paywall/_api_docs.html

@ -6,23 +6,23 @@
> >
<q-expansion-item group="api" dense expand-separator label="List paywalls"> <q-expansion-item group="api" dense expand-separator label="List paywalls">
<q-card> <q-card>
<q-card-section> <q-card-section> </q-card-section>
</q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a paywall"> <q-expansion-item group="api" dense expand-separator label="Create a paywall">
<q-card> <q-card>
<q-card-section> <q-card-section> </q-card-section>
</q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Delete a paywall" class="q-pb-md"> <q-expansion-item
group="api"
dense
expand-separator
label="Delete a paywall"
class="q-pb-md"
>
<q-card> <q-card>
<q-card-section> <q-card-section> </q-card-section>
</q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
</q-expansion-item> </q-expansion-item>

199
lnbits/extensions/paywall/templates/paywall/display.html

@ -1,107 +1,122 @@
{% extends "public.html" %} {% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
{% block page %} <q-card class="q-pa-lg">
<div class="row q-col-gutter-md justify-center"> <q-card-section class="q-pa-none">
<div class="col-12 col-sm-6 col-md-5 col-lg-4"> <h5 class="text-subtitle1 q-my-none">{{ paywall.memo }}</h5>
<q-card class="q-pa-lg"> <strong class="text-purple"
<q-card-section class="q-pa-none"> >Price:
<h5 class="text-subtitle1 q-my-none">{{ paywall.memo }}</h5> <lnbits-fsat :amount="{{ paywall.amount }}"></lnbits-fsat> sat</strong
<strong class="text-purple">Price: <lnbits-fsat :amount="{{ paywall.amount }}"></lnbits-fsat> sat</strong> >
<q-separator class="q-my-lg"></q-separator> <q-separator class="q-my-lg"></q-separator>
<div v-if="paymentReq"> <div v-if="paymentReq">
<a :href="'lightning:' + paymentReq"> <a :href="'lightning:' + paymentReq">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> <q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode :value="paymentReq" :options="{width: 800}" class="rounded-borders"></qrcode> <qrcode
</q-responsive> :value="paymentReq"
</a> :options="{width: 800}"
<div class="row q-mt-lg"> class="rounded-borders"
<q-btn outline color="grey" @click="copyText(paymentReq)">Copy invoice</q-btn> ></qrcode>
</div> </q-responsive>
</a>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(paymentReq)"
>Copy invoice</q-btn
>
</div> </div>
<div v-if="redirectUrl"> </div>
<p>You can access the URL behind this paywall:<br> <div v-if="redirectUrl">
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong></p> <p>
<div class="row q-mt-lg"> You can access the URL behind this paywall:<br />
<q-btn outline color="grey" type="a" :href="redirectUrl">Open URL</q-btn> <strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong>
</div> </p>
<div class="row q-mt-lg">
<q-btn outline color="grey" type="a" :href="redirectUrl"
>Open URL</q-btn
>
</div> </div>
</q-card-section> </div>
</q-card> </q-card-section>
</div> </q-card>
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits paywall</h6>
</q-card-section>
</q-card>
</div>
</div> </div>
{% endblock %} <div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
<q-card>
{% block scripts %} <q-card-section>
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> <h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits paywall</h6>
<script> </q-card-section>
Vue.component(VueQrcode.name, VueQrcode); </q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
paymentReq: null, paymentReq: null,
redirectUrl: null redirectUrl: null
}; }
}, },
methods: { methods: {
getInvoice: function () { getInvoice: function () {
var self = this; var self = this
axios.get( axios
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice' .get('/paywall/api/v1/paywalls/{{ paywall.id }}/invoice')
).then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request; self.paymentReq = response.data.payment_request
dismissMsg = self.$q.notify({ dismissMsg = self.$q.notify({
timeout: 0, timeout: 0,
message: 'Waiting for payment...' message: 'Waiting for payment...'
}); })
paymentChecker = setInterval(function () { paymentChecker = setInterval(function () {
axios.post( axios
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice', .post(
{checking_id: response.data.checking_id} '/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
).then(function (res) { {checking_id: response.data.checking_id}
if (res.data.paid) { )
clearInterval(paymentChecker); .then(function (res) {
dismissMsg(); if (res.data.paid) {
self.redirectUrl = res.data.url; clearInterval(paymentChecker)
self.$q.localStorage.set('lnbits.paywall.{{ paywall.id }}', res.data.url); dismissMsg()
self.redirectUrl = res.data.url
self.$q.localStorage.set(
'lnbits.paywall.{{ paywall.id }}',
res.data.url
)
self.$q.notify({ self.$q.notify({
type: 'positive', type: 'positive',
message: 'Payment received!', message: 'Payment received!',
icon: null icon: null
}); })
} }
}).catch(function (error) { })
LNbits.utils.notifyApiError(error); .catch(function (error) {
}); LNbits.utils.notifyApiError(error)
}, 2000); })
}).catch(function (error) { }, 2000)
LNbits.utils.notifyApiError(error); })
}); .catch(function (error) {
} LNbits.utils.notifyApiError(error)
}, })
created: function () { }
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}'); },
created: function () {
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}')
if (url) { if (url) {
this.redirectUrl = url; this.redirectUrl = url
} else { } else {
this.getInvoice(); this.getInvoice()
};
} }
}); }
</script> })
</script>
{% endblock %} {% endblock %}

444
lnbits/extensions/paywall/templates/paywall/index.html

@ -1,214 +1,274 @@
{% extends "base.html" %} {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New paywall</q-btn
>
</q-card-section>
</q-card>
{% from "macros.jinja" import window_vars with context %} <q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
{% block page %} <div class="col">
<div class="row q-col-gutter-md"> <h5 class="text-subtitle1 q-my-none">Paywalls</h5>
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true">New paywall</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Paywalls</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div> </div>
<q-table dense flat <div class="col-auto">
:data="paywalls" <q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
row-key="id"
:columns="paywallsTable.columns"
:pagination.sync="paywallsTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.displayUrl" target="_blank"></q-btn>
<q-btn unelevated dense size="xs" icon="link" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.url" target="_blank"></q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="deletePaywall(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits paywall extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "paywall/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createPaywall" class="q-gutter-md">
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="formDialog.data.url"
type="url"
label="Target URL *"></q-input>
<q-input filled dense
v-model.number="formDialog.data.amount"
type="number"
label="Amount (sat) *"></q-input>
<q-input filled dense
v-model.trim="formDialog.data.memo"
label="Memo"
placeholder="LNbits invoice"></q-input>
<div class="row q-mt-lg">
<q-btn unelevated
color="deep-purple"
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null"
type="submit">Create paywall</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div> </div>
</q-form> </div>
</q-card> <q-table
</q-dialog> dense
flat
:data="paywalls"
row-key="id"
:columns="paywallsTable.columns"
:pagination.sync="paywallsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.url"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deletePaywall(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div> </div>
{% endblock %}
{% block scripts %} <div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
{{ window_vars(user) }} <q-card>
<script> <q-card-section>
var mapPaywall = function (obj) { <h6 class="text-subtitle1 q-my-none">LNbits paywall extension</h6>
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm'); </q-card-section>
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount); <q-card-section class="q-pa-none">
obj.displayUrl = ['/paywall/', obj.id].join(''); <q-separator></q-separator>
return obj; <q-list>
} {% include "paywall/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createPaywall" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.url"
type="url"
label="Target URL *"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.amount"
type="number"
label="Amount (sat) *"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.memo"
label="Memo"
placeholder="LNbits invoice"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null"
type="submit"
>Create paywall</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapPaywall = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/paywall/', obj.id].join('')
return obj
}
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
paywalls: [], paywalls: [],
paywallsTable: { paywallsTable: {
columns: [ columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'}, {name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'}, {name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true}, {
{ name: 'date',
name: 'amount', align: 'right', label: 'Amount (sat)', field: 'fsat', sortable: true, align: 'left',
sort: function (a, b, rowA, rowB) { label: 'Date',
return rowA.amount - rowB.amount; field: 'date',
} sortable: true
},
{
name: 'amount',
align: 'right',
label: 'Amount (sat)',
field: 'fsat',
sortable: true,
sort: function (a, b, rowA, rowB) {
return rowA.amount - rowB.amount
} }
],
pagination: {
rowsPerPage: 10
} }
}, ],
formDialog: { pagination: {
show: false, rowsPerPage: 10
data: {}
} }
}; },
}, formDialog: {
methods: { show: false,
getPaywalls: function () { data: {}
var self = this; }
}
},
methods: {
getPaywalls: function () {
var self = this
LNbits.api.request( LNbits.api
.request(
'GET', 'GET',
'/paywall/api/v1/paywalls?all_wallets', '/paywall/api/v1/paywalls?all_wallets',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
).then(function (response) { )
.then(function (response) {
self.paywalls = response.data.map(function (obj) { self.paywalls = response.data.map(function (obj) {
return mapPaywall(obj); return mapPaywall(obj)
}); })
}); })
}, },
createPaywall: function () { createPaywall: function () {
var data = { var data = {
url: this.formDialog.data.url, url: this.formDialog.data.url,
memo: this.formDialog.data.memo, memo: this.formDialog.data.memo,
amount: this.formDialog.data.amount amount: this.formDialog.data.amount
}; }
var self = this; var self = this
LNbits.api.request( LNbits.api
.request(
'POST', 'POST',
'/paywall/api/v1/paywalls', '/paywall/api/v1/paywalls',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey, _.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data data
).then(function (response) { )
self.paywalls.push(mapPaywall(response.data)); .then(function (response) {
self.formDialog.show = false; self.paywalls.push(mapPaywall(response.data))
self.formDialog.data = {}; self.formDialog.show = false
}).catch(function (error) { self.formDialog.data = {}
LNbits.utils.notifyApiError(error); })
}); .catch(function (error) {
}, LNbits.utils.notifyApiError(error)
deletePaywall: function (paywallId) { })
var self = this; },
var paywall = _.findWhere(this.paywalls, {id: paywallId}); deletePaywall: function (paywallId) {
var self = this
var paywall = _.findWhere(this.paywalls, {id: paywallId})
LNbits.utils.confirmDialog( LNbits.utils
'Are you sure you want to delete this paywall link?' .confirmDialog('Are you sure you want to delete this paywall link?')
).onOk(function () { .onOk(function () {
LNbits.api.request( LNbits.api
'DELETE', .request(
'/paywall/api/v1/paywalls/' + paywallId, 'DELETE',
_.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey '/paywall/api/v1/paywalls/' + paywallId,
).then(function (response) { _.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey
self.paywalls = _.reject(self.paywalls, function (obj) { return obj.id == paywallId; }); )
}).catch(function (error) { .then(function (response) {
LNbits.utils.notifyApiError(error); self.paywalls = _.reject(self.paywalls, function (obj) {
}); return obj.id == paywallId
}); })
}, })
exportCSV: function () { .catch(function (error) {
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls); LNbits.utils.notifyApiError(error)
} })
})
}, },
created: function () { exportCSV: function () {
if (this.g.user.wallets.length) { LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
this.getPaywalls();
}
} }
}); },
</script> created: function () {
if (this.g.user.wallets.length) {
this.getPaywalls()
}
}
})
</script>
{% endblock %} {% endblock %}

65
lnbits/extensions/tpos/templates/tpos/_api_docs.html

@ -4,49 +4,84 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-expansion-item group="api" dense expand-separator label="List all users TPoS"> <q-expansion-item
group="api"
dense
expand-separator
label="List all users TPoS"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">GET</span> /tpos/api/v1/tposs</code> <code><span class="text-light-blue">GET</span> /tpos/api/v1/tposs</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">
<code>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name": &lt;string&gt;, "wallet": &lt;string&gt;}</code> Returns 201 CREATED (application/json)
</h5>
<code
>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name":
&lt;string&gt;, "wallet": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}tpos/api/v1/tposs -H "X-Api-Key: &lt;invoice_key&gt;" </code> <code
>curl -X GET {{ request.url_root }}tpos/api/v1/tposs -H "X-Api-Key:
&lt;invoice_key&gt;"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a TPoS"> <q-expansion-item group="api" dense expand-separator label="Create a TPoS">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-green">POST</span> /tpos/api/v1/tposs</code> <code
><span class="text-light-green">POST</span> /tpos/api/v1/tposs</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"name": &lt;string&gt;, "currency": &lt;string*ie USD*&gt;}</code> <code
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> >{"name": &lt;string&gt;, "currency": &lt;string*ie USD*&gt;}</code
<code>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name": &lt;string&gt;, "wallet": &lt;string&gt;}</code> >
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name":
&lt;string&gt;, "wallet": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}tpos/api/v1/tposs -d '{"name": &lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type: application/json" -H "X-Api-Key: &lt;admin_key&gt;" <code
</code> >curl -X POST {{ request.url_root }}tpos/api/v1/tposs -d '{"name":
&lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Delete a TPoS" class="q-pb-md"> <q-expansion-item
group="api"
dense
expand-separator
label="Delete a TPoS"
class="q-pb-md"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-green">DELETE</span> /tpos/api/v1/tposs/&lt;tpos_id&gt;</code> <code
><span class="text-light-green">DELETE</span>
/tpos/api/v1/tposs/&lt;tpos_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 NO_CONTENT</h5> <h5 class="text-caption q-mt-sm q-mb-none">Returns 201 NO_CONTENT</h5>
<code></code> <code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X DELETE {{ request.url_root }}tpos/api/v1/tposs/&lt;tpos_id&gt; -H "X-Api-Key: &lt;admin_key&gt;" <code
</code> >curl -X DELETE {{ request.url_root
}}tpos/api/v1/tposs/&lt;tpos_id&gt; -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

19
lnbits/extensions/tpos/templates/tpos/_tpos.html

@ -1,11 +1,18 @@
<q-expansion-item <q-expansion-item group="extras" icon="info" label="About TPoS">
group="extras"
icon="info"
label="About TPoS">
<q-card> <q-card>
<q-card-section> <q-card-section>
<p>Thiago's Point of Sale is a secure, mobile-ready, instant and shareable point of sale terminal (PoS) for merchants. The PoS is linked to your LNbits wallet but completely air-gapped so users can ONLY create invoices. To share the TPoS hit the hash on the terminal.</p> <p>
<small>Created by <a href="https://github.com/talvasconcelos" target="_blank">Tiago Vasconcelos</a>.</small> Thiago's Point of Sale is a secure, mobile-ready, instant and shareable
point of sale terminal (PoS) for merchants. The PoS is linked to your
LNbits wallet but completely air-gapped so users can ONLY create
invoices. To share the TPoS hit the hash on the terminal.
</p>
<small
>Created by
<a href="https://github.com/talvasconcelos" target="_blank"
>Tiago Vasconcelos</a
>.</small
>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

596
lnbits/extensions/tpos/templates/tpos/index.html

@ -1,221 +1,423 @@
{% extends "base.html" %} {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New TPoS</q-btn
>
</q-card-section>
</q-card>
{% from "macros.jinja" import window_vars with context %} <q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
{% block page %} <div class="col">
<div class="row q-col-gutter-md"> <h5 class="text-subtitle1 q-my-none">TPoS</h5>
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> </div>
<q-card> <div class="col-auto">
<q-card-section> <q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true">New TPoS</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">TPoS</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div> </div>
<q-table dense flat </div>
:data="tposs" <q-table
row-key="id" dense
:columns="tpossTable.columns" flat
:pagination.sync="tpossTable.pagination"> :data="tposs"
{% raw %} row-key="id"
<template v-slot:header="props"> :columns="tpossTable.columns"
<q-tr :props="props"> :pagination.sync="tpossTable.pagination"
<q-th auto-width></q-th> >
<q-th {% raw %}
v-for="col in props.cols" <template v-slot:header="props">
:key="col.name" <q-tr :props="props">
:props="props" <q-th auto-width></q-th>
> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} {{ col.label }}
</q-th> </q-th>
<q-th auto-width></q-th> <q-th auto-width></q-th>
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <template v-slot:body="props">
<q-tr :props="props"> <q-tr :props="props">
<q-td auto-width> <q-td auto-width>
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.tpos" target="_blank"></q-btn> <q-btn
</q-td> unelevated
<q-td dense
v-for="col in props.cols" size="xs"
:key="col.name" icon="launch"
:props="props" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
> type="a"
{{ col.value }} :href="props.row.tpos"
</q-td> target="_blank"
<q-td auto-width> ></q-btn>
<q-btn flat dense size="xs" @click="deleteTPoS(props.row.id)" icon="cancel" color="pink"></q-btn> </q-td>
</q-td> <q-td v-for="col in props.cols" :key="col.name" :props="props">
</q-tr> {{ col.value }}
</template> </q-td>
{% endraw %} <q-td auto-width>
</q-table> <q-btn
</q-card-section> flat
</q-card> dense
</div> size="xs"
@click="deleteTPoS(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-5 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits TPoS extension</h6> <h6 class="text-subtitle1 q-my-none">LNbits TPoS extension</h6>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "tpos/_api_docs.html" %}
<q-separator></q-separator> <q-separator></q-separator>
<q-list> {% include "tpos/_tpos.html" %}
{% include "tpos/_api_docs.html" %} </q-list>
<q-separator></q-separator> </q-card-section>
{% include "tpos/_tpos.html" %} </q-card>
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="createTPoS" class="q-gutter-md">
<q-input filled dense
v-model.trim="formDialog.data.name"
label="Name"
placeholder="Tiago's PoS"></q-input>
<q-select filled dense
emit-value v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"></q-select>
<q-select filled dense
emit-value v-model="formDialog.data.currency"
:options="currencyOptions"
label="Currency *"></q-select>
<div class="row q-mt-lg">
<q-btn unelevated
color="deep-purple"
:disable="formDialog.data.currency == null || formDialog.data.name == null"
type="submit">Create TPoS</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</div> </div>
{% endblock %}
{% block scripts %} <q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
{{ window_vars(user) }} <q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
<script> <q-form @submit="createTPoS" class="q-gutter-md">
var mapTPoS = function (obj) { <q-input
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm'); filled
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount); dense
obj.tpos = ['/tpos/', obj.id].join(''); v-model.trim="formDialog.data.name"
return obj; label="Name"
} placeholder="Tiago's PoS"
></q-input>
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-select
filled
dense
emit-value
v-model="formDialog.data.currency"
:options="currencyOptions"
label="Currency *"
></q-select>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.currency == null || formDialog.data.name == null"
type="submit"
>Create TPoS</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapTPoS = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.tpos = ['/tpos/', obj.id].join('')
return obj
}
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
tposs: [], tposs: [],
currencyOptions: [ currencyOptions: [
'USD','EUR','GBP','AED','AFN','ALL','AMD','ANG','AOA','ARS','AUD','AWG','AZN','BAM','BBD','BDT','BGN','BHD', 'USD',
'BIF','BMD','BND','BOB','BRL','BSD','BTN','BWP','BYN','BZD','CAD','CDF','CHF','CLF','CLP','CNH','CNY','COP', 'EUR',
'CRC','CUC','CUP','CVE','CZK','DJF','DKK','DOP','DZD','EGP','ERN','ETB','EUR','FJD','FKP','GBP','GEL','GGP', 'GBP',
'GHS','GIP','GMD','GNF','GTQ','GYD','HKD','HNL','HRK','HTG','HUF','IDR','ILS','IMP','INR','IQD','IRR','ISK', 'AED',
'JEP','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KWD','KYD','KZT','LAK','LBP','LKR','LRD','LSL', 'AFN',
'LYD','MAD','MDL','MGA','MKD','MMK','MNT','MOP','MRO','MUR','MVR','MWK','MXN','MYR','MZN','NAD','NGN','NIO','NOK','NPR','NZD','OMR','PAB','PEN','PGK','PHP','PKR','PLN','PYG','QAR','RON','RSD','RUB','RWF','SAR','SBD', 'ALL',
'SCR','SDG','SEK','SGD','SHP','SLL','SOS','SRD','SSP','STD','SVC','SYP','SZL','THB','TJS','TMT','TND','TOP', 'AMD',
'TRY','TTD','TWD','TZS','UAH','UGX','USD','UYU','UZS','VEF','VES','VND','VUV','WST','XAF','XAG','XAU','XCD', 'ANG',
'XDR','XOF','XPD','XPF','XPT','YER','ZAR','ZMW','ZWL' 'AOA',
], 'ARS',
tpossTable: { 'AUD',
columns: [ 'AWG',
{name: 'id', align: 'left', label: 'ID', field: 'id'}, 'AZN',
{name: 'name', align: 'left', label: 'Name', field: 'name'}, 'BAM',
{name: 'currency', align: 'left', label: 'Currency', field: 'currency'} 'BBD',
], 'BDT',
pagination: { 'BGN',
rowsPerPage: 10 'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BTN',
'BWP',
'BYN',
'BZD',
'CAD',
'CDF',
'CHF',
'CLF',
'CLP',
'CNH',
'CNY',
'COP',
'CRC',
'CUC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ERN',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GGP',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'IMP',
'INR',
'IQD',
'IRR',
'ISK',
'JEP',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KPW',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRO',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SDG',
'SEK',
'SGD',
'SHP',
'SLL',
'SOS',
'SRD',
'SSP',
'STD',
'SVC',
'SYP',
'SZL',
'THB',
'TJS',
'TMT',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VEF',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XAG',
'XAU',
'XCD',
'XDR',
'XOF',
'XPD',
'XPF',
'XPT',
'YER',
'ZAR',
'ZMW',
'ZWL'
],
tpossTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'currency',
align: 'left',
label: 'Currency',
field: 'currency'
} }
}, ],
formDialog: { pagination: {
show: false, rowsPerPage: 10
data: {}
} }
};
},
methods: {
closeFormDialog: function () {
this.formDialog.data = {};
}, },
getTPoSs: function () { formDialog: {
var self = this; show: false,
data: {}
}
}
},
methods: {
closeFormDialog: function () {
this.formDialog.data = {}
},
getTPoSs: function () {
var self = this
LNbits.api.request( LNbits.api
.request(
'GET', 'GET',
'/tpos/api/v1/tposs?all_wallets', '/tpos/api/v1/tposs?all_wallets',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
).then(function (response) { )
.then(function (response) {
self.tposs = response.data.map(function (obj) { self.tposs = response.data.map(function (obj) {
return mapTPoS(obj); return mapTPoS(obj)
}); })
}); })
}, },
createTPoS: function () { createTPoS: function () {
var data = { var data = {
name: this.formDialog.data.name, name: this.formDialog.data.name,
currency: this.formDialog.data.currency currency: this.formDialog.data.currency
}; }
var self = this; var self = this
LNbits.api.request( LNbits.api
.request(
'POST', 'POST',
'/tpos/api/v1/tposs', '/tpos/api/v1/tposs',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey, _.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data data
).then(function (response) { )
self.tposs.push(mapTPoS(response.data)); .then(function (response) {
self.formDialog.show = false; self.tposs.push(mapTPoS(response.data))
}).catch(function (error) { self.formDialog.show = false
LNbits.utils.notifyApiError(error); })
}); .catch(function (error) {
}, LNbits.utils.notifyApiError(error)
deleteTPoS: function (tposId) { })
var self = this; },
var tpos = _.findWhere(this.tposs, {id: tposId}); deleteTPoS: function (tposId) {
var self = this
var tpos = _.findWhere(this.tposs, {id: tposId})
LNbits.utils.confirmDialog( LNbits.utils
'Are you sure you want to delete this TPoS?' .confirmDialog('Are you sure you want to delete this TPoS?')
).onOk(function () { .onOk(function () {
LNbits.api.request( LNbits.api
'DELETE', .request(
'/tpos/api/v1/tposs/' + tposId, 'DELETE',
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey '/tpos/api/v1/tposs/' + tposId,
).then(function (response) { _.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
self.tposs = _.reject(self.tposs, function (obj) { return obj.id == tposId; }); )
}).catch(function (error) { .then(function (response) {
LNbits.utils.notifyApiError(error); self.tposs = _.reject(self.tposs, function (obj) {
}); return obj.id == tposId
}); })
}, })
exportCSV: function () { .catch(function (error) {
LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs); LNbits.utils.notifyApiError(error)
} })
})
}, },
created: function () { exportCSV: function () {
if (this.g.user.wallets.length) { LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs)
this.getTPoSs(); }
} },
created: function () {
if (this.g.user.wallets.length) {
this.getTPoSs()
} }
}); }
</script> })
</script>
{% endblock %} {% endblock %}

454
lnbits/extensions/tpos/templates/tpos/tpos.html

@ -1,225 +1,265 @@
{% extends "public.html" %} {% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% endblock
%} {% block footer %}{% endblock %} {% block page_container %}
<q-page-container>
{% block toolbar_title %}{{ tpos.name }}{% endblock %} <q-page>
<q-page-sticky v-if="exchangeRate" expand position="top">
{% block footer %}{% endblock %} <div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
{% block page_container %} <h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
<q-page-container> <h5 class="q-mt-none">
<q-page> {% raw %}{{ fsat }}{% endraw %} <small>sat</small>
<q-page-sticky v-if="exchangeRate" expand position="top"> </h5>
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
</div>
</div> </div>
</q-page-sticky> </div>
<q-page-sticky expand position="bottom"> </q-page-sticky>
<div class="row justify-center full-width"> <q-page-sticky expand position="bottom">
<div class="col-12 col-sm-8 col-md-6 col-lg-4"> <div class="row justify-center full-width">
<div class="keypad q-pa-sm"> <div class="col-12 col-sm-8 col-md-6 col-lg-4">
<q-btn unelevated <div class="keypad q-pa-sm">
@click="stack.push(1)" <q-btn unelevated @click="stack.push(1)" size="xl" color="grey-8"
size="xl" color="grey-8">1</q-btn> >1</q-btn
<q-btn unelevated >
@click="stack.push(2)" <q-btn unelevated @click="stack.push(2)" size="xl" color="grey-8"
size="xl" color="grey-8">2</q-btn> >2</q-btn
<q-btn unelevated >
@click="stack.push(3)" <q-btn unelevated @click="stack.push(3)" size="xl" color="grey-8"
size="xl" color="grey-8">3</q-btn> >3</q-btn
<q-btn unelevated >
@click="stack = []" <q-btn
size="xl" color="pink" class="btn-cancel">C</q-btn> unelevated
<q-btn unelevated @click="stack = []"
@click="stack.push(4)" size="xl"
size="xl" color="grey-8">4</q-btn> color="pink"
<q-btn unelevated class="btn-cancel"
@click="stack.push(5)" >C</q-btn
size="xl" color="grey-8">5</q-btn> >
<q-btn unelevated <q-btn unelevated @click="stack.push(4)" size="xl" color="grey-8"
@click="stack.push(6)" >4</q-btn
size="xl" color="grey-8">6</q-btn> >
<q-btn unelevated <q-btn unelevated @click="stack.push(5)" size="xl" color="grey-8"
@click="stack.push(7)" >5</q-btn
size="xl" color="grey-8">7</q-btn> >
<q-btn unelevated <q-btn unelevated @click="stack.push(6)" size="xl" color="grey-8"
@click="stack.push(8)" >6</q-btn
size="xl" color="grey-8">8</q-btn> >
<q-btn unelevated <q-btn unelevated @click="stack.push(7)" size="xl" color="grey-8"
@click="stack.push(9)" >7</q-btn
size="xl" color="grey-8">9</q-btn> >
<q-btn unelevated <q-btn unelevated @click="stack.push(8)" size="xl" color="grey-8"
:disabled="amount == 0" >8</q-btn
@click="showInvoice()" >
size="xl" color="green" class="btn-confirm">OK</q-btn> <q-btn unelevated @click="stack.push(9)" size="xl" color="grey-8"
<q-btn unelevated >9</q-btn
@click="stack.splice(-1, 1)" >
size="xl" color="grey-7">DEL</q-btn> <q-btn
<q-btn unelevated unelevated
@click="stack.push(0)" :disabled="amount == 0"
size="xl" color="grey-8">0</q-btn> @click="showInvoice()"
<q-btn unelevated size="xl"
@click="urlDialog.show = true" color="green"
size="xl" color="grey-7">#</q-btn> class="btn-confirm"
</div> >OK</q-btn
>
<q-btn
unelevated
@click="stack.splice(-1, 1)"
size="xl"
color="grey-7"
>DEL</q-btn
>
<q-btn unelevated @click="stack.push(0)" size="xl" color="grey-8"
>0</q-btn
>
<q-btn
unelevated
@click="urlDialog.show = true"
size="xl"
color="grey-7"
>#</q-btn
>
</div> </div>
</div> </div>
</q-page-sticky> </div>
<q-dialog v-model="invoiceDialog.show" position="top" @hide="closeInvoiceDialog"> </q-page-sticky>
<q-card v-if="invoiceDialog.data" class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-dialog
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> v-model="invoiceDialog.show"
<qrcode :value="invoiceDialog.data.payment_request" :options="{width: 800}" class="rounded-borders"></qrcode> position="top"
</q-responsive> @hide="closeInvoiceDialog"
<div class="text-center"> >
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3> <q-card
<h5 class="q-mt-none"> v-if="invoiceDialog.data"
{% raw %}{{ fsat }}{% endraw %} <small>sat</small> class="q-pa-lg q-pt-xl lnbits__dialog-card"
</h5> >
</div> <q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<div class="row q-mt-lg"> <qrcode
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> :value="invoiceDialog.data.payment_request"
</div> :options="{width: 800}"
</q-card> class="rounded-borders"
</q-dialog> ></qrcode>
<q-dialog v-model="urlDialog.show" position="top"> </q-responsive>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <div class="text-center">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> <h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
<qrcode value="{{ request.url }}" :options="{width: 800}" class="rounded-borders"></qrcode> <h5 class="q-mt-none">
</q-responsive> {% raw %}{{ fsat }}{% endraw %} <small>sat</small>
<div class="text-center q-mb-xl"> </h5>
<p style="word-break: break-all"><strong>{{ tpos.name }}</strong><br>{{ request.url }}</p> </div>
</div> <div class="row q-mt-lg">
<div class="row q-mt-lg"> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
<q-btn outline color="grey" @click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')">Copy URL</q-btn> </div>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> </q-card>
</div> </q-dialog>
</q-card> <q-dialog v-model="urlDialog.show" position="top">
</q-dialog> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
</q-page> <q-responsive :ratio="1" class="q-mx-xl q-mb-md">
</q-page-container> <qrcode
{% endblock %} value="{{ request.url }}"
:options="{width: 800}"
{% block styles %} class="rounded-borders"
<style> ></qrcode>
.keypad { </q-responsive>
display: grid; <div class="text-center q-mb-xl">
grid-gap: 8px; <p style="word-break: break-all;">
grid-template-columns: repeat(4, 1fr); <strong>{{ tpos.name }}</strong><br />{{ request.url }}
grid-template-rows: repeat(4, 1fr); </p>
} </div>
.keypad .btn { <div class="row q-mt-lg">
height: 100%; <q-btn
} outline
.btn-cancel, .btn-confirm { color="grey"
grid-row: auto/span 2; @click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')"
} >Copy URL</q-btn
</style> >
{% endblock %} <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
{% block scripts %} </q-card>
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> </q-dialog>
<script> </q-page>
Vue.component(VueQrcode.name, VueQrcode); </q-page-container>
{% endblock %} {% block styles %}
<style>
.keypad {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.keypad .btn {
height: 100%;
}
.btn-cancel,
.btn-confirm {
grid-row: auto/span 2;
}
</style>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
tposId: '{{ tpos.id }}', tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}', currency: '{{ tpos.currency }}',
exchangeRate: null, exchangeRate: null,
stack: [], stack: [],
invoiceDialog: { invoiceDialog: {
show: false, show: false,
data: null, data: null,
dismissMsg: null, dismissMsg: null,
paymentChecker: null paymentChecker: null
},
urlDialog: {
show: false
}
};
},
computed: {
amount: function () {
if (!this.stack.length) return 0.00;
return (Number(this.stack.join('')) / 100).toFixed(2);
}, },
famount: function () { urlDialog: {
return LNbits.utils.formatCurrency(this.amount, this.currency); show: false
},
sat: function () {
if (!this.exchangeRate) return 0;
return Math.ceil((this.amount / this.exchangeRate) * 100000000);
},
fsat: function () {
return LNbits.utils.formatSat(this.sat);
} }
}
},
computed: {
amount: function () {
if (!this.stack.length) return 0.0
return (Number(this.stack.join('')) / 100).toFixed(2)
}, },
methods: { famount: function () {
closeInvoiceDialog: function () { return LNbits.utils.formatCurrency(this.amount, this.currency)
this.stack = []; },
var dialog = this.invoiceDialog; sat: function () {
setTimeout(function () { if (!this.exchangeRate) return 0
clearInterval(dialog.paymentChecker); return Math.ceil((this.amount / this.exchangeRate) * 100000000)
dialog.dismissMsg(); },
}, 3000); fsat: function () {
}, return LNbits.utils.formatSat(this.sat)
showInvoice: function () { }
var self = this; },
var dialog = this.invoiceDialog; methods: {
closeInvoiceDialog: function () {
this.stack = []
var dialog = this.invoiceDialog
setTimeout(function () {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
}, 3000)
},
showInvoice: function () {
var self = this
var dialog = this.invoiceDialog
axios.post( axios
'/tpos/api/v1/tposs/' + this.tposId + '/invoices/', .post('/tpos/api/v1/tposs/' + this.tposId + '/invoices/', {
{amount: this.sat} amount: this.sat
).then(function (response) { })
dialog.data = response.data; .then(function (response) {
dialog.show = true; dialog.data = response.data
dialog.show = true
dialog.dismissMsg = self.$q.notify({ dialog.dismissMsg = self.$q.notify({
timeout: 0, timeout: 0,
message: 'Waiting for payment...' message: 'Waiting for payment...'
}); })
dialog.paymentChecker = setInterval(function () { dialog.paymentChecker = setInterval(function () {
axios.get( axios
'/tpos/api/v1/tposs/' + self.tposId + '/invoices/' + response.data.checking_id .get(
).then(function (res) { '/tpos/api/v1/tposs/' +
if (res.data.paid) { self.tposId +
clearInterval(dialog.paymentChecker); '/invoices/' +
dialog.dismissMsg(); response.data.checking_id
dialog.show = false; )
.then(function (res) {
if (res.data.paid) {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
dialog.show = false
self.$q.notify({ self.$q.notify({
type: 'positive', type: 'positive',
message: self.fsat + ' sat received!', message: self.fsat + ' sat received!',
icon: null icon: null
}); })
} }
}); })
}, 2000); }, 2000)
})
}).catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error); LNbits.utils.notifyApiError(error)
}); })
},
getRates: function () {
var self = this;
axios.get("https://api.opennode.co/v1/rates").then(function (response) {
self.exchangeRate = response.data.data['BTC' + self.currency][self.currency];
});
}
}, },
created: function () { getRates: function () {
var getRates = this.getRates; var self = this
getRates(); axios.get('https://api.opennode.co/v1/rates').then(function (response) {
setInterval(function () { getRates(); }, 20000); self.exchangeRate =
response.data.data['BTC' + self.currency][self.currency]
})
} }
}); },
</script> created: function () {
var getRates = this.getRates
getRates()
setInterval(function () {
getRates()
}, 20000)
}
})
</script>
{% endblock %} {% endblock %}

200
lnbits/extensions/usermanager/templates/usermanager/_api_docs.html

@ -1,19 +1,26 @@
<q-expansion-item
<q-expansion-item group="extras"
group="extras" icon="swap_vertical_circle"
icon="swap_vertical_circle" label="Info"
label="Info" :content-inset-level="0.5"
:content-inset-level="0.5" >
> <q-card>
<q-card> <q-card-section>
<q-card-section> <h5 class="text-subtitle1 q-my-none">
<h5 class="text-subtitle1 q-my-none">User Manager: Make and manager users/wallets</h5> User Manager: Make and manager users/wallets
<p>To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. <br/>For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the develpoers stack as the user and wallet manager.<br/> </h5>
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p> <p>
</q-card> To help developers use LNbits to manage their users, the User Manager
</q-card-section> extension allows the creation and management of users and wallets.
<br />For example, a games developer may be developing a game that needs
</q-card-section> each user to have their own wallet, LNbits can be included in the
develpoers stack as the user and wallet manager.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item <q-expansion-item
group="extras" group="extras"
@ -21,101 +28,188 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-expansion-item group="api" dense expand-separator label="GET users"> <q-expansion-item group="api" dense expand-separator label="GET users">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">GET</span> /usermanager/api/v1/users</code> <code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/users</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON list of users</code> <code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" </code> <code
>curl -X GET {{ request.url_root }}usermanager/api/v1/users -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET wallets"> <q-expansion-item group="api" dense expand-separator label="GET wallets">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">GET</span> /usermanager/api/v1/wallets/&lt;user_id&gt;</code> <code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/wallets/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON wallet data</code> <code>JSON wallet data</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/&lt;user_id&gt; -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" </code> <code
>curl -X GET {{ request.url_root
}}usermanager/api/v1/wallets/&lt;user_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET transactions"> <q-expansion-item group="api" dense expand-separator label="GET transactions">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">GET</span> /usermanager/api/v1/wallets&lt;wallet_id&gt;</code> <code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/wallets&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON a wallets transactions</code> <code>JSON a wallets transactions</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}usermanager/api/v1/wallets&lt;wallet_id&gt; -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" </code> <code
>curl -X GET {{ request.url_root
}}usermanager/api/v1/wallets&lt;wallet_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST user + initial wallet"> <q-expansion-item
group="api"
dense
expand-separator
label="POST user + initial wallet"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-green">POST</span> /usermanager/api/v1/users</code> <code
><span class="text-light-green">POST</span>
/usermanager/api/v1/users</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;, "Content-type": "application/json"}</code> <code
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json) - "admin_id" is a YOUR user ID</h5> >{"X-Api-Key": &lt;string&gt;, "Content-type":
<code>{"admin_id": &lt;string&gt;, "user_name": &lt;string&gt;, "wallet_name": &lt;string&gt;}</code> "application/json"}</code
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> >
<code>{"checking_id": &lt;string&gt;,"payment_request": &lt;string&gt;}</code> <h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json) - "admin_id" is a YOUR user ID
</h5>
<code
>{"admin_id": &lt;string&gt;, "user_name": &lt;string&gt;,
"wallet_name": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}usermanager/api/v1/users -d '{"admin_id": "{{ g.user.id }}", "wallet_name": &lt;string&gt;, "user_name": &lt;string&gt;}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H "Content-type: application/json" <code
</code> >curl -X POST {{ request.url_root }}usermanager/api/v1/users -d
'{"admin_id": "{{ g.user.id }}", "wallet_name": &lt;string&gt;,
"user_name": &lt;string&gt;}' -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST wallet"> <q-expansion-item group="api" dense expand-separator label="POST wallet">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-green">POST</span> /usermanager/api/v1/wallets</code> <code
><span class="text-light-green">POST</span>
/usermanager/api/v1/wallets</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;, "Content-type": "application/json"}</code> <code
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json) - "admin_id" is a YOUR user ID</h5> >{"X-Api-Key": &lt;string&gt;, "Content-type":
<code>{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;, "admin_id": &lt;string&gt;}</code> "application/json"}</code
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> >
<code>{"checking_id": &lt;string&gt;,"payment_request": &lt;string&gt;}</code> <h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json) - "admin_id" is a YOUR user ID
</h5>
<code
>{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d '{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;, "admin_id": "{{ g.user.id }}"}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H "Content-type: application/json" <code
</code> >curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d
'{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": "{{ g.user.id }}"}' -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="DELETE user and their wallets"> <q-expansion-item
group="api"
dense
expand-separator
label="DELETE user and their wallets"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-red">DELETE</span> /usermanager/api/v1/users/&lt;user_id&gt;</code> <code
><span class="text-red">DELETE</span>
/usermanager/api/v1/users/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/&lt;user_id&gt; -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" </code> <code
>curl -X DELETE {{ request.url_root
}}usermanager/api/v1/users/&lt;user_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="DELETE wallet"> <q-expansion-item group="api" dense expand-separator label="DELETE wallet">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-red">DELETE</span> /usermanager/api/v1/wallets/&lt;wallet_id&gt;</code> <code
><span class="text-red">DELETE</span>
/usermanager/api/v1/wallets/&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code> <code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/&lt;wallet_id&gt; -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" </code> <code
>curl -X DELETE {{ request.url_root
}}usermanager/api/v1/wallets/&lt;wallet_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
</q-expansion-item> </q-expansion-item>

777
lnbits/extensions/usermanager/templates/usermanager/index.html

@ -1,416 +1,453 @@
{% extends "base.html" %} {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% from "macros.jinja" import window_vars with context %} <div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
{% block page %} <q-card-section>
<div class="row q-col-gutter-md"> <q-btn unelevated color="deep-purple" @click="userDialog.show = true"
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> >New User</q-btn
<q-card> >
<q-card-section> <q-btn unelevated color="deep-purple" @click="walletDialog.show = true"
<q-btn unelevated color="deep-purple" @click="userDialog.show = true">New User</q-btn> >New Wallet
<q-btn unelevated color="deep-purple" @click="walletDialog.show = true">New Wallet </q-btn>
</q-btn> </q-card-section>
</q-card-section> </q-card>
</q-card>
<q-card>
<q-card> <q-card-section>
<q-card-section> <div class="row items-center no-wrap q-mb-md">
<div class="row items-center no-wrap q-mb-md"> <div class="col">
<div class="col"> <h5 class="text-subtitle1 q-my-none">Users</h5>
<h5 class="text-subtitle1 q-my-none">Users</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportUsersCSV">Export to CSV</q-btn>
</div>
</div> </div>
<q-table dense flat <div class="col-auto">
:data="users" <q-btn flat color="grey" @click="exportUsersCSV"
row-key="id" >Export to CSV</q-btn
:columns="usersTable.columns" >
:pagination.sync="usersTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="deleteUser(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Wallets</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportWalletsCSV">Export to CSV</q-btn>
</div>
</div> </div>
<q-table dense flat </div>
:data="wallets" <q-table
row-key="id" dense
:columns="walletsTable.columns" flat
:pagination.sync="walletsTable.pagination"> :data="users"
{% raw %} row-key="id"
<template v-slot:header="props"> :columns="usersTable.columns"
<q-tr :props="props"> :pagination.sync="usersTable.pagination"
<q-th auto-width></q-th> >
<q-th {% raw %}
v-for="col in props.cols" <template v-slot:header="props">
:key="col.name" <q-tr :props="props">
:props="props" <q-th v-for="col in props.cols" :key="col.name" :props="props">
> {{ col.label }}
{{ col.label }} </q-th>
</q-th> <q-th auto-width></q-th>
<q-th auto-width></q-th> </q-tr>
</q-tr> </template>
</template> <template v-slot:body="props">
<template v-slot:body="props"> <q-tr :props="props">
<q-tr :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
<q-td auto-width> {{ col.value }}
<q-btn unelevated dense size="xs" icon="account_balance_wallet" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.walllink" target="_blank"></q-btn> </q-td>
<q-tooltip> <q-td auto-width>
Link to wallet <q-btn
</q-tooltip> flat
dense
</q-td> size="xs"
<q-td @click="deleteUser(props.row.id)"
v-for="col in props.cols" icon="cancel"
:key="col.name" color="pink"
:props="props" ></q-btn>
> </q-td>
</q-tr>
{{ col.value }} </template>
</q-td> {% endraw %}
<q-td auto-width> </q-table>
</q-card-section>
<q-btn flat dense size="xs" @click="deleteWallet(props.row.id)" icon="cancel" color="pink"></q-btn> </q-card>
</q-td>
</q-tr> <q-card>
</template> <q-card-section>
{% endraw %} <div class="row items-center no-wrap q-mb-md">
</q-table> <div class="col">
</q-card-section> <h5 class="text-subtitle1 q-my-none">Wallets</h5>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits User Manager Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "usermanager/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="userDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendUserFormData" class="q-gutter-md">
<q-input filled dense
v-model.trim="userDialog.data.usrname"
label="Username"></q-input>
<q-input filled dense
v-model.trim="userDialog.data.walname"
label="Initial wallet name"></q-input>
<q-btn unelevated
color="deep-purple"
:disable="userDialog.data.walname == null"
type="submit">Create User</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div> </div>
</q-form> <div class="col-auto">
</q-card> <q-btn flat color="grey" @click="exportWalletsCSV"
</q-dialog> >Export to CSV</q-btn
>
<q-dialog v-model="walletDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendWalletFormData" class="q-gutter-md">
<q-select filled dense emit-value v-model="walletDialog.data.user" :options="userOptions" label="User *">
</q-select>
<q-input filled dense
v-model.trim="walletDialog.data.walname"
label="Wallet name"></q-input>
<q-btn unelevated
color="deep-purple"
:disable="walletDialog.data.walname == null"
type="submit">Create Wallet</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div> </div>
</q-form> </div>
</q-card> <q-table
</q-dialog> dense
flat
:data="wallets"
row-key="id"
:columns="walletsTable.columns"
:pagination.sync="walletsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="account_balance_wallet"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.walllink"
target="_blank"
></q-btn>
<q-tooltip>
Link to wallet
</q-tooltip>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteWallet(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div> </div>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
var mapUserManager = function (obj) {
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
obj.walllink = ['../wallet?usr=', obj.user, '&wal=', obj.id].join('');
obj._data = _.clone(obj);
return obj;
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
wallets: [],
users: [],
usersTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Username', field: 'name'}
],
pagination: {
rowsPerPage: 10
}
},
walletsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'user', align: 'left', label: 'User', field: 'user'},
{name: 'adminkey', align: 'left', label: 'Admin Key', field: 'adminkey'},
{name: 'inkey', align: 'left', label: 'Invoice Key', field: 'inkey'}
],
pagination: {
rowsPerPage: 10
}
},
walletDialog: {
show: false,
data: {}
},
userDialog: {
show: false,
data: {}
},
};
},
computed: {
userOptions: function () {
return this.users.map(function (obj) {
console.log(obj.id)
return {
value: String(obj.id),
label: String(obj.id)
};
});
},
},
methods: {
///////////////Users////////////////////////////
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits User Manager Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "usermanager/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
getUsers: function () { <q-dialog v-model="userDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
<q-form @submit="sendUserFormData" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="userDialog.data.usrname"
label="Username"
></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.walname"
label="Initial wallet name"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="userDialog.data.walname == null"
type="submit"
>Create User</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="walletDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
<q-form @submit="sendWalletFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="walletDialog.data.user"
:options="userOptions"
label="User *"
>
</q-select>
<q-input
filled
dense
v-model.trim="walletDialog.data.walname"
label="Wallet name"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="walletDialog.data.walname == null"
type="submit"
>Create Wallet</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapUserManager = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.walllink = ['../wallet?usr=', obj.user, '&wal=', obj.id].join('')
obj._data = _.clone(obj)
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
wallets: [],
users: [],
usersTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Username', field: 'name'}
],
pagination: {
rowsPerPage: 10
}
},
walletsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'user', align: 'left', label: 'User', field: 'user'},
{
name: 'adminkey',
align: 'left',
label: 'Admin Key',
field: 'adminkey'
},
{name: 'inkey', align: 'left', label: 'Invoice Key', field: 'inkey'}
],
pagination: {
rowsPerPage: 10
}
},
walletDialog: {
show: false,
data: {}
},
userDialog: {
show: false,
data: {}
}
}
},
computed: {
userOptions: function () {
return this.users.map(function (obj) {
console.log(obj.id)
return {
value: String(obj.id),
label: String(obj.id)
}
})
}
},
methods: {
///////////////Users////////////////////////////
var self = this; getUsers: function () {
var self = this
LNbits.api.request( LNbits.api
.request(
'GET', 'GET',
'/usermanager/api/v1/users', '/usermanager/api/v1/users',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
).then(function (response) { )
.then(function (response) {
self.users = response.data.map(function (obj) { self.users = response.data.map(function (obj) {
return mapUserManager(obj)
})
})
},
return mapUserManager(obj); openUserUpdateDialog: function (linkId) {
}); var link = _.findWhere(this.users, {id: linkId})
});
},
openUserUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId});
this.userDialog.data = _.clone(link._data);
this.userDialog.show = true;
},
sendUserFormData: function () {
if (this.userDialog.data.id){}
else{
this.userDialog.data = _.clone(link._data)
this.userDialog.show = true
},
sendUserFormData: function () {
if (this.userDialog.data.id) {
} else {
var data = { var data = {
admin_id: this.g.user.id, admin_id: this.g.user.id,
user_name: this.userDialog.data.usrname, user_name: this.userDialog.data.usrname,
wallet_name: this.userDialog.data.walname wallet_name: this.userDialog.data.walname
};} }
}
{ this.createUser(data); } {
}, this.createUser(data)
}
},
createUser: function (data) { createUser: function (data) {
var self = this; var self = this
LNbits.api.request( LNbits.api
.request(
'POST', 'POST',
'/usermanager/api/v1/users', '/usermanager/api/v1/users',
this.g.user.wallets[0].inkey, this.g.user.wallets[0].inkey,
data data
).then(function (response) { )
.then(function (response) {
self.users.push(mapUserManager(response.data)); self.users.push(mapUserManager(response.data))
self.userDialog.show = false; self.userDialog.show = false
self.userDialog.data = {}; self.userDialog.data = {}
data = {}; data = {}
}).catch(function (error) { })
LNbits.utils.notifyApiError(error); .catch(function (error) {
}); LNbits.utils.notifyApiError(error)
}, })
deleteUser: function (userId) { },
var self = this; deleteUser: function (userId) {
var self = this
console.log(userId)
LNbits.utils.confirmDialog( console.log(userId)
'Are you sure you want to delete this User link?' LNbits.utils
).onOk(function () { .confirmDialog('Are you sure you want to delete this User link?')
.onOk(function () {
LNbits.api.request( LNbits.api
'DELETE', .request(
'/usermanager/api/v1/users/' + userId, 'DELETE',
self.g.user.wallets[0].inkey '/usermanager/api/v1/users/' + userId,
).then(function (response) { self.g.user.wallets[0].inkey
self.users = _.reject(self.users, function (obj) { return obj.id == userId; }); )
}).catch(function (error) { .then(function (response) {
LNbits.utils.notifyApiError(error); self.users = _.reject(self.users, function (obj) {
}); return obj.id == userId
}); })
}, })
.catch(function (error) {
exportUsersCSV: function () { LNbits.utils.notifyApiError(error)
LNbits.utils.exportCSV(this.usersTable.columns, this.users); })
}, })
},
exportUsersCSV: function () {
LNbits.utils.exportCSV(this.usersTable.columns, this.users)
},
///////////////Wallets//////////////////////////// ///////////////Wallets////////////////////////////
getWallets: function () { getWallets: function () {
var self = this; var self = this
LNbits.api.request( LNbits.api
.request(
'GET', 'GET',
'/usermanager/api/v1/wallets', '/usermanager/api/v1/wallets',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
).then(function (response) { )
.then(function (response) {
self.wallets = response.data.map(function (obj) { self.wallets = response.data.map(function (obj) {
return mapUserManager(obj); return mapUserManager(obj)
}); })
}); })
}, },
openWalletUpdateDialog: function (linkId) { openWalletUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId}); var link = _.findWhere(this.users, {id: linkId})
this.walletDialog.data = _.clone(link._data); this.walletDialog.data = _.clone(link._data)
this.walletDialog.show = true; this.walletDialog.show = true
}, },
sendWalletFormData: function () { sendWalletFormData: function () {
if (this.walletDialog.data.id){} if (this.walletDialog.data.id) {
else{ } else {
var data = { var data = {
user_id: this.walletDialog.data.user, user_id: this.walletDialog.data.user,
admin_id: this.g.user.id, admin_id: this.g.user.id,
wallet_name: this.walletDialog.data.walname wallet_name: this.walletDialog.data.walname
};} }
}
{ this.createWallet(data); } {
}, this.createWallet(data)
}
},
createWallet: function (data) { createWallet: function (data) {
var self = this; var self = this
LNbits.api.request( LNbits.api
.request(
'POST', 'POST',
'/usermanager/api/v1/wallets', '/usermanager/api/v1/wallets',
this.g.user.wallets[0].inkey, this.g.user.wallets[0].inkey,
data data
).then(function (response) { )
self.wallets.push(mapUserManager(response.data)); .then(function (response) {
self.walletDialog.show = false; self.wallets.push(mapUserManager(response.data))
self.walletDialog.data = {}; self.walletDialog.show = false
data = {}; self.walletDialog.data = {}
}).catch(function (error) { data = {}
LNbits.utils.notifyApiError(error); })
}); .catch(function (error) {
}, LNbits.utils.notifyApiError(error)
deleteWallet: function (userId) { })
var self = this;
LNbits.utils.confirmDialog(
'Are you sure you want to delete this wallet link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/usermanager/api/v1/wallets/' + userId,
self.g.user.wallets[0].inkey
).then(function (response) {
self.wallets = _.reject(self.wallets, function (obj) { return obj.id == userId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportWalletsCSV: function () {
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets);
}
}, },
created: function () { deleteWallet: function (userId) {
if (this.g.user.wallets.length) { var self = this
this.getUsers();
this.getWallets(); LNbits.utils
} .confirmDialog('Are you sure you want to delete this wallet link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/usermanager/api/v1/wallets/' + userId,
self.g.user.wallets[0].inkey
)
.then(function (response) {
self.wallets = _.reject(self.wallets, function (obj) {
return obj.id == userId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportWalletsCSV: function () {
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets)
} }
}); },
created: function () {
</script> if (this.g.user.wallets.length) {
this.getUsers()
this.getWallets()
}
}
})
</script>
{% endblock %} {% endblock %}

104
lnbits/extensions/withdraw/templates/withdraw/_api_docs.html

@ -4,64 +4,126 @@
label="API info" label="API info"
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-expansion-item group="api" dense expand-separator label="List withdraw links"> <q-expansion-item
group="api"
dense
expand-separator
label="List withdraw links"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">GET</span> /withdraw/api/v1/links</code> <code
><span class="text-light-blue">GET</span> /withdraw/api/v1/links</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}withdraw/api/v1/links -H "X-Api-Key: &lt;invoice_key&gt;" </code> <code
>curl -X GET {{ request.url_root }}withdraw/api/v1/links -H
"X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a withdraw link"> <q-expansion-item
group="api"
dense
expand-separator
label="Create a withdraw link"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-green">POST</span> /withdraw/api/v1/links</code> <code
><span class="text-light-green">POST</span>
/withdraw/api/v1/links</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code> <code
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> >{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}withdraw/api/v1/links -d '{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H "Content-type: application/json" -H "X-Api-Key: &lt;admin_key&gt;" <code
</code> >curl -X POST {{ request.url_root }}withdraw/api/v1/links -d
'{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Update a withdraw link"> <q-expansion-item
group="api"
dense
expand-separator
label="Update a withdraw link"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-blue">PUT</span> /withdraw/api/v1/links/&lt;withdraw_id&gt;</code> <code
><span class="text-light-blue">PUT</span>
/withdraw/api/v1/links/&lt;withdraw_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code> <code
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5> >{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X PUT {{ request.url_root }}withdraw/api/v1/links/&lt;withdraw_id&gt; -d '{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H "Content-type: application/json" -H "X-Api-Key: &lt;admin_key&gt;" <code
</code> >curl -X PUT {{ request.url_root
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -d '{"title":
&lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Delete a withdraw link" class="q-pb-md"> <q-expansion-item
group="api"
dense
expand-separator
label="Delete a withdraw link"
class="q-pb-md"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-light-green">DELETE</span> /withdraw/api/v1/links/&lt;withdraw_id&gt;</code> <code
><span class="text-light-green">DELETE</span>
/withdraw/api/v1/links/&lt;withdraw_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 NO_CONTENT</h5> <h5 class="text-caption q-mt-sm q-mb-none">Returns 201 NO_CONTENT</h5>
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X DELETE {{ request.url_root }}withdraw/api/v1/links/&lt;withdraw_id&gt; -H "X-Api-Key: &lt;admin_key&gt;" <code
</code> >curl -X DELETE {{ request.url_root
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -H "X-Api-Key:
&lt;admin_key&gt;"
</code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

31
lnbits/extensions/withdraw/templates/withdraw/_lnurl.html

@ -1,12 +1,29 @@
<q-expansion-item <q-expansion-item group="extras" icon="info" label="Powered by LNURL">
group="extras"
icon="info"
label="Powered by LNURL">
<q-card> <q-card>
<q-card-section> <q-card-section>
<p><b>WARNING: LNURL must be used over https or TOR</b><br/> LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL withdraw is the permission for someone to pull a certain amount of funds from a lightning wallet. In this extension time is also added - an amount can be withdraw over a period of time. A typical use case for an LNURL withdraw is a faucet, although it is a very powerful technology, with much further reaching implications. For example, an LNURL withdraw could be minted to pay for a subscription service.</p> <p>
<p>Exploring LNURL and finding use cases, is really helping inform lightning protocol development, rather than the protocol dictating how lightning-network should be engaged with.</p> <b>WARNING: LNURL must be used over https or TOR</b><br />
<small>Check <a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank">Awesome LNURL</a> for further information.</small> LNURL is a range of lightning-network standards that allow us to use
lightning-network differently. An LNURL withdraw is the permission for
someone to pull a certain amount of funds from a lightning wallet. In
this extension time is also added - an amount can be withdraw over a
period of time. A typical use case for an LNURL withdraw is a faucet,
although it is a very powerful technology, with much further reaching
implications. For example, an LNURL withdraw could be minted to pay for
a subscription service.
</p>
<p>
Exploring LNURL and finding use cases, is really helping inform
lightning protocol development, rather than the protocol dictating how
lightning-network should be engaged with.
</p>
<small
>Check
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
>Awesome LNURL</a
>
for further information.</small
>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

103
lnbits/extensions/withdraw/templates/withdraw/display.html

@ -1,52 +1,57 @@
{% extends "public.html" %} {% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
{% block page %} <q-card class="q-pa-lg">
<div class="row q-col-gutter-md justify-center"> <q-card-section class="q-pa-none">
<div class="col-12 col-sm-6 col-md-5 col-lg-4"> <div class="text-center">
<q-card class="q-pa-lg"> {% if link.is_spent %}
<q-card-section class="q-pa-none"> <q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
<div class="text-center"> {% endif %}
{% if link.is_spent %} <a href="lightning:{{ link.lnurl }}">
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge> <q-responsive :ratio="1" class="q-mx-md">
{% endif %} <qrcode
<a href="lightning:{{ link.lnurl }}"> value="{{ link.lnurl }}"
<q-responsive :ratio="1" class="q-mx-md"> :options="{width: 800}"
<qrcode value="{{ link.lnurl }}" :options="{width: 800}" class="rounded-borders"></qrcode> class="rounded-borders"
</q-responsive> ></qrcode>
</a> </q-responsive>
</div> </a>
<div class="row q-mt-lg"> </div>
<q-btn outline color="grey" @click="copyText('{{ link.lnurl }}')">Copy LNURL</q-btn> <div class="row q-mt-lg">
</div> <q-btn outline color="grey" @click="copyText('{{ link.lnurl }}')"
</q-card-section> >Copy LNURL</q-btn
</q-card> >
</div> </div>
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md"> </q-card-section>
<q-card> </q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits LNURL-withdraw link</h6>
<p class="q-my-none">Use a LNURL compatible bitcoin wallet to claim the sats.</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "withdraw/_lnurl.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
</div> </div>
{% endblock %} <div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
<q-card>
{% block scripts %} <q-card-section>
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> <h6 class="text-subtitle1 q-mb-sm q-mt-none">
<script> LNbits LNURL-withdraw link
Vue.component(VueQrcode.name, VueQrcode); </h6>
<p class="q-my-none">
Use a LNURL compatible bitcoin wallet to claim the sats.
</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "withdraw/_lnurl.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin] mixins: [windowMixin]
}); })
</script> </script>
{% endblock %} {% endblock %}

440
lnbits/extensions/withdraw/templates/withdraw/index.html

@ -1,148 +1,213 @@
{% extends "base.html" %} {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/withdraw/index.js',
'withdraw/js/index.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endblock %} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New withdraw link</q-btn
>
</q-card-section>
</q-card>
{% from "macros.jinja" import window_vars with context %} <q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
{% block scripts %} <div class="col">
{{ window_vars(user) }} <h5 class="text-subtitle1 q-my-none">Withdraw links</h5>
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> </div>
{% assets filters='rjsmin', output='__bundle__/withdraw/index.js', <div class="col-auto">
'withdraw/js/index.js' %} <q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% endblock %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true">New withdraw link</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Withdraw links</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div> </div>
<q-table dense flat </div>
:data="sortedWithdrawLinks" <q-table
row-key="id" dense
:columns="withdrawLinksTable.columns" flat
:pagination.sync="withdrawLinksTable.pagination"> :data="sortedWithdrawLinks"
{% raw %} row-key="id"
<template v-slot:header="props"> :columns="withdrawLinksTable.columns"
<q-tr :props="props"> :pagination.sync="withdrawLinksTable.pagination"
<q-th auto-width></q-th> >
<q-th {% raw %}
v-for="col in props.cols" <template v-slot:header="props">
:key="col.name" <q-tr :props="props">
:props="props" <q-th auto-width></q-th>
> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} {{ col.label }}
</q-th> </q-th>
<q-th auto-width></q-th> <q-th auto-width></q-th>
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <template v-slot:body="props">
<q-tr :props="props"> <q-tr :props="props">
<q-td auto-width> <q-td auto-width>
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.withdraw_url" target="_blank"></q-btn> <q-btn
<q-btn unelevated dense size="xs" icon="visibility" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" @click="openQrCodeDialog(props.row.id)"></q-btn> unelevated
</q-td> dense
<q-td size="xs"
v-for="col in props.cols" icon="launch"
:key="col.name" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
:props="props" type="a"
> :href="props.row.withdraw_url"
{{ col.value }} target="_blank"
</q-td> ></q-btn>
<q-td auto-width> <q-btn
<q-btn flat dense size="xs" @click="openUpdateDialog(props.row.id)" icon="edit" color="light-blue"></q-btn> unelevated
<q-btn flat dense size="xs" @click="deleteWithdrawLink(props.row.id)" icon="cancel" color="pink"></q-btn> dense
</q-td> size="xs"
</q-tr> icon="visibility"
</template> :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
{% endraw %} @click="openQrCodeDialog(props.row.id)"
</q-table> ></q-btn>
</q-card-section> </q-td>
</q-card> <q-td v-for="col in props.cols" :key="col.name" :props="props">
</div> {{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteWithdrawLink(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-5 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits LNURL-withdraw extension</h6> <h6 class="text-subtitle1 q-my-none">
</q-card-section> LNbits LNURL-withdraw extension
<q-card-section class="q-pa-none"> </h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "withdraw/_api_docs.html" %}
<q-separator></q-separator> <q-separator></q-separator>
<q-list> {% include "withdraw/_lnurl.html" %}
{% include "withdraw/_api_docs.html" %} </q-list>
<q-separator></q-separator> </q-card-section>
{% include "withdraw/_lnurl.html" %} </q-card>
</q-list> </div>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog"> <q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md"> <q-form @submit="sendFormData" class="q-gutter-md">
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *"> <q-select
</q-select> filled
<q-input filled dense dense
v-model.trim="formDialog.data.title" emit-value
type="text" v-model="formDialog.data.wallet"
label="Link title *"></q-input> :options="g.user.walletOptions"
<q-input filled dense label="Wallet *"
v-model.number="formDialog.data.min_withdrawable" >
type="number" </q-select>
label="Min withdrawable (sat) *"></q-input> <q-input
<q-input filled dense filled
v-model.number="formDialog.data.max_withdrawable" dense
type="number" v-model.trim="formDialog.data.title"
label="Max withdrawable (sat) *"></q-input> type="text"
<q-input filled dense label="Link title *"
v-model.number="formDialog.data.uses" ></q-input>
type="number" <q-input
:default="1" filled
label="Amount of uses *"></q-input> dense
<div class="row q-col-gutter-none"> v-model.number="formDialog.data.min_withdrawable"
<div class="col-8"> type="number"
<q-input filled dense label="Min withdrawable (sat) *"
v-model.number="formDialog.data.wait_time" ></q-input>
type="number" <q-input
:default="1" filled
label="Time between withdrawals *"> dense
</q-input> v-model.number="formDialog.data.max_withdrawable"
</div> type="number"
<div class="col-4 q-pl-xs"> label="Max withdrawable (sat) *"
<q-select filled dense v-model="formDialog.secondMultiplier" :options="formDialog.secondMultiplierOptions"> ></q-input>
</q-select> <q-input
</div> filled
dense
v-model.number="formDialog.data.uses"
type="number"
:default="1"
label="Amount of uses *"
></q-input>
<div class="row q-col-gutter-none">
<div class="col-8">
<q-input
filled
dense
v-model.number="formDialog.data.wait_time"
type="number"
:default="1"
label="Time between withdrawals *"
>
</q-input>
</div>
<div class="col-4 q-pl-xs">
<q-select
filled
dense
v-model="formDialog.secondMultiplier"
:options="formDialog.secondMultiplierOptions"
>
</q-select>
</div> </div>
<q-list> </div>
<q-item tag="label" class="rounded-borders"> <q-list>
<q-item-section avatar> <q-item tag="label" class="rounded-borders">
<q-checkbox v-model="formDialog.data.is_unique" color="deep-purple"></q-checkbox> <q-item-section avatar>
</q-item-section> <q-checkbox
<q-item-section> v-model="formDialog.data.is_unique"
<q-item-label>Use unique withdraw QR codes to reduce `assmilking`</q-item-label> color="deep-purple"
<q-item-label caption>This is recommended if you are sharing the links on social media. NOT if you plan to print QR codes.</q-item-label> ></q-checkbox>
</q-item-section> </q-item-section>
</q-item> <q-item-section>
</q-list> <q-item-label
<div class="row q-mt-lg"> >Use unique withdraw QR codes to reduce
<q-btn v-if="formDialog.data.id" unelevated color="deep-purple" type="submit">Update withdraw link</q-btn> `assmilking`</q-item-label
<q-btn v-else unelevated >
color="deep-purple" <q-item-label caption
:disable=" >This is recommended if you are sharing the links on social
media. NOT if you plan to print QR codes.</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
type="submit"
>Update withdraw link</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
:disable="
formDialog.data.wallet == null || formDialog.data.wallet == null ||
formDialog.data.title == null || formDialog.data.title == null ||
(formDialog.data.min_withdrawable == null || formDialog.data.min_withdrawable < 1) || (formDialog.data.min_withdrawable == null || formDialog.data.min_withdrawable < 1) ||
@ -153,34 +218,73 @@
) || ) ||
formDialog.data.uses == null || formDialog.data.uses == null ||
formDialog.data.wait_time == null" formDialog.data.wait_time == null"
type="submit">Create withdraw link</q-btn> type="submit"
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> >Create withdraw link</q-btn
</div> >
</q-form> <q-btn v-close-popup flat color="grey" class="q-ml-auto"
</q-card> >Cancel</q-btn
</q-dialog> >
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
{% raw %}
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode :value="qrCodeDialog.data.lnurl" :options="{width: 800}" class="rounded-borders"></qrcode>
</q-responsive>
<p style="word-break: break-all">
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br>
<strong>Unique:</strong> {{ qrCodeDialog.data.is_unique }}<span v-if="qrCodeDialog.data.is_unique" class="text-deep-purple"> (QR code will change after each withdrawal)</span><br>
<strong>Max. withdrawable:</strong> {{ qrCodeDialog.data.max_withdrawable }} sat<br>
<strong>Wait time:</strong> {{ qrCodeDialog.data.wait_time }} seconds<br>
<strong>Withdraws:</strong> {{ qrCodeDialog.data.used }} / {{ qrCodeDialog.data.uses }} <q-linear-progress :value="qrCodeDialog.data.used / qrCodeDialog.data.uses" color="deep-purple" class="q-mt-sm"></q-linear-progress>
</p>
{% endraw %}
<div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')" class="q-ml-sm">Copy LNURL</q-btn>
<q-btn outline color="grey" @click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')">Shareable link</q-btn>
<q-btn v-if="!qrCodeDialog.data.is_unique" outline color="grey" icon="print" type="a" :href="qrCodeDialog.data.print_url" target="_blank"></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-form>
</q-dialog> </q-card>
</div> </q-dialog>
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
{% raw %}
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="qrCodeDialog.data.lnurl"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<p style="word-break: break-all;">
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
<strong>Unique:</strong> {{ qrCodeDialog.data.is_unique }}<span
v-if="qrCodeDialog.data.is_unique"
class="text-deep-purple"
>
(QR code will change after each withdrawal)</span
><br />
<strong>Max. withdrawable:</strong> {{
qrCodeDialog.data.max_withdrawable }} sat<br />
<strong>Wait time:</strong> {{ qrCodeDialog.data.wait_time }} seconds<br />
<strong>Withdraws:</strong> {{ qrCodeDialog.data.used }} / {{
qrCodeDialog.data.uses }}
<q-linear-progress
:value="qrCodeDialog.data.used / qrCodeDialog.data.uses"
color="deep-purple"
class="q-mt-sm"
></q-linear-progress>
</p>
{% endraw %}
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')"
class="q-ml-sm"
>Copy LNURL</q-btn
>
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.withdraw_url, 'Link copied to clipboard!')"
>Shareable link</q-btn
>
<q-btn
v-if="!qrCodeDialog.data.is_unique"
outline
color="grey"
icon="print"
type="a"
:href="qrCodeDialog.data.print_url"
target="_blank"
></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% endblock %}

115
lnbits/extensions/withdraw/templates/withdraw/print_qr.html

@ -1,66 +1,59 @@
{% extends "print.html" %} {% extends "print.html" %} {% block page %}
<div class="row justify-center">
<div class="col-12 col-sm-8 col-lg-6 text-center">
{% block page %} {% for i in range(link.uses) %}
<div class="row justify-center"> <div class="zimbabwe">
<div class="col-12 col-sm-8 col-lg-6 text-center"> <div class="qr">
{% for i in range(link.uses) %} <qrcode value="{{ link.lnurl }}" :options="{width: 150}"></qrcode>
<div class="zimbabwe"> <br /><br />
<div class="qr"> <strong>{{ SITE_TITLE }}</strong><br />
<qrcode value="{{ link.lnurl }}" :options="{width: 150}"></qrcode> <strong>{{ link.max_withdrawable }} FREE SATS</strong><br />
<br><br> <small>Scan and follow link<br />or use Lightning wallet</small>
<strong>{{ SITE_TITLE }}</strong><br> </div>
<strong>{{ link.max_withdrawable }} FREE SATS</strong><br> <img src="{{ url_for('static', filename='images/note.jpg') }}" />
<small>Scan and follow link<br>or use Lightning wallet</small>
</div>
<img src="{{ url_for('static', filename='images/note.jpg') }}">
</div>
{% endfor %}
</div> </div>
{% endfor %}
</div> </div>
{% endblock %} </div>
{% endblock %} {% block styles %}
<style>
.zimbabwe {
page-break-inside: avoid;
height: 7cm;
width: 16cm;
position: relative;
margin-bottom: 10px;
overflow: hidden;
}
.zimbabwe img {
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
}
.zimbabwe .qr {
position: absolute;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
background: rgb(255, 255, 255, 0.7);
padding: 10px;
text-align: center;
line-height: 1.1;
}
</style>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
{% block styles %} new Vue({
<style> el: '#vue',
.zimbabwe { created: function () {
page-break-inside: avoid; window.print()
height: 7cm;
width: 16cm;
position: relative;
margin-bottom: 10px;
overflow: hidden;
}
.zimbabwe img {
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
} }
.zimbabwe .qr { })
position: absolute; </script>
top: 0;
bottom: 0;
right: 0;
z-index: 10;
background: rgb(255, 255, 255, 0.7);
padding: 10px;
text-align: center;
line-height: 1.1;
}
</style>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode);
new Vue({
el: '#vue',
created: function () {
window.print();
}
});
</script>
{% endblock %} {% endblock %}

244
lnbits/static/js/base.js

@ -1,6 +1,6 @@
var LOCALE = 'en'; var LOCALE = 'en'
var EventHub = new Vue(); var EventHub = new Vue()
var LNbits = { var LNbits = {
api: { api: {
@ -12,78 +12,97 @@ var LNbits = {
'X-Api-Key': apiKey 'X-Api-Key': apiKey
}, },
data: data data: data
}); })
}, },
createInvoice: function (wallet, amount, memo) { createInvoice: function (wallet, amount, memo) {
return this.request('post', '/api/v1/payments', wallet.inkey, { return this.request('post', '/api/v1/payments', wallet.inkey, {
out: false, out: false,
amount: amount, amount: amount,
memo: memo memo: memo
}); })
}, },
payInvoice: function (wallet, bolt11) { payInvoice: function (wallet, bolt11) {
return this.request('post', '/api/v1/payments', wallet.adminkey, { return this.request('post', '/api/v1/payments', wallet.adminkey, {
out: true, out: true,
bolt11: bolt11 bolt11: bolt11
}); })
}, },
getPayments: function (wallet, checkPending) { getPayments: function (wallet, checkPending) {
var query_param = (checkPending) ? '?check_pending' : ''; var query_param = checkPending ? '?check_pending' : ''
return this.request('get', ['/api/v1/payments', query_param].join(''), wallet.inkey); return this.request(
'get',
['/api/v1/payments', query_param].join(''),
wallet.inkey
)
}, },
getPayment: function (wallet, payhash) { getPayment: function (wallet, payhash) {
return this.request('get', '/api/v1/payments/' + payhash, wallet.inkey); return this.request('get', '/api/v1/payments/' + payhash, wallet.inkey)
} }
}, },
href: { href: {
createWallet: function (walletName, userId) { createWallet: function (walletName, userId) {
window.location.href = '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName; window.location.href =
'/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName
}, },
deleteWallet: function (walletId, userId) { deleteWallet: function (walletId, userId) {
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId; window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId
} }
}, },
map: { map: {
extension: function (data) { extension: function (data) {
var obj = _.object(['code', 'isValid', 'name', 'shortDescription', 'icon'], data); var obj = _.object(
obj.url = ['/', obj.code, '/'].join(''); ['code', 'isValid', 'name', 'shortDescription', 'icon'],
return obj; data
)
obj.url = ['/', obj.code, '/'].join('')
return obj
}, },
user: function (data) { user: function (data) {
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data); var obj = _.object(['id', 'email', 'extensions', 'wallets'], data)
var mapWallet = this.wallet; var mapWallet = this.wallet
obj.wallets = obj.wallets.map(function (obj) { obj.wallets = obj.wallets
return mapWallet(obj); .map(function (obj) {
}).sort(function (a, b) { return mapWallet(obj)
return a.name.localeCompare(b.name); })
}); .sort(function (a, b) {
return a.name.localeCompare(b.name)
})
obj.walletOptions = obj.wallets.map(function (obj) { obj.walletOptions = obj.wallets.map(function (obj) {
return { return {
label: [obj.name, ' - ', obj.id].join(''), label: [obj.name, ' - ', obj.id].join(''),
value: obj.id value: obj.id
}; }
}); })
return obj; return obj
}, },
wallet: function (data) { wallet: function (data) {
var obj = _.object(['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], data); var obj = _.object(
obj.msat = obj.balance; ['id', 'name', 'user', 'adminkey', 'inkey', 'balance'],
obj.sat = Math.round(obj.balance / 1000); data
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat); )
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join(''); obj.msat = obj.balance
return obj; obj.sat = Math.round(obj.balance / 1000)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('')
return obj
}, },
payment: function (data) { payment: function (data) {
var obj = _.object(['payhash', 'pending', 'amount', 'fee', 'memo', 'time'], data); var obj = _.object(
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm'); ['payhash', 'pending', 'amount', 'fee', 'memo', 'time'],
obj.msat = obj.amount; data
obj.sat = obj.msat / 1000; )
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat); obj.date = Quasar.utils.date.formatDate(
obj.isIn = obj.amount > 0; new Date(obj.time * 1000),
obj.isOut = obj.amount < 0; 'YYYY-MM-DD HH:mm'
obj.isPaid = obj.pending == 0; )
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase(); obj.msat = obj.amount
return obj; obj.sat = obj.msat / 1000
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
obj.isIn = obj.amount > 0
obj.isOut = obj.amount < 0
obj.isPaid = obj.pending == 0
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
return obj
} }
}, },
utils: { utils: {
@ -98,84 +117,99 @@ var LNbits = {
flat: true, flat: true,
color: 'grey' color: 'grey'
} }
}); })
}, },
formatCurrency: function (value, currency) { formatCurrency: function (value, currency) {
return new Intl.NumberFormat(LOCALE, {style: 'currency', currency: currency}).format(value); return new Intl.NumberFormat(LOCALE, {
style: 'currency',
currency: currency
}).format(value)
}, },
formatSat: function (value) { formatSat: function (value) {
return new Intl.NumberFormat(LOCALE).format(value); return new Intl.NumberFormat(LOCALE).format(value)
}, },
notifyApiError: function (error) { notifyApiError: function (error) {
var types = { var types = {
400: 'warning', 400: 'warning',
401: 'warning', 401: 'warning',
500: 'negative' 500: 'negative'
}; }
Quasar.plugins.Notify.create({ Quasar.plugins.Notify.create({
timeout: 5000, timeout: 5000,
type: types[error.response.status] || 'warning', type: types[error.response.status] || 'warning',
message: error.response.data.message || null, message: error.response.data.message || null,
caption: [error.response.status, ' ', error.response.statusText].join('').toUpperCase() || null, caption:
[error.response.status, ' ', error.response.statusText]
.join('')
.toUpperCase() || null,
icon: null icon: null
}); })
}, },
search: function (data, q, field, separator) { search: function (data, q, field, separator) {
var field = field || '_q'; var field = field || '_q'
try { try {
var queries = q.toLowerCase().split(separator || ' '); var queries = q.toLowerCase().split(separator || ' ')
return data.filter(function (obj) { return data.filter(function (obj) {
var matches = 0; var matches = 0
_.each(queries, function (q) { _.each(queries, function (q) {
if (obj[field].indexOf(q) !== -1) matches++; if (obj[field].indexOf(q) !== -1) matches++
}); })
return matches == queries.length; return matches == queries.length
}); })
} catch (err) { } catch (err) {
return data; return data
} }
}, },
exportCSV: function (columns, data) { exportCSV: function (columns, data) {
var wrapCsvValue = function(val, formatFn) { var wrapCsvValue = function (val, formatFn) {
var formatted = formatFn !== void 0 var formatted = formatFn !== void 0 ? formatFn(val) : val
? formatFn(val)
: val;
formatted = (formatted === void 0 || formatted === null) formatted =
? '' formatted === void 0 || formatted === null ? '' : String(formatted)
: String(formatted);
formatted = formatted.split('"').join('""'); formatted = formatted.split('"').join('""')
return `"${formatted}"`; return `"${formatted}"`
} }
var content = [columns.map(function (col) { var content = [
return wrapCsvValue(col.label); columns.map(function (col) {
})].concat(data.map(function (row) { return wrapCsvValue(col.label)
return columns.map(function (col) { })
return wrapCsvValue( ]
(typeof col.field === 'function') .concat(
? col.field(row) data.map(function (row) {
: row[(col.field === void 0) ? col.name : col.field], return columns
col.format .map(function (col) {
); return wrapCsvValue(
}).join(','); typeof col.field === 'function'
})).join('\r\n'); ? col.field(row)
: row[col.field === void 0 ? col.name : col.field],
col.format
)
})
.join(',')
})
)
.join('\r\n')
var status = Quasar.utils.exportFile('table-export.csv', content, 'text/csv'); var status = Quasar.utils.exportFile(
'table-export.csv',
content,
'text/csv'
)
if (status !== true) { if (status !== true) {
Quasar.plugins.Notify.create({ Quasar.plugins.Notify.create({
message: 'Browser denied file download...', message: 'Browser denied file download...',
color: 'negative', color: 'negative',
icon: null icon: null
}); })
} }
} }
} }
}; }
var windowMixin = { var windowMixin = {
data: function () { data: function () {
@ -185,44 +219,52 @@ var windowMixin = {
extensions: [], extensions: [],
user: null, user: null,
wallet: null, wallet: null,
payments: [], payments: []
} }
}; }
}, },
methods: { methods: {
toggleDarkMode: function () { toggleDarkMode: function () {
this.$q.dark.toggle(); this.$q.dark.toggle()
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive); this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
}, },
copyText: function (text, message, position) { copyText: function (text, message, position) {
var notify = this.$q.notify; var notify = this.$q.notify
Quasar.utils.copyToClipboard(text).then(function () { Quasar.utils.copyToClipboard(text).then(function () {
notify({message: message || 'Copied to clipboard!', position: position || 'bottom'}); notify({
}); message: message || 'Copied to clipboard!',
position: position || 'bottom'
})
})
} }
}, },
created: function () { created: function () {
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode')); this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
if (window.user) { if (window.user) {
this.g.user = Object.freeze(LNbits.map.user(window.user)); this.g.user = Object.freeze(LNbits.map.user(window.user))
} }
if (window.wallet) { if (window.wallet) {
this.g.wallet = Object.freeze(LNbits.map.wallet(window.wallet)); this.g.wallet = Object.freeze(LNbits.map.wallet(window.wallet))
} }
if (window.extensions) { if (window.extensions) {
var user = this.g.user; var user = this.g.user
this.g.extensions = Object.freeze(window.extensions.map(function (data) { this.g.extensions = Object.freeze(
return LNbits.map.extension(data); window.extensions
}).map(function (obj) { .map(function (data) {
if (user) { return LNbits.map.extension(data)
obj.isEnabled = user.extensions.indexOf(obj.code) != -1; })
} else { .map(function (obj) {
obj.isEnabled = false; if (user) {
} obj.isEnabled = user.extensions.indexOf(obj.code) != -1
return obj; } else {
}).sort(function (a, b) { obj.isEnabled = false
return a.name > b.name; }
})); return obj
})
.sort(function (a, b) {
return a.name > b.name
})
)
} }
} }
}; }

65
lnbits/static/js/components.js

@ -8,10 +8,10 @@ Vue.component('lnbits-fsat', {
template: '<span>{{ fsat }}</span>', template: '<span>{{ fsat }}</span>',
computed: { computed: {
fsat: function () { fsat: function () {
return LNbits.utils.formatSat(this.amount); return LNbits.utils.formatSat(this.amount)
} }
} }
}); })
Vue.component('lnbits-wallet-list', { Vue.component('lnbits-wallet-list', {
data: function () { data: function () {
@ -70,33 +70,34 @@ Vue.component('lnbits-wallet-list', {
`, `,
computed: { computed: {
wallets: function () { wallets: function () {
var bal = this.activeBalance; var bal = this.activeBalance
return this.user.wallets.map(function (obj) { return this.user.wallets.map(function (obj) {
obj.live_fsat = (bal.length && bal[0] == obj.id) obj.live_fsat =
? LNbits.utils.formatSat(bal[1]) bal.length && bal[0] == obj.id
: obj.fsat; ? LNbits.utils.formatSat(bal[1])
return obj; : obj.fsat
}); return obj
})
} }
}, },
methods: { methods: {
createWallet: function () { createWallet: function () {
LNbits.href.createWallet(this.walletName, this.user.id); LNbits.href.createWallet(this.walletName, this.user.id)
}, },
updateWalletBalance: function (payload) { updateWalletBalance: function (payload) {
this.activeBalance = payload; this.activeBalance = payload
} }
}, },
created: function () { created: function () {
if (window.user) { if (window.user) {
this.user = LNbits.map.user(window.user); this.user = LNbits.map.user(window.user)
} }
if (window.wallet) { if (window.wallet) {
this.activeWallet = LNbits.map.wallet(window.wallet); this.activeWallet = LNbits.map.wallet(window.wallet)
} }
EventHub.$on('update-wallet-balance', this.updateWalletBalance); EventHub.$on('update-wallet-balance', this.updateWalletBalance)
} }
}); })
Vue.component('lnbits-extension-list', { Vue.component('lnbits-extension-list', {
data: function () { data: function () {
@ -140,30 +141,34 @@ Vue.component('lnbits-extension-list', {
`, `,
computed: { computed: {
userExtensions: function () { userExtensions: function () {
if (!this.user) return []; if (!this.user) return []
var path = window.location.pathname; var path = window.location.pathname
var userExtensions = this.user.extensions; var userExtensions = this.user.extensions
return this.extensions.filter(function (obj) { return this.extensions
return userExtensions.indexOf(obj.code) !== -1; .filter(function (obj) {
}).map(function (obj) { return userExtensions.indexOf(obj.code) !== -1
obj.isActive = path.startsWith(obj.url); })
return obj; .map(function (obj) {
}); obj.isActive = path.startsWith(obj.url)
return obj
})
} }
}, },
created: function () { created: function () {
if (window.extensions) { if (window.extensions) {
this.extensions = window.extensions.map(function (data) { this.extensions = window.extensions
return LNbits.map.extension(data); .map(function (data) {
}).sort(function (a, b) { return LNbits.map.extension(data)
return a.name.localeCompare(b.name); })
}); .sort(function (a, b) {
return a.name.localeCompare(b.name)
})
} }
if (window.user) { if (window.user) {
this.user = LNbits.map.user(window.user); this.user = LNbits.map.user(window.user)
} }
} }
}); })

5
package.json

@ -0,0 +1,5 @@
{
"devDependencies": {
"prettier": "^2.0.5"
}
}
Loading…
Cancel
Save