539 lines
15 KiB
HTML
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 %}
|