diff --git a/lnbits/extensions/tpos/__init__.py b/lnbits/extensions/tpos/__init__.py index c62981d72..f4bfc7908 100644 --- a/lnbits/extensions/tpos/__init__.py +++ b/lnbits/extensions/tpos/__init__.py @@ -1,7 +1,10 @@ +import asyncio + from fastapi import APIRouter from lnbits.db import Database from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_tpos") @@ -11,6 +14,10 @@ tpos_ext: APIRouter = APIRouter(prefix="/tpos", tags=["TPoS"]) def tpos_renderer(): return template_renderer(["lnbits/extensions/tpos/templates"]) - +from .tasks import wait_for_paid_invoices from .views_api import * # noqa from .views import * # noqa + +def tpos_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/tpos/config.json b/lnbits/extensions/tpos/config.json index c5789afb7..3bd1a71a4 100644 --- a/lnbits/extensions/tpos/config.json +++ b/lnbits/extensions/tpos/config.json @@ -2,5 +2,5 @@ "name": "TPoS", "short_description": "A shareable PoS terminal!", "icon": "dialpad", - "contributors": ["talvasconcelos", "arcbtc"] + "contributors": ["talvasconcelos", "arcbtc", "leesalminen"] } diff --git a/lnbits/extensions/tpos/crud.py b/lnbits/extensions/tpos/crud.py index 1a1987696..5f0cf96ec 100644 --- a/lnbits/extensions/tpos/crud.py +++ b/lnbits/extensions/tpos/crud.py @@ -10,10 +10,10 @@ async def create_tpos(wallet_id: str, data: CreateTposData) -> TPoS: tpos_id = urlsafe_short_hash() await db.execute( """ - INSERT INTO tpos.tposs (id, wallet, name, currency) - VALUES (?, ?, ?, ?) + INSERT INTO tpos.tposs (id, wallet, name, currency, tip_options, tip_wallet) + VALUES (?, ?, ?, ?, ?, ?) """, - (tpos_id, wallet_id, data.name, data.currency), + (tpos_id, wallet_id, data.name, data.currency, data.tip_options, data.tip_wallet), ) tpos = await get_tpos(tpos_id) diff --git a/lnbits/extensions/tpos/migrations.py b/lnbits/extensions/tpos/migrations.py index 7a7fff0d5..396bd7f27 100644 --- a/lnbits/extensions/tpos/migrations.py +++ b/lnbits/extensions/tpos/migrations.py @@ -12,3 +12,23 @@ async def m001_initial(db): ); """ ) + +async def m002_addtip_wallet(db): + """ + Add tips to tposs table + """ + await db.execute( + """ + ALTER TABLE tpos.tposs ADD tip_wallet TEXT NULL; + """ + ) + +async def m003_addtip_options(db): + """ + Add tips to tposs table + """ + await db.execute( + """ + ALTER TABLE tpos.tposs ADD tip_options TEXT NULL; + """ + ) \ No newline at end of file diff --git a/lnbits/extensions/tpos/models.py b/lnbits/extensions/tpos/models.py index 653a055ca..6a2ff1d2c 100644 --- a/lnbits/extensions/tpos/models.py +++ b/lnbits/extensions/tpos/models.py @@ -6,6 +6,8 @@ from pydantic import BaseModel class CreateTposData(BaseModel): name: str currency: str + tip_options: str + tip_wallet: str class TPoS(BaseModel): @@ -13,6 +15,8 @@ class TPoS(BaseModel): wallet: str name: str currency: str + tip_options: str + tip_wallet: str @classmethod def from_row(cls, row: Row) -> "TPoS": diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py new file mode 100644 index 000000000..baa8e6c7d --- /dev/null +++ b/lnbits/extensions/tpos/tasks.py @@ -0,0 +1,70 @@ +import asyncio +import json + +from lnbits.core import db as core_db +from lnbits.core.crud import create_payment +from lnbits.core.models import Payment +from lnbits.helpers import urlsafe_short_hash +from lnbits.tasks import internal_invoice_queue, register_invoice_listener + +from .crud import get_tpos + + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if "tpos" == payment.extra.get("tag") and payment.extra.get("tipSplitted"): + # already splitted, ignore + return + + # now we make some special internal transfers (from no one to the receiver) + tpos = await get_tpos(payment.extra.get("tposId")) + + tipAmount = payment.extra.get("tipAmount") + + if tipAmount is None: + #no tip amount + return + + tipAmount = tipAmount * 1000 + + # mark the original payment with one extra key, "splitted" + # (this prevents us from doing this process again and it's informative) + # and reduce it by the amount we're going to send to the producer + await core_db.execute( + """ + UPDATE apipayments + SET extra = ?, amount = amount - ? + WHERE hash = ? + AND checking_id NOT LIKE 'internal_%' + """, + ( + json.dumps(dict(**payment.extra, tipSplitted=True)), + tipAmount, + payment.payment_hash, + ), + ) + + # perform the internal transfer using the same payment_hash + internal_checking_id = f"internal_{urlsafe_short_hash()}" + await create_payment( + wallet_id=tpos.tip_wallet, + checking_id=internal_checking_id, + payment_request="", + payment_hash=payment.payment_hash, + amount=tipAmount, + memo=payment.memo, + pending=False, + extra={"tipSplitted": True}, + ) + + # manually send this for now + await internal_invoice_queue.put(internal_checking_id) + return diff --git a/lnbits/extensions/tpos/templates/tpos/index.html b/lnbits/extensions/tpos/templates/tpos/index.html index a8971211a..af3b0573f 100644 --- a/lnbits/extensions/tpos/templates/tpos/index.html +++ b/lnbits/extensions/tpos/templates/tpos/index.html @@ -54,7 +54,7 @@ > - {{ col.value }} + {{ (col.name == 'tip_options' ? JSON.parse(col.value).join(", ") : col.value) }} + +
parseInt(str))) : JSON.stringify([])), + tip_wallet: this.formDialog.data.tip_wallet || "", } var self = this diff --git a/lnbits/extensions/tpos/templates/tpos/tpos.html b/lnbits/extensions/tpos/templates/tpos/tpos.html index 49d88140f..e4ea14992 100644 --- a/lnbits/extensions/tpos/templates/tpos/tpos.html +++ b/lnbits/extensions/tpos/templates/tpos/tpos.html @@ -1,5 +1,17 @@ -{% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% endblock -%} {% block footer %}{% endblock %} {% block page_container %} + +{% extends "public.html" %} +{% block toolbar_title %} +{{ tpos.name }} + +{% endblock %} +{% block footer %}{% endblock %} {% block page_container %} @@ -43,16 +55,6 @@ color="primary" >3 - C 9 - OK #C + OK
@@ -176,6 +179,38 @@ + + + +
+ Would you like to leave a tip? +
+
+ {% raw %}{{ tip }}{% endraw %}% +
+ +
+ Close +
+
+
+ @@ -214,6 +249,10 @@ {% endblock %} {% block styles %} {% endblock %} {% block scripts %} @@ -241,14 +279,19 @@ return { tposId: '{{ tpos.id }}', currency: '{{ tpos.currency }}', + tip_options: JSON.parse('{{ tpos.tip_options }}'), exchangeRate: null, stack: [], + tipAmount: 0.00, invoiceDialog: { show: false, data: null, dismissMsg: null, paymentChecker: null }, + tipDialog: { + show: false, + }, urlDialog: { show: false }, @@ -269,6 +312,10 @@ if (!this.exchangeRate) return 0 return Math.ceil((this.amount / this.exchangeRate) * 100000000) }, + tipAmountSat: function () { + if (!this.exchangeRate) return 0 + return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000) + }, fsat: function () { console.log('sat', this.sat, LNbits.utils.formatSat(this.sat)) return LNbits.utils.formatSat(this.sat) @@ -277,12 +324,46 @@ methods: { closeInvoiceDialog: function () { this.stack = [] + this.tipAmount = 0.00 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.length) { + this.showTipModal() + } else { + this.showInvoice() + } + }, + showTipModal: function() { + this.tipDialog.show = true + }, showInvoice: function () { var self = this var dialog = this.invoiceDialog @@ -290,7 +371,8 @@ axios .post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, { params: { - amount: this.sat + amount: this.sat, + tipAmount: this.tipAmountSat, } }) .then(function (response) { diff --git a/lnbits/extensions/tpos/views.py b/lnbits/extensions/tpos/views.py index 2d78ecce1..dbee84341 100644 --- a/lnbits/extensions/tpos/views.py +++ b/lnbits/extensions/tpos/views.py @@ -8,6 +8,10 @@ from starlette.responses import HTMLResponse from lnbits.core.models import User from lnbits.decorators import check_user_exists +from lnbits.settings import ( + LNBITS_CUSTOM_LOGO, + LNBITS_SITE_TITLE, +) from . import tpos_ext, tpos_renderer from .crud import get_tpos @@ -33,3 +37,37 @@ async def tpos(request: Request, tpos_id): return tpos_renderer().TemplateResponse( "tpos/tpos.html", {"request": request, "tpos": tpos} ) + +@tpos_ext.get("/manifest/{tpos_id}.webmanifest") +async def manifest(tpos_id: str): + tpos = await get_tpos(tpos_id) + if not tpos: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." + ) + + return { + "short_name": LNBITS_SITE_TITLE, + "name": tpos.name + ' - ' + LNBITS_SITE_TITLE, + "icons": [ + { + "src": LNBITS_CUSTOM_LOGO if LNBITS_CUSTOM_LOGO else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", + "type": "image/png", + "sizes": "900x900", + } + ], + "start_url": "/tpos/" + tpos_id, + "background_color": "#1F2234", + "description": "Bitcoin Lightning tPOS", + "display": "standalone", + "scope": "/tpos/" + tpos_id, + "theme_color": "#1F2234", + "shortcuts": [ + { + "name": tpos.name + ' - ' + LNBITS_SITE_TITLE, + "short_name": tpos.name, + "description": tpos.name + ' - ' + LNBITS_SITE_TITLE, + "url": "/tpos/" + tpos_id, + } + ], + } \ No newline at end of file diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index ae457b61a..e39a9814a 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -52,7 +52,7 @@ async def api_tpos_delete( @tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED) -async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tpos_id: str = None): +async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tipAmount: int = None, tpos_id: str = None): tpos = await get_tpos(tpos_id) if not tpos: @@ -65,7 +65,7 @@ async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tpos_id: str = wallet_id=tpos.wallet, amount=amount, memo=f"{tpos.name}", - extra={"tag": "tpos"}, + extra={"tag": "tpos", "tipAmount": tipAmount, "tposId": tpos_id}, ) except Exception as e: raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) @@ -84,6 +84,7 @@ async def api_tpos_check_invoice(tpos_id: str, payment_hash: str): ) try: status = await api_payment(payment_hash) + except Exception as exc: print(exc) return {"paid": False}