Browse Source

add a dialog with payment details for each payment.

for outgoing payments this needs a preimage to be good,
but we don't have it yet because we don't get it from
backends.
aiosqlite
fiatjaf 4 years ago
parent
commit
ce28db76c9
  1. 104
      lnbits/core/static/js/wallet.js
  2. 75
      lnbits/core/templates/core/wallet.html
  3. 9
      lnbits/extensions/lnticket/templates/lnticket/display.html
  4. 9
      lnbits/static/css/base.css
  5. 68
      lnbits/static/js/base.js

104
lnbits/core/static/js/wallet.js

@ -14,10 +14,10 @@ function generateChart(canvas, payments) {
}
_.each(
payments.slice(0).sort(function(a, b) {
payments.slice(0).sort(function (a, b) {
return a.time - b.time
}),
function(tx) {
function (tx) {
txs.push({
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
sat: tx.sat
@ -25,17 +25,17 @@ function generateChart(canvas, payments) {
}
)
_.each(_.groupBy(txs, 'hour'), function(value, day) {
_.each(_.groupBy(txs, 'hour'), function (value, day) {
var income = _.reduce(
value,
function(memo, tx) {
function (memo, tx) {
return tx.sat >= 0 ? memo + tx.sat : memo
},
0
)
var outcome = _.reduce(
value,
function(memo, tx) {
function (memo, tx) {
return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo
},
0
@ -67,20 +67,14 @@ function generateChart(canvas, payments) {
type: 'bar',
label: 'in',
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,
type: 'bar',
label: 'out',
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
}
]
},
@ -121,7 +115,7 @@ function generateChart(canvas, payments) {
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function() {
data: function () {
return {
user: LNbits.map.user(window.user),
receive: {
@ -183,49 +177,49 @@ new Vue({
}
},
computed: {
filteredPayments: function() {
filteredPayments: function () {
var q = this.paymentsTable.filter
if (!q || q === '') return this.payments
return LNbits.utils.search(this.payments, q)
},
balance: function() {
balance: function () {
if (this.payments.length) {
return (
_.pluck(this.payments, 'amount').reduce(function(a, b) {
_.pluck(this.payments, 'amount').reduce(function (a, b) {
return a + b
}, 0) / 1000
)
}
return this.g.wallet.sat
},
fbalance: function() {
fbalance: function () {
return LNbits.utils.formatSat(this.balance)
},
canPay: function() {
canPay: function () {
if (!this.send.invoice) return false
return this.send.invoice.sat <= this.balance
},
pendingPaymentsExist: function() {
pendingPaymentsExist: function () {
return this.payments
? _.where(this.payments, {pending: 1}).length > 0
: false
}
},
methods: {
closeCamera: function() {
closeCamera: function () {
this.sendCamera.show = false
},
showCamera: function() {
showCamera: function () {
this.sendCamera.show = true
},
showChart: function() {
showChart: function () {
this.paymentsChart.show = true
this.$nextTick(function() {
this.$nextTick(function () {
generateChart(this.$refs.canvas, this.payments)
})
},
showReceiveDialog: function() {
showReceiveDialog: function () {
this.receive = {
show: true,
status: 'pending',
@ -237,7 +231,7 @@ new Vue({
paymentChecker: null
}
},
showSendDialog: function() {
showSendDialog: function () {
this.send = {
show: true,
invoice: null,
@ -247,20 +241,20 @@ new Vue({
paymentChecker: null
}
},
closeReceiveDialog: function() {
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
setTimeout(function() {
setTimeout(function () {
clearInterval(checker)
}, 10000)
},
closeSendDialog: function() {
closeSendDialog: function () {
this.sendCamera.show = false
var checker = this.send.paymentChecker
setTimeout(function() {
setTimeout(function () {
clearInterval(checker)
}, 1000)
},
createInvoice: function() {
createInvoice: function () {
var self = this
this.receive.status = 'loading'
LNbits.api
@ -269,14 +263,14 @@ new Vue({
this.receive.data.amount,
this.receive.data.memo
)
.then(function(response) {
.then(function (response) {
self.receive.status = 'success'
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.payment_hash)
.then(function(response) {
.then(function (response) {
if (response.data.paid) {
self.fetchPayments()
self.receive.show = false
@ -285,17 +279,17 @@ new Vue({
})
}, 2000)
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
self.receive.status = 'pending'
})
},
decodeQR: function(res) {
decodeQR: function (res) {
this.send.data.bolt11 = res
this.decodeInvoice()
this.sendCamera.show = false
},
decodeInvoice: function() {
decodeInvoice: function () {
if (this.send.data.bolt11.startsWith('lightning:')) {
this.send.data.bolt11 = this.send.data.bolt11.slice(10)
}
@ -320,7 +314,7 @@ new Vue({
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 (tag.description === 'payment_hash') {
cleanInvoice.hash = tag.value
@ -341,7 +335,7 @@ new Vue({
this.send.invoice = Object.freeze(cleanInvoice)
},
payInvoice: function() {
payInvoice: function () {
var self = this
let dismissPaymentMsg = this.$q.notify({
@ -352,11 +346,11 @@ new Vue({
LNbits.api
.payInvoice(this.g.wallet, this.send.data.bolt11)
.then(function(response) {
self.send.paymentChecker = setInterval(function() {
.then(function (response) {
self.send.paymentChecker = setInterval(function () {
LNbits.api
.getPayment(self.g.wallet, response.data.payment_hash)
.then(function(res) {
.then(function (res) {
if (res.data.paid) {
self.send.show = false
clearInterval(self.send.paymentChecker)
@ -366,58 +360,58 @@ new Vue({
})
}, 2000)
})
.catch(function(error) {
.catch(function (error) {
dismissPaymentMsg()
LNbits.utils.notifyApiError(error)
})
},
deleteWallet: function(walletId, user) {
deleteWallet: function (walletId, user) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet?')
.onOk(function() {
.onOk(function () {
LNbits.href.deleteWallet(walletId, user)
})
},
fetchPayments: function(checkPending) {
fetchPayments: function (checkPending) {
var self = this
return LNbits.api
.getPayments(this.g.wallet, checkPending)
.then(function(response) {
.then(function (response) {
self.payments = response.data
.map(function(obj) {
.map(function (obj) {
return LNbits.map.payment(obj)
})
.sort(function(a, b) {
.sort(function (a, b) {
return b.time - a.time
})
})
},
checkPendingPayments: function() {
checkPendingPayments: function () {
var dismissMsg = this.$q.notify({
timeout: 0,
message: 'Checking pending transactions...',
icon: null
})
this.fetchPayments(true).then(function() {
this.fetchPayments(true).then(function () {
dismissMsg()
})
},
exportCSV: function() {
exportCSV: function () {
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments)
}
},
watch: {
payments: function() {
payments: function () {
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance])
}
},
created: function() {
created: function () {
this.fetchPayments()
setTimeout(this.checkPendingPayments(), 1200)
},
mounted: function() {
mounted: function () {
if (
this.$refs.disclaimer &&
!this.$q.localStorage.getItem('lnbits.disclaimerShown')

75
lnbits/core/templates/core/wallet.html

@ -103,16 +103,25 @@
<q-icon
v-if="props.row.isPaid"
size="14px"
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'"
:color="(props.row.sat < 0) ? 'pink' : 'green'"
:name="props.row.isOut ? 'call_made' : 'call_received'"
:color="props.row.isOut ? 'pink' : 'green'"
@click="props.expand = !props.expand"
></q-icon>
<q-icon v-else name="settings_ethernet" color="grey">
<q-icon
v-else
name="settings_ethernet"
color="grey"
@click="props.expand = !props.expand"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
</q-td>
<q-td key="memo" :props="props">
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a class="inherit" :href="['/', props.row.tag, '?usr=', user.id].join('')">
<a
class="inherit"
:href="['/', props.row.tag, '?usr=', user.id].join('')"
>
#{{ props.row.tag }}
</a>
</q-badge>
@ -125,6 +134,64 @@
{{ props.row.fsat }}
</q-td>
</q-tr>
<q-dialog v-model="props.expand" :props="props">
<q-card
v-if="props.row.amount > 0 && props.row.pending"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Close</q-btn
>
</div>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<div v-if="props.row.isPaid && props.row.isIn">
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
Payment Received
</div>
<div v-else-if="props.row.isPaid && props.row.isOut">
<q-icon
size="18px"
:name="'call_made'"
:color="'pink'"
></q-icon>
Payment Sent
</div>
<div v-else>
<q-icon name="settings_ethernet" color="grey"></q-icon>
Outgoing payment pending
</div>
<q-tooltip>Payment Hash</q-tooltip>
<div class="text-wrap mono q-pa-md">
{{ props.row.payment_hash }}
</div>
</div>
</q-card>
</q-dialog>
</template>
{% endraw %}
</q-table>

9
lnbits/extensions/lnticket/templates/lnticket/display.html

@ -137,15 +137,12 @@
Invoice: function () {
var self = this
axios
.post(
'/lnticket/api/v1/tickets/{{ form_id }}',
{
.post('/lnticket/api/v1/tickets/{{ form_id }}', {
form: '{{ form_id }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email,
ltext: self.formDialog.data.text,
}
)
ltext: self.formDialog.data.text
})
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash

9
lnbits/static/css/base.css

@ -66,3 +66,12 @@ a.inherit {
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale; }
.text-wrap {
word-wrap: break-word;
word-break: break-all;
}
.mono {
font-family: monospace;
}

68
lnbits/static/js/base.js

@ -6,7 +6,7 @@ var EventHub = new Vue()
var LNbits = {
api: {
request: function(method, url, apiKey, data) {
request: function (method, url, apiKey, data) {
return axios({
method: method,
url: url,
@ -16,20 +16,20 @@ var LNbits = {
data: data
})
},
createInvoice: function(wallet, amount, memo) {
createInvoice: function (wallet, amount, memo) {
return this.request('post', '/api/v1/payments', wallet.inkey, {
out: false,
amount: amount,
memo: memo
})
},
payInvoice: function(wallet, bolt11) {
payInvoice: function (wallet, bolt11) {
return this.request('post', '/api/v1/payments', wallet.adminkey, {
out: true,
bolt11: bolt11
})
},
getPayments: function(wallet, checkPending) {
getPayments: function (wallet, checkPending) {
var query_param = checkPending ? '?check_pending' : ''
return this.request(
'get',
@ -37,7 +37,7 @@ var LNbits = {
wallet.inkey
)
},
getPayment: function(wallet, paymentHash) {
getPayment: function (wallet, paymentHash) {
return this.request(
'get',
'/api/v1/payments/' + paymentHash,
@ -46,16 +46,16 @@ var LNbits = {
}
},
href: {
createWallet: function(walletName, userId) {
createWallet: function (walletName, userId) {
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
}
},
map: {
extension: function(data) {
extension: function (data) {
var obj = _.object(
['code', 'isValid', 'name', 'shortDescription', 'icon'],
data
@ -63,17 +63,17 @@ var LNbits = {
obj.url = ['/', obj.code, '/'].join('')
return obj
},
user: function(data) {
user: function (data) {
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data)
var mapWallet = this.wallet
obj.wallets = obj.wallets
.map(function(obj) {
.map(function (obj) {
return mapWallet(obj)
})
.sort(function(a, b) {
.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 {
label: [obj.name, ' - ', obj.id].join(''),
value: obj.id
@ -81,7 +81,7 @@ var LNbits = {
})
return obj
},
wallet: function(data) {
wallet: function (data) {
var obj = _.object(
['id', 'name', 'user', 'adminkey', 'inkey', 'balance'],
data
@ -92,7 +92,7 @@ var LNbits = {
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('')
return obj
},
payment: function(data) {
payment: function (data) {
var obj = _.object(
[
'checking_id',
@ -124,7 +124,7 @@ var LNbits = {
}
},
utils: {
confirmDialog: function(msg) {
confirmDialog: function (msg) {
return Quasar.plugins.Dialog.create({
message: msg,
ok: {
@ -137,16 +137,16 @@ var LNbits = {
}
})
},
formatCurrency: function(value, currency) {
formatCurrency: function (value, currency) {
return new Intl.NumberFormat(LOCALE, {
style: 'currency',
currency: currency
}).format(value)
},
formatSat: function(value) {
formatSat: function (value) {
return new Intl.NumberFormat(LOCALE).format(value)
},
notifyApiError: function(error) {
notifyApiError: function (error) {
var types = {
400: 'warning',
401: 'warning',
@ -163,12 +163,12 @@ var LNbits = {
icon: null
})
},
search: function(data, q, field, separator) {
search: function (data, q, field, separator) {
try {
var queries = q.toLowerCase().split(separator || ' ')
return data.filter(function(obj) {
return data.filter(function (obj) {
var matches = 0
_.each(queries, function(q) {
_.each(queries, function (q) {
if (obj[field].indexOf(q) !== -1) matches++
})
return matches === queries.length
@ -177,8 +177,8 @@ var LNbits = {
return data
}
},
exportCSV: function(columns, data) {
var wrapCsvValue = function(val, formatFn) {
exportCSV: function (columns, data) {
var wrapCsvValue = function (val, formatFn) {
var formatted = formatFn !== void 0 ? formatFn(val) : val
formatted =
@ -190,14 +190,14 @@ var LNbits = {
}
var content = [
columns.map(function(col) {
columns.map(function (col) {
return wrapCsvValue(col.label)
})
]
.concat(
data.map(function(row) {
data.map(function (row) {
return columns
.map(function(col) {
.map(function (col) {
return wrapCsvValue(
typeof col.field === 'function'
? col.field(row)
@ -228,7 +228,7 @@ var LNbits = {
}
var windowMixin = {
data: function() {
data: function () {
return {
g: {
visibleDrawer: false,
@ -240,13 +240,13 @@ var windowMixin = {
}
},
methods: {
toggleDarkMode: function() {
toggleDarkMode: function () {
this.$q.dark.toggle()
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
Quasar.utils.copyToClipboard(text).then(function() {
Quasar.utils.copyToClipboard(text).then(function () {
notify({
message: message || 'Copied to clipboard!',
position: position || 'bottom'
@ -254,7 +254,7 @@ var windowMixin = {
})
}
},
created: function() {
created: function () {
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
if (window.user) {
this.g.user = Object.freeze(LNbits.map.user(window.user))
@ -266,10 +266,10 @@ var windowMixin = {
var user = this.g.user
this.g.extensions = Object.freeze(
window.extensions
.map(function(data) {
.map(function (data) {
return LNbits.map.extension(data)
})
.map(function(obj) {
.map(function (obj) {
if (user) {
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
} else {
@ -277,7 +277,7 @@ var windowMixin = {
}
return obj
})
.sort(function(a, b) {
.sort(function (a, b) {
return a.name > b.name
})
)

Loading…
Cancel
Save