Add NFC support to TPOS Extension (#856)

* WIP

* styling

* WIP

* WIP

* WIP

* Clean up for PR

* isort

* hold abortContoller for a while longer
This commit is contained in:
Lee Salminen 2022-08-15 07:37:10 -06:00 committed by GitHub
parent 4ac73f1a79
commit 6aef9bfd45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 169 additions and 3 deletions

View file

@ -23,3 +23,7 @@ class TPoS(BaseModel):
@classmethod @classmethod
def from_row(cls, row: Row) -> "TPoS": def from_row(cls, row: Row) -> "TPoS":
return cls(**dict(row)) return cls(**dict(row))
class PayLnurlWData(BaseModel):
lnurl: str

View file

@ -14,7 +14,7 @@
<div class="row justify-center full-width"> <div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center"> <div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3> <h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none"> <h5 class="q-mt-none q-mb-sm">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small> {% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5> </h5>
</div> </div>
@ -174,6 +174,13 @@
> >
{% endraw %} {% endraw %}
</h5> </h5>
<q-btn
outline
color="grey"
icon="nfc"
@click="readNfcTag()"
:disable="nfcTagReading"
></q-btn>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
@ -281,6 +288,7 @@
exchangeRate: null, exchangeRate: null,
stack: [], stack: [],
tipAmount: 0.0, tipAmount: 0.0,
nfcTagReading: false,
invoiceDialog: { invoiceDialog: {
show: false, show: false,
data: null, data: null,
@ -356,7 +364,7 @@
this.showInvoice() this.showInvoice()
}, },
submitForm: function () { submitForm: function () {
if (this.tip_options) { if (this.tip_options.length) {
this.showTipModal() this.showTipModal()
} else { } else {
this.showInvoice() this.showInvoice()
@ -410,6 +418,98 @@
LNbits.utils.notifyApiError(error) 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 () { getRates: function () {
var self = this var self = this
axios.get('https://api.opennode.co/v1/rates').then(function (response) { axios.get('https://api.opennode.co/v1/rates').then(function (response) {

View file

@ -1,7 +1,9 @@
from http import HTTPStatus from http import HTTPStatus
import httpx
from fastapi import Query from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from lnurl import decode as decode_lnurl
from loguru import logger from loguru import logger
from starlette.exceptions import HTTPException 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 . import tpos_ext
from .crud import create_tpos, delete_tpos, get_tpos, get_tposs 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) @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} 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( @tpos_ext.get(
"/api/v1/tposs/{tpos_id}/invoices/{payment_hash}", status_code=HTTPStatus.OK "/api/v1/tposs/{tpos_id}/invoices/{payment_hash}", status_code=HTTPStatus.OK
) )