mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-20 18:51:05 +01:00
commit
50edc5cd5a
15
lnbits/extensions/tpos/README.md
Normal file
15
lnbits/extensions/tpos/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# TPoS
|
||||
|
||||
## A Shareable PoS (Point of Sale) that doesn't need to be installed and can run in the browser!
|
||||
|
||||
An easy, fast and secure way to accept Bitcoin, over Lightning Network, at your business. The PoS is isolated from the wallet, so it's safe for any employee to use. You can create as many TPOS's as you need, for example one for each employee, or one for each branch of your business.
|
||||
|
||||
### Usage
|
||||
|
||||
1. Enable extension
|
||||
2. Create a TPOS\
|
||||
![create](https://imgur.com/8jNj8Zq.jpg)
|
||||
3. Open TPOS on the browser\
|
||||
![open](https://imgur.com/LZuoWzb.jpg)
|
||||
4. Present invoice QR to costumer\
|
||||
![pay](https://imgur.com/tOwxn77.jpg)
|
12
lnbits/extensions/tpos/__init__.py
Normal file
12
lnbits/extensions/tpos/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
from quart import Blueprint
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_tpos")
|
||||
|
||||
tpos_ext: Blueprint = Blueprint(
|
||||
"tpos", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
6
lnbits/extensions/tpos/config.json
Normal file
6
lnbits/extensions/tpos/config.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "TPoS",
|
||||
"short_description": "A shareable PoS terminal!",
|
||||
"icon": "dialpad",
|
||||
"contributors": ["talvasconcelos", "arcbtc"]
|
||||
}
|
42
lnbits/extensions/tpos/crud.py
Normal file
42
lnbits/extensions/tpos/crud.py
Normal file
@ -0,0 +1,42 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import TPoS
|
||||
|
||||
|
||||
async def create_tpos(*, wallet_id: str, name: str, currency: str) -> TPoS:
|
||||
tpos_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO tpos.tposs (id, wallet, name, currency)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(tpos_id, wallet_id, name, currency),
|
||||
)
|
||||
|
||||
tpos = await get_tpos(tpos_id)
|
||||
assert tpos, "Newly created tpos couldn't be retrieved"
|
||||
return tpos
|
||||
|
||||
|
||||
async def get_tpos(tpos_id: str) -> Optional[TPoS]:
|
||||
row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,))
|
||||
return TPoS.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
|
||||
return [TPoS.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def delete_tpos(tpos_id: str) -> None:
|
||||
await db.execute("DELETE FROM tpos.tposs WHERE id = ?", (tpos_id,))
|
14
lnbits/extensions/tpos/migrations.py
Normal file
14
lnbits/extensions/tpos/migrations.py
Normal file
@ -0,0 +1,14 @@
|
||||
async def m001_initial(db):
|
||||
"""
|
||||
Initial tposs table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE tpos.tposs (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
currency TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
14
lnbits/extensions/tpos/models.py
Normal file
14
lnbits/extensions/tpos/models.py
Normal file
@ -0,0 +1,14 @@
|
||||
from sqlite3 import Row
|
||||
from pydantic import BaseModel
|
||||
#from typing import NamedTuple
|
||||
|
||||
|
||||
class TPoS(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
currency: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "TPoS":
|
||||
return cls(**dict(row))
|
78
lnbits/extensions/tpos/templates/tpos/_api_docs.html
Normal file
78
lnbits/extensions/tpos/templates/tpos/_api_docs.html
Normal file
@ -0,0 +1,78 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="List TPoS">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">GET</span> /tpos/api/v1/tposs</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</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">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<tpos_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/tposs -H "X-Api-Key:
|
||||
<invoice_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create a TPoS">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-green">POST</span> /tpos/api/v1/tposs</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"name": <string>, "currency": <string*ie USD*>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"currency": <string>, "id": <string>, "name":
|
||||
<string>, "wallet": <string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/tposs -d '{"name":
|
||||
<string>, "currency": <string>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: <admin_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a TPoS"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/tpos/api/v1/tposs/<tpos_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root }}api/v1/tposs/<tpos_id> -H
|
||||
"X-Api-Key: <admin_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
18
lnbits/extensions/tpos/templates/tpos/_tpos.html
Normal file
18
lnbits/extensions/tpos/templates/tpos/_tpos.html
Normal file
@ -0,0 +1,18 @@
|
||||
<q-expansion-item group="extras" icon="info" label="About TPoS">
|
||||
<q-card>
|
||||
<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>
|
||||
<small
|
||||
>Created by
|
||||
<a href="https://github.com/talvasconcelos" target="_blank"
|
||||
>Tiago Vasconcelos</a
|
||||
>.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
423
lnbits/extensions/tpos/templates/tpos/index.html
Normal file
423
lnbits/extensions/tpos/templates/tpos/index.html
Normal file
@ -0,0 +1,423 @@
|
||||
{% 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="primary" @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>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tposs"
|
||||
row-key="id"
|
||||
:columns="tpossTable.columns"
|
||||
:pagination.sync="tpossTable.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.tpos"
|
||||
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="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">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} TPoS extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "tpos/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
{% include "tpos/_tpos.html" %}
|
||||
</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="primary"
|
||||
: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({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tposs: [],
|
||||
currencyOptions: [
|
||||
'USD',
|
||||
'EUR',
|
||||
'GBP',
|
||||
'AED',
|
||||
'AFN',
|
||||
'ALL',
|
||||
'AMD',
|
||||
'ANG',
|
||||
'AOA',
|
||||
'ARS',
|
||||
'AUD',
|
||||
'AWG',
|
||||
'AZN',
|
||||
'BAM',
|
||||
'BBD',
|
||||
'BDT',
|
||||
'BGN',
|
||||
'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'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {}
|
||||
},
|
||||
getTPoSs: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/tpos/api/v1/tposs?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tposs = response.data.map(function (obj) {
|
||||
return mapTPoS(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
createTPoS: function () {
|
||||
var data = {
|
||||
name: this.formDialog.data.name,
|
||||
currency: this.formDialog.data.currency
|
||||
}
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/tpos/api/v1/tposs',
|
||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
|
||||
.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tposs.push(mapTPoS(response.data))
|
||||
self.formDialog.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteTPoS: function (tposId) {
|
||||
var self = this
|
||||
var tpos = _.findWhere(this.tposs, {id: tposId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this TPoS?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/tpos/api/v1/tposs/' + tposId,
|
||||
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tposs = _.reject(self.tposs, function (obj) {
|
||||
return obj.id == tposId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTPoSs()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
264
lnbits/extensions/tpos/templates/tpos/tpos.html
Normal file
264
lnbits/extensions/tpos/templates/tpos/tpos.html
Normal file
@ -0,0 +1,264 @@
|
||||
{% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% 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">
|
||||
{% 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" color="grey-8"
|
||||
>1</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(2)" size="xl" color="grey-8"
|
||||
>2</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(3)" size="xl" color="grey-8"
|
||||
>3</q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="stack = []"
|
||||
size="xl"
|
||||
color="pink"
|
||||
class="btn-cancel"
|
||||
>C</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(4)" size="xl" color="grey-8"
|
||||
>4</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(5)" size="xl" color="grey-8"
|
||||
>5</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(6)" size="xl" color="grey-8"
|
||||
>6</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(7)" size="xl" color="grey-8"
|
||||
>7</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(8)" size="xl" color="grey-8"
|
||||
>8</q-btn
|
||||
>
|
||||
<q-btn unelevated @click="stack.push(9)" size="xl" color="grey-8"
|
||||
>9</q-btn
|
||||
>
|
||||
<q-btn
|
||||
unelevated
|
||||
:disabled="amount == 0"
|
||||
@click="showInvoice()"
|
||||
size="xl"
|
||||
color="green"
|
||||
class="btn-confirm"
|
||||
>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>
|
||||
</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 }}{% endraw %} <small>sat</small>
|
||||
</h5>
|
||||
</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-page>
|
||||
</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>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tposId: '{{ tpos.id }}',
|
||||
currency: '{{ tpos.currency }}',
|
||||
exchangeRate: null,
|
||||
stack: [],
|
||||
invoiceDialog: {
|
||||
show: false,
|
||||
data: null,
|
||||
dismissMsg: null,
|
||||
paymentChecker: null
|
||||
},
|
||||
urlDialog: {
|
||||
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.exchangeRate) * 100000000)
|
||||
},
|
||||
fsat: function () {
|
||||
return LNbits.utils.formatSat(this.sat)
|
||||
}
|
||||
},
|
||||
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('/tpos/api/v1/tposs/' + this.tposId + '/invoices/', {
|
||||
amount: this.sat
|
||||
})
|
||||
.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.$q.notify({
|
||||
type: 'positive',
|
||||
message: self.fsat + ' sat received!',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
})
|
||||
}, 3000)
|
||||
})
|
||||
.catch(function (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 () {
|
||||
var getRates = this.getRates
|
||||
getRates()
|
||||
setInterval(function () {
|
||||
getRates()
|
||||
}, 20000)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
23
lnbits/extensions/tpos/views.py
Normal file
23
lnbits/extensions/tpos/views.py
Normal file
@ -0,0 +1,23 @@
|
||||
from quart import g, abort, render_template
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
|
||||
from . import tpos_ext
|
||||
from .crud import get_tpos
|
||||
|
||||
|
||||
@tpos_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("tpos/index.html", user=g.user)
|
||||
|
||||
|
||||
@tpos_ext.route("/<tpos_id>")
|
||||
async def tpos(tpos_id):
|
||||
tpos = await get_tpos(tpos_id)
|
||||
if not tpos:
|
||||
abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.")
|
||||
|
||||
return await render_template("tpos/tpos.html", tpos=tpos)
|
103
lnbits/extensions/tpos/views_api.py
Normal file
103
lnbits/extensions/tpos/views_api.py
Normal file
@ -0,0 +1,103 @@
|
||||
from quart import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice, check_invoice_status
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from . import tpos_ext
|
||||
from .crud import create_tpos, get_tpos, get_tposs, delete_tpos
|
||||
from .models import TPoS
|
||||
|
||||
|
||||
@tpos_ext.get("/api/v1/tposs")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_tposs(all_wallets: boolean = Query(None)):
|
||||
wallet_ids = [g.wallet.id]
|
||||
if all_wallets:
|
||||
wallet_ids = wallet_ids = (await get_user(g.wallet.user)).wallet_ids(await get_user(g.wallet.user)).wallet_ids
|
||||
# if "all_wallets" in request.args:
|
||||
# wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
|
||||
return [tpos._asdict() for tpos in await get_tposs(wallet_ids)], HTTPStatus.OK
|
||||
|
||||
|
||||
@tpos_ext.post("/api/v1/tposs")
|
||||
@api_check_wallet_key("invoice")
|
||||
# @api_validate_post_request(
|
||||
# schema={
|
||||
# "name": {"type": "string", "empty": False, "required": True},
|
||||
# "currency": {"type": "string", "empty": False, "required": True},
|
||||
# }
|
||||
# )
|
||||
async def api_tpos_create(name: str = Query(...), currency: str = Query(...)):
|
||||
tpos = await create_tpos(wallet_id=g.wallet.id, **g.data)
|
||||
return tpos._asdict(), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@tpos_ext.delete("/api/v1/tposs/{tpos_id}")
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_tpos_delete(tpos_id: str):
|
||||
tpos = await get_tpos(tpos_id)
|
||||
|
||||
if not tpos:
|
||||
return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND
|
||||
|
||||
if tpos.wallet != g.wallet.id:
|
||||
return {"message": "Not your TPoS."}, HTTPStatus.FORBIDDEN
|
||||
|
||||
await delete_tpos(tpos_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices/")
|
||||
# @api_validate_post_request(
|
||||
# schema={"amount": {"type": "integer", "min": 1, "required": True}}
|
||||
# )
|
||||
async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tpos_id: str):
|
||||
tpos = await get_tpos(tpos_id)
|
||||
|
||||
if not tpos:
|
||||
return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND
|
||||
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=tpos.wallet,
|
||||
amount=amount,
|
||||
memo=f"{tpos.name}",
|
||||
extra={"tag": "tpos"},
|
||||
)
|
||||
except Exception as e:
|
||||
return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return {"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices/{payment_hash}")
|
||||
async def api_tpos_check_invoice(tpos_id: str, payment_hash: str):
|
||||
tpos = await get_tpos(tpos_id)
|
||||
|
||||
if not tpos:
|
||||
return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND
|
||||
|
||||
try:
|
||||
status = await check_invoice_status(tpos.wallet, payment_hash)
|
||||
is_paid = not status.pending
|
||||
except Exception as exc:
|
||||
print(exc)
|
||||
return {"paid": False}, HTTPStatus.OK
|
||||
|
||||
if is_paid:
|
||||
wallet = await get_wallet(tpos.wallet)
|
||||
payment = await wallet.get_payment(payment_hash)
|
||||
await payment.set_pending(False)
|
||||
|
||||
return {"paid": True}, HTTPStatus.OK
|
||||
|
||||
return {"paid": False}, HTTPStatus.OK
|
Loading…
Reference in New Issue
Block a user