From 6aef9bfd459a62b1938708f7d2a971384f42e97c Mon Sep 17 00:00:00 2001 From: Lee Salminen Date: Mon, 15 Aug 2022 07:37:10 -0600 Subject: [PATCH] Add NFC support to TPOS Extension (#856) * WIP * styling * WIP * WIP * WIP * Clean up for PR * isort * hold abortContoller for a while longer --- lnbits/extensions/tpos/models.py | 4 + .../extensions/tpos/templates/tpos/tpos.html | 104 +++++++++++++++++- lnbits/extensions/tpos/views_api.py | 64 ++++++++++- 3 files changed, 169 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/tpos/models.py b/lnbits/extensions/tpos/models.py index 36bca79b..f6522add 100644 --- a/lnbits/extensions/tpos/models.py +++ b/lnbits/extensions/tpos/models.py @@ -23,3 +23,7 @@ class TPoS(BaseModel): @classmethod def from_row(cls, row: Row) -> "TPoS": return cls(**dict(row)) + + +class PayLnurlWData(BaseModel): + lnurl: str diff --git a/lnbits/extensions/tpos/templates/tpos/tpos.html b/lnbits/extensions/tpos/templates/tpos/tpos.html index ebc6595e..c9aabe4c 100644 --- a/lnbits/extensions/tpos/templates/tpos/tpos.html +++ b/lnbits/extensions/tpos/templates/tpos/tpos.html @@ -14,7 +14,7 @@

{% raw %}{{ famount }}{% endraw %}

-
+
{% raw %}{{ fsat }}{% endraw %} sat
@@ -174,6 +174,13 @@ > {% endraw %} +
Close @@ -281,6 +288,7 @@ exchangeRate: null, stack: [], tipAmount: 0.0, + nfcTagReading: false, invoiceDialog: { show: false, data: null, @@ -356,7 +364,7 @@ this.showInvoice() }, submitForm: function () { - if (this.tip_options) { + if (this.tip_options.length) { this.showTipModal() } else { this.showInvoice() @@ -410,6 +418,98 @@ 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) { diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index 9609956e..b7f14b98 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -1,7 +1,9 @@ from http import HTTPStatus +import httpx from fastapi import Query from fastapi.params import Depends +from lnurl import decode as decode_lnurl from loguru import logger from starlette.exceptions import HTTPException @@ -12,7 +14,7 @@ from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from . import tpos_ext from .crud import create_tpos, delete_tpos, get_tpos, get_tposs -from .models import CreateTposData +from .models import CreateTposData, PayLnurlWData @tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK) @@ -79,6 +81,66 @@ async def api_tpos_create_invoice( return {"payment_hash": payment_hash, "payment_request": payment_request} +@tpos_ext.post( + "/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK +) +async def api_tpos_pay_invoice( + lnurl_data: PayLnurlWData, payment_request: str = None, tpos_id: str = None +): + tpos = await get_tpos(tpos_id) + + if not tpos: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." + ) + + lnurl = ( + lnurl_data.lnurl.replace("lnurlw://", "") + .replace("lightning://", "") + .replace("LIGHTNING://", "") + .replace("lightning:", "") + .replace("LIGHTNING:", "") + ) + + if lnurl.lower().startswith("lnurl"): + lnurl = decode_lnurl(lnurl) + else: + lnurl = "https://" + lnurl + + async with httpx.AsyncClient() as client: + try: + r = await client.get(lnurl, follow_redirects=True) + if r.is_error: + lnurl_response = {"success": False, "detail": "Error loading"} + else: + resp = r.json() + if resp["tag"] != "withdrawRequest": + lnurl_response = {"success": False, "detail": "Wrong tag type"} + else: + r2 = await client.get( + resp["callback"], + follow_redirects=True, + params={ + "k1": resp["k1"], + "pr": payment_request, + }, + ) + resp2 = r2.json() + if r2.is_error: + lnurl_response = { + "success": False, + "detail": "Error loading callback", + } + elif resp2["status"] == "ERROR": + lnurl_response = {"success": False, "detail": resp2["reason"]} + else: + lnurl_response = {"success": True, "detail": resp2} + except (httpx.ConnectError, httpx.RequestError): + lnurl_response = {"success": False, "detail": "Unexpected error occurred"} + + return lnurl_response + + @tpos_ext.get( "/api/v1/tposs/{tpos_id}/invoices/{payment_hash}", status_code=HTTPStatus.OK )