Files
lnbits/lnbits/extensions/tpos/templates/tpos/tpos.html
T

539 lines
15 KiB
HTML

{% extends "public.html" %} {% block toolbar_title %} {{ tpos.name }}
<q-btn
flat
dense
size="md"
@click.prevent="urlDialog.show = true"
icon="share"
color="white"
></q-btn>
{% endblock %} {% block footer %}{% endblock %} {% block page_container %}
<q-page-container>
<q-page>
<q-page-sticky v-if="exchangeRate" expand position="top">
<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 q-mb-sm">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
</div>
</div>
</q-page-sticky>
<q-page-sticky expand position="bottom">
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<div class="keypad q-pa-sm">
<q-btn
unelevated
@click="stack.push(1)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>1</q-btn
>
<q-btn
unelevated
@click="stack.push(2)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>2</q-btn
>
<q-btn
unelevated
@click="stack.push(3)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>3</q-btn
>
<q-btn
unelevated
@click="stack.push(4)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>4</q-btn
>
<q-btn
unelevated
@click="stack.push(5)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>5</q-btn
>
<q-btn
unelevated
@click="stack.push(6)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>6</q-btn
>
<q-btn
unelevated
@click="stack.push(7)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>7</q-btn
>
<q-btn
unelevated
@click="stack.push(8)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>8</q-btn
>
<q-btn
unelevated
@click="stack.push(9)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>9</q-btn
>
<q-btn
unelevated
@click="stack.splice(-1, 1)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>DEL</q-btn
>
<q-btn
unelevated
@click="stack.push(0)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
>0</q-btn
>
<q-btn
unelevated
@click="stack = []"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
class="btn-cancel"
>C</q-btn
>
<q-btn
unelevated
:disabled="amount == 0"
@click="submitForm()"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
class="btn-confirm"
>OK</q-btn
>
</div>
</div>
</div>
</q-page-sticky>
<q-dialog
v-model="invoiceDialog.show"
position="top"
@hide="closeInvoiceDialog"
>
<q-card
v-if="invoiceDialog.data"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="invoiceDialog.data.payment_request"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="text-center">
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}
<small>sat</small>
<span v-show="tip_options" style="font-size: 0.75rem"
>( + {{ tipAmountSat }} tip)</span
>
{% endraw %}
</h5>
<q-btn
outline
color="grey"
icon="nfc"
@click="readNfcTag()"
:disable="nfcTagReading"
></q-btn>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(invoiceDialog.data.payment_request)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="tipDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-xl">
<b style="font-size: 24px">Would you like to leave a tip?</b>
</div>
<div class="text-center q-mb-xl">
<q-btn
style="padding: 10px; margin: 3px"
unelevated
@click="processTipSelection(tip)"
size="xl"
:outline="!($q.dark.isActive)"
rounded
color="primary"
v-for="tip in this.tip_options"
:key="tip"
>{% raw %}{{ tip }}{% endraw %}%</q-btn
>
</div>
<div class="text-center q-mb-xl">
<p><a @click="processTipSelection(0)"> No, thanks</a></p>
</div>
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="urlDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
value="{{ request.url }}"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="text-center q-mb-xl">
<p style="word-break: break-all">
<strong>{{ tpos.name }}</strong><br />{{ request.url }}
</p>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')"
>Copy URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="complete.show" position="top">
<q-icon
name="check"
transition-show="fade"
class="text-light-green"
style="font-size: min(90vw, 40em)"
></q-icon>
</q-dialog>
</q-page>
</q-page-container>
{% endblock %} {% block styles %}
<style>
* {
touch-action: manipulation;
}
.keypad {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.keypad .btn {
height: 100%;
}
.keypad .btn-confirm {
grid-area: 1 / 4 / 5 / 4;
}
</style>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}',
tip_options: null,
exchangeRate: null,
stack: [],
tipAmount: 0.0,
hasNFC: false,
nfcTagReading: false,
invoiceDialog: {
show: false,
data: null,
dismissMsg: null,
paymentChecker: null
},
tipDialog: {
show: false
},
urlDialog: {
show: false
},
complete: {
show: false
}
}
},
computed: {
amount: function () {
if (!this.stack.length) return 0.0
return (Number(this.stack.join('')) / 100).toFixed(2)
},
famount: function () {
return LNbits.utils.formatCurrency(this.amount, this.currency)
},
sat: function () {
if (!this.exchangeRate) return 0
return Math.ceil(
((this.amount - this.tipAmount) / this.exchangeRate) * 100000000
)
},
tipAmountSat: function () {
if (!this.exchangeRate) return 0
return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000)
},
fsat: function () {
return LNbits.utils.formatSat(this.sat)
}
},
methods: {
closeInvoiceDialog: function () {
this.stack = []
this.tipAmount = 0.0
var dialog = this.invoiceDialog
setTimeout(function () {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
}, 3000)
},
processTipSelection: function (selectedTipOption) {
this.tipDialog.show = false
if (selectedTipOption) {
const tipAmount = parseFloat(
parseFloat((selectedTipOption / 100) * this.amount)
)
const subtotal = parseFloat(this.amount)
const grandTotal = parseFloat((tipAmount + subtotal).toFixed(2))
const totalString = grandTotal.toFixed(2).toString()
this.stack = []
for (var i = 0; i < totalString.length; i++) {
const char = totalString[i]
if (char !== '.') {
this.stack.push(char)
}
}
this.tipAmount = tipAmount
}
this.showInvoice()
},
submitForm: function () {
if (this.tip_options && this.tip_options.length) {
this.showTipModal()
} else {
this.showInvoice()
}
},
showTipModal: function () {
this.tipDialog.show = true
},
showInvoice: function () {
var self = this
var dialog = this.invoiceDialog
axios
.post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, {
params: {
amount: this.sat,
tipAmount: this.tipAmountSat
}
})
.then(function (response) {
dialog.data = response.data
dialog.show = true
dialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
dialog.paymentChecker = setInterval(function () {
axios
.get(
'/tpos/api/v1/tposs/' +
self.tposId +
'/invoices/' +
response.data.payment_hash
)
.then(function (res) {
if (res.data.paid) {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
dialog.show = false
self.complete.show = true
}
})
}, 3000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
readNfcTag: function () {
try {
const self = this
if (typeof NDEFReader == 'undefined') {
throw {
toString: function () {
return 'NFC not supported on this device or browser.'
}
}
}
const ndef = new NDEFReader()
const readerAbortController = new AbortController()
readerAbortController.signal.onabort = event => {
console.log('All NFC Read operations have been aborted.')
}
this.nfcTagReading = true
this.$q.notify({
message: 'Tap your NFC tag to pay this invoice with LNURLw.'
})
return ndef.scan({signal: readerAbortController.signal}).then(() => {
ndef.onreadingerror = () => {
self.nfcTagReading = false
this.$q.notify({
type: 'negative',
message: 'There was an error reading this NFC tag.'
})
readerAbortController.abort()
}
ndef.onreading = ({message}) => {
//Decode NDEF data from tag
const textDecoder = new TextDecoder('utf-8')
const record = message.records.find(el => {
const payload = textDecoder.decode(el.data)
return payload.toUpperCase().indexOf('LNURL') !== -1
})
const lnurl = textDecoder.decode(record.data)
//User feedback, show loader icon
self.nfcTagReading = false
self.payInvoice(lnurl, readerAbortController)
this.$q.notify({
type: 'positive',
message: 'NFC tag read successfully.'
})
}
})
} catch (error) {
this.nfcTagReading = false
this.$q.notify({
type: 'negative',
message: error
? error.toString()
: 'An unexpected error has occurred.'
})
}
},
payInvoice: function (lnurl, readerAbortController) {
const self = this
return axios
.post(
'/tpos/api/v1/tposs/' +
self.tposId +
'/invoices/' +
self.invoiceDialog.data.payment_request +
'/pay',
{
lnurl: lnurl
}
)
.then(response => {
if (!response.data.success) {
this.$q.notify({
type: 'negative',
message: response.data.detail
})
}
readerAbortController.abort()
})
},
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 () {
var getRates = this.getRates
getRates()
this.tip_options =
'{{ tpos.tip_options | tojson }}' == 'null'
? null
: JSON.parse('{{ tpos.tip_options }}')
setInterval(function () {
getRates()
}, 120000)
}
})
</script>
{% endblock %}