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
)