Browse Source

refactor(tpos): migrate extension

fee_issues
Eneko Illarramendi 5 years ago
parent
commit
a455379ee1
  1. 1
      lnbits/__init__.py
  2. 4
      lnbits/extensions/tpos/config.json
  3. 6
      lnbits/extensions/tpos/migrations.py
  4. 76
      lnbits/extensions/tpos/templates/tpos/index.html
  5. 426
      lnbits/extensions/tpos/templates/tpos/tpos.html
  6. 11
      lnbits/extensions/tpos/views.py
  7. 47
      lnbits/extensions/tpos/views_api.py
  8. 12
      lnbits/templates/base.html

1
lnbits/__init__.py

@ -30,6 +30,7 @@ Talisman(
"cdnjs.cloudflare.com", "cdnjs.cloudflare.com",
"code.ionicframework.com", "code.ionicframework.com",
"code.jquery.com", "code.jquery.com",
"api.opennode.co",
"fonts.googleapis.com", "fonts.googleapis.com",
"fonts.gstatic.com", "fonts.gstatic.com",
"maxcdn.bootstrapcdn.com", "maxcdn.bootstrapcdn.com",

4
lnbits/extensions/tpos/config.json

@ -1,6 +1,6 @@
{ {
"name": "TPOS", "name": "TPoS",
"short_description": "A shareable POS!", "short_description": "A shareable PoS terminal!",
"icon": "dialpad", "icon": "dialpad",
"contributors": ["talvasconcelos", "arcbtc"] "contributors": ["talvasconcelos", "arcbtc"]
} }

6
lnbits/extensions/tpos/migrations.py

@ -5,14 +5,16 @@ def m001_initial(db):
""" """
Initial tposs table. Initial tposs table.
""" """
db.execute(""" db.execute(
"""
CREATE TABLE IF NOT EXISTS tposs ( CREATE TABLE IF NOT EXISTS tposs (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
currency TEXT NOT NULL currency TEXT NOT NULL
); );
""") """
)
def migrate(): def migrate():

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

@ -8,7 +8,7 @@
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> <div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<q-btn unelevated color="deep-purple" @click="tposDialog.show = true">New TPoS</q-btn> <q-btn unelevated color="deep-purple" @click="formDialog.show = true">New TPoS</q-btn>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -31,7 +31,6 @@
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th <q-th
v-for="col in props.cols" v-for="col in props.cols"
:key="col.name" :key="col.name"
@ -39,16 +38,14 @@
> >
{{ 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="vpn_lock" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.tpos" target="_blank"></q-btn> <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>
<q-td <q-td
v-for="col in props.cols" v-for="col in props.cols"
@ -78,45 +75,41 @@
<q-list> <q-list>
<q-expansion-item <q-expansion-item
group="extras" group="extras"
icon="swap_vertical_circle" icon="info"
label="Info" label="Info">
:content-inset-level="0.5"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<h5 class="text-subtitle1 q-my-none">Tiago's Point of Sale</h5> <h5 class="text-subtitle1 q-mt-none q-mb-sm">Tiago's Point of Sale</h5>
<p>Is an instant and secure point of sale terminal. A merchant can use the PoS to accept payments to their LNbits wallet, without having to expose the wallet itself, which means it can even be shared with others. <br/>For example, a merchant could setup multiple TPoSs for each one of their colleagues, and use TPoS to track which colleague has taken sales. The terminal is optimized for mobile use, and is easily shared as a QR code, via the # share function on the TPoS. <br/> <p>Is an instant and secure point of sale terminal. A merchant can use the PoS to accept payments to their LNbits wallet, without having to expose the wallet itself, which means it can even be shared with others.</p>
<small> Created by, <a href="https://github.com/talvasconcelos">Tiago Vasconcelos</a></small></p> <p>For example, a merchant could setup multiple TPoSs for each one of their colleagues, and use TPoS to track which colleague has taken sales. The terminal is optimized for mobile use, and is easily shared as a QR code, via the "#" share function on the TPoS.</p>
</q-card> <small>Created by <a href="https://github.com/talvasconcelos" target="_blank">Tiago Vasconcelos</a>.</small>
</q-card-section> </q-card-section>
</q-card>
</q-expansion-item> </q-expansion-item>
</q-list> </q-list>
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
<q-dialog v-model="tposDialog.show" position="top"> <q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px"> <q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
<q-input filled dense <q-input filled dense
v-model.trim="tposDialog.data.name" v-model.trim="formDialog.data.name"
label="Name" label="Name"
placeholder="Tiago's PoS"></q-input> placeholder="Tiago's PoS"></q-input>
<q-select filled dense
<q-select filled dense emit-value v-model="tposDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *"> emit-value v-model="formDialog.data.wallet"
</q-select> :options="g.user.walletOptions"
label="Wallet *"></q-select>
<q-select filled dense emit-value v-model="tposDialog.data.currency" :options="currencyOptions" label="Currency *"> <q-select filled dense
</q-select> emit-value v-model="formDialog.data.currency"
:options="currencyOptions"
label="Currency *"></q-select>
<q-btn unelevated <q-btn unelevated
color="deep-purple" color="deep-purple"
:disable="tposDialog.data.currency == null || tposDialog.data.name == null" :disable="formDialog.data.currency == null || formDialog.data.name == null"
@click="createTPoS">Create TPoS</q-btn> @click="createTPoS">Create TPoS</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form> </q-form>
</q-card> </q-card>
@ -141,9 +134,13 @@
return { return {
tposs: [], tposs: [],
currencyOptions: [ currencyOptions: [
'USD', 'EUR', 'GBP', 'DZD', 'ARP', 'AUD', 'ATS', 'BSD', 'BBD', 'BEF', 'BMD', 'BRL', 'BGL', 'CAD', 'CLP', 'CNY', 'CYP', 'CSK', 'DKK', 'NLG', 'XCD', 'EGP', 'FJD', 'FIM', 'FRF', 'DEM', 'XAU', 'GRD', 'HKD', 'HUF', 'ISK', 'INR', 'IDR', 'IEP', 'ILS', 'ITL', 'JMD', 'JPY', 'JOD', 'KRW', 'LBP', 'LUF', 'MYR', 'MXP', 'NLG', 'NZD', 'NOK', 'PKR', 'XPD', 'PHP', 'XPT', 'PLZ', 'PTE', 'ROL', 'RUR', 'SAR', 'XAG', 'SGD', 'SKK', 'ZAR', 'KRW', 'ESP', 'XDR', 'SDD', 'SEK', 'CHF', 'TWD', 'THB', 'TTD', 'TRL', 'VEB', 'ZMK', 'EUR', 'XCD', 'XDR', 'XAG', 'XAU', 'XPD', 'XPT', 'USD', 'EUR', 'GBP', 'DZD', 'ARP', 'AUD', 'ATS', 'BSD', 'BBD', 'BEF', 'BMD', 'BRL', 'BGL', 'CAD', 'CLP',
'CNY', 'CYP', 'CSK', 'DKK', 'NLG', 'XCD', 'EGP', 'FJD', 'FIM', 'FRF', 'DEM', 'XAU', 'GRD', 'HKD', 'HUF',
'ISK', 'INR', 'IDR', 'IEP', 'ILS', 'ITL', 'JMD', 'JPY', 'JOD', 'KRW', 'LBP', 'LUF', 'MYR', 'MXP', 'NLG',
'NZD', 'NOK', 'PKR', 'XPD', 'PHP', 'XPT', 'PLZ', 'PTE', 'ROL', 'RUR', 'SAR', 'XAG', 'SGD', 'SKK', 'ZAR',
'KRW', 'ESP', 'XDR', 'SDD', 'SEK', 'CHF', 'TWD', 'THB', 'TTD', 'TRL', 'VEB', 'ZMK', 'EUR', 'XCD', 'XDR',
'XAG', 'XAU', 'XPD', 'XPT'
], ],
tpossTable: { tpossTable: {
columns: [ columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'}, {name: 'id', align: 'left', label: 'ID', field: 'id'},
@ -154,13 +151,16 @@
rowsPerPage: 10 rowsPerPage: 10
} }
}, },
tposDialog: { formDialog: {
show: false, show: false,
data: {} data: {}
} }
}; };
}, },
methods: { methods: {
closeFormDialog: function () {
this.formDialog.data = {};
},
getTPoSs: function () { getTPoSs: function () {
var self = this; var self = this;
@ -170,29 +170,25 @@
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) {
console.log(obj)
return mapTPoS(obj); return mapTPoS(obj);
}); });
}); });
}, },
createTPoS: function () { createTPoS: function () {
var data = { var data = {
name: this.tposDialog.data.name, name: this.formDialog.data.name,
currency: this.tposDialog.data.currency currency: this.formDialog.data.currency
}; };
var self = this; var self = this;
console.log(this.tposDialog.data.wallet);
LNbits.api.request( LNbits.api.request(
'POST', 'POST',
'/tpos/api/v1/tposs', '/tpos/api/v1/tposs',
_.findWhere(this.g.user.wallets, {id: this.tposDialog.data.wallet}).inkey, _.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey,
data data
).then(function (response) { ).then(function (response) {
self.tposs.push(mapTPoS(response.data)); self.tposs.push(mapTPoS(response.data));
self.tposDialog.show = false; self.formDialog.show = false;
self.tposDialog.data = {};
}).catch(function (error) { }).catch(function (error) {
LNbits.utils.notifyApiError(error); LNbits.utils.notifyApiError(error);
}); });
@ -202,7 +198,7 @@
var tpos = _.findWhere(this.tposs, {id: tposId}); var tpos = _.findWhere(this.tposs, {id: tposId});
this.$q.dialog({ this.$q.dialog({
message: 'Are you sure you want to delete this TPoS link?', message: 'Are you sure you want to delete this TPoS?',
ok: { ok: {
flat: true, flat: true,
color: 'orange' color: 'orange'
@ -235,5 +231,3 @@
}); });
</script> </script>
{% endblock %} {% endblock %}

426
lnbits/extensions/tpos/templates/tpos/tpos.html

File diff suppressed because one or more lines are too long

11
lnbits/extensions/tpos/views.py

@ -1,8 +1,5 @@
import requests
from flask import g, abort, render_template from flask import g, abort, render_template
from lnbits.core.crud import get_wallet
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.helpers import Status from lnbits.helpers import Status
@ -19,8 +16,6 @@ def index():
@tpos_ext.route("/<tpos_id>") @tpos_ext.route("/<tpos_id>")
def tpos(tpos_id): def tpos(tpos_id):
tpos = get_tpos(tpos_id) or abort(Status.NOT_FOUND, "tpos does not exist.") tpos = get_tpos(tpos_id) or abort(Status.NOT_FOUND, "TPoS does not exist.")
r = requests.get("https://api.opennode.co/v1/rates")
r_json = r.json() return render_template("tpos/tpos.html", tpos=tpos)
rr = get_wallet(tpos.wallet)
return render_template("tpos/tpos.html", tpos=tpos.id, inkey=rr.inkey, rate=r_json["data"]["BTC" + tpos.currency][tpos.currency], curr=tpos.currency)

47
lnbits/extensions/tpos/views_api.py

@ -1,6 +1,6 @@
from flask import g, jsonify, request from flask import g, jsonify, request
from lnbits.core.crud import get_user from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status from lnbits.helpers import Status
@ -30,9 +30,7 @@ def api_tposs():
} }
) )
def api_tpos_create(): def api_tpos_create():
print("poo") tpos = create_tpos(wallet_id=g.wallet.id, **g.data)
tpos = create_tpos(wallet_id=g.wallet.id, name=g.data["name"], currency=g.data["currency"])
return jsonify(tpos._asdict()), Status.CREATED return jsonify(tpos._asdict()), Status.CREATED
@ -50,31 +48,44 @@ def api_tpos_delete(tpos_id):
delete_tpos(tpos_id) delete_tpos(tpos_id)
return '', Status.NO_CONTENT return "", Status.NO_CONTENT
@tpos_ext.route("/api/v1/tposs/invoice/<tpos_id>", methods=["POST"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/", methods=["POST"])
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}}) @api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
def api_tpos_create_invoice(tpos_id): def api_tpos_create_invoice(tpos_id):
tpos = get_tpos(tpos_id) tpos = get_tpos(tpos_id)
if not tpos: if not tpos:
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
try:
memo = f"TPoS {tpos_id}"
checking_id, payment_request = create_invoice(wallet_id=tpos.wallet, amount=g.data["amount"], memo=memo)
try:
checking_id, payment_request = create_invoice(
wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}"
)
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.OK return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.CREATED
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<checking_id>", methods=["GET"])
def api_tpos_check_invoice(tpos_id, checking_id):
tpos = get_tpos(tpos_id)
if not tpos:
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
try:
is_paid = not WALLET.get_invoice_status(checking_id).pending
except Exception:
return jsonify({"paid": False}), Status.OK
if is_paid:
wallet = get_wallet(tpos.wallet)
payment = wallet.get_payment(checking_id)
payment.set_pending(False)
@tpos_ext.route("/api/v1/tposs/invoice/<checking_id>", methods=["GET"]) return jsonify({"paid": True}), Status.OK
def api_tpos_check_invoice(checking_id):
print(checking_id)
PAID = WALLET.get_invoice_status(checking_id).paid
if PAID == True: return jsonify({"paid": False}), Status.OK
return jsonify({"PAID": True}), Status.OK
return jsonify({"PAID": False}), Status.OK

12
lnbits/templates/base.html

@ -26,11 +26,15 @@
{% block drawer_toggle %} {% block drawer_toggle %}
<q-btn dense flat round icon="menu" @click="g.visibleDrawer = !g.visibleDrawer"></q-btn> <q-btn dense flat round icon="menu" @click="g.visibleDrawer = !g.visibleDrawer"></q-btn>
{% endblock %} {% endblock %}
<q-toolbar-title>
{% block toolbar_title %}
{% if SITE_TITLE != 'LNbits' %} {% if SITE_TITLE != 'LNbits' %}
<q-toolbar-title>{{ SITE_TITLE }}</q-toolbar-title> {{ SITE_TITLE }}
{% else %} {% else %}
<q-toolbar-title><strong>LN</strong>bits</q-toolbar-title> <strong>LN</strong>bits
{% endif %} {% endif %}
{% endblock %}
</q-toolbar-title>
{% block beta %} {% block beta %}
<q-badge color="yellow" text-color="black"> <q-badge color="yellow" text-color="black">
<span><span v-show="$q.screen.gt.sm">USE WITH CAUTION - LNbits wallet is still in </span>BETA</span> <span><span v-show="$q.screen.gt.sm">USE WITH CAUTION - LNbits wallet is still in </span>BETA</span>
@ -49,12 +53,15 @@
</q-drawer> </q-drawer>
{% endblock %} {% endblock %}
{% block page_container %}
<q-page-container> <q-page-container>
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}"> <q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
{% block page %}{% endblock %} {% block page %}{% endblock %}
</q-page> </q-page>
</q-page-container> </q-page-container>
{% endblock %}
{% block footer %}
<q-footer class="bg-transparent q-px-lg q-py-md" :class="{'text-dark': !$q.dark.isActive}"> <q-footer class="bg-transparent q-px-lg q-py-md" :class="{'text-dark': !$q.dark.isActive}">
<q-toolbar> <q-toolbar>
<q-toolbar-title class="text-caption"> <q-toolbar-title class="text-caption">
@ -66,6 +73,7 @@
</q-btn> </q-btn>
</q-toolbar> </q-toolbar>
</q-footer> </q-footer>
{% endblock %}
</q-layout> </q-layout>

Loading…
Cancel
Save