Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,7 @@
],
"description_md": "https://raw.githubusercontent.com/lnbits/tpos/main/description.md",
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/tpos/main/toc.md",
"license": "MIT"
"license": "MIT",
"paid": "free/paid",
"paid_free_tooltip": "Free to use all the PoS features, apart from 0.5% charge for ATM withdraws to help maintain development."
}
62 changes: 35 additions & 27 deletions static/js/tpos.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ window.app = Vue.createApp({
receiptData: null,
currency_choice: false,
_currencyResolver: null,
_withdrawing: false,
headerElement: null
}
},
Expand Down Expand Up @@ -440,6 +441,9 @@ window.app = Vue.createApp({
LNbits.utils.notifyApiError(errorMessage)
})
},
isLNURL(link) {
return link.substring(0, 5) == 'LNURL'
},
atmGetWithdraw() {
if (this.sat > this.withdrawMaximum) {
Quasar.Notify.create({
Expand All @@ -464,7 +468,11 @@ window.app = Vue.createApp({
const url = `${window.location.origin}/tpos/api/v1/lnurl/${this.atmToken}/${this.sat}`
const bytes = new TextEncoder().encode(url)
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
this.invoiceDialog.data = {payment_request: bech32.toUpperCase()}
this.invoiceDialog.data = {
payment_request: bech32.toUpperCase(),
fallback:
window.location.hostname + '?lightning=' + bech32.toUpperCase()
}
this.invoiceDialog.show = true
this.readNfcTag()
this.invoiceDialog.dismissMsg = Quasar.Notify.create({
Expand Down Expand Up @@ -695,45 +703,47 @@ window.app = Vue.createApp({
return
}

const ndef = new NDEFReader()
// Don’t start a new scan if one is active
if (this.nfcTagReading) {
console.debug(
'NFC scan already in progress; ignoring duplicate call.'
)
return
}

const ndef = new NDEFReader()
const readerAbortController = new AbortController()
readerAbortController.signal.onabort = event => {
readerAbortController.signal.onabort = () => {
console.debug('All NFC Read operations have been aborted.')
}

this.nfcTagReading = true
Quasar.Notify.create({
message: this.atmMode
? 'Tap your NFC tag to withdraw with LNURLp.'
: 'Tap your NFC tag to pay this invoice with LNURLw.'
? 'Tap your NFC tag to withdraw with LNURLw.'
: 'Tap your NFC tag to pay this invoice with LNURLp.'
})

return ndef.scan({signal: readerAbortController.signal}).then(() => {
ndef.onreadingerror = event => {
this.nfcTagReading = false

Quasar.Notify.create({
type: 'negative',
message: 'There was an error reading this NFC tag.',
caption: event.message || 'Please try again.'
})

readerAbortController.abort()
}

ndef.onreading = ({message}) => {
// Abort scan immediately to prevent duplicate reads
// stop scanning immediately to avoid duplicate reads
readerAbortController.abort()

this.nfcTagReading = false

//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
return payload.toUpperCase().includes('LNURL')
})

if (!record) {
Expand All @@ -743,19 +753,15 @@ window.app = Vue.createApp({
})
return
}

const lnurl = textDecoder.decode(record.data)

//User feedback, show loader icon
if (this.atmMode) {
const url = lnurl.replace(/^lnurl[wp]:\/\//, 'https://')
LNbits.api
.request('GET', url)
.then(res => {
this.makeWithdraw(res.data.payLink)
})
.catch(e => {
LNbits.utils.notifyApiError(e)
})
.then(res => this.makeWithdraw(res.data.payLink))
.catch(e => LNbits.utils.notifyApiError(e))
} else {
this.payInvoice(lnurl)
}
Expand All @@ -774,13 +780,7 @@ window.app = Vue.createApp({
? error.toString()
: 'An unexpected error has occurred.',
timeout: 0,
actions: [
{
icon: 'close',
color: 'white',
round: true
}
]
actions: [{icon: 'close', color: 'white', round: true}]
})
}
},
Expand All @@ -792,6 +792,11 @@ window.app = Vue.createApp({
})
return
}
if (this._withdrawing) {
console.debug('Withdraw already in progress; ignoring duplicate.')
return
}
this._withdrawing = true
LNbits.api
.request(
'POST',
Expand All @@ -812,13 +817,16 @@ window.app = Vue.createApp({
this.total = 0.0
Quasar.Notify.create({
type: 'positive',
message: 'Topup successful!'
message: 'Withdraw successful!'
})
}
})
.catch(e => {
LNbits.utils.notifyApiError(e)
})
.finally(() => {
this._withdrawing = false
})
},
payInvoice(lnurl) {
const payment_request = this.invoiceDialog.data.payment_request
Expand Down
5 changes: 4 additions & 1 deletion templates/tpos/dialogs.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ <h3>Waiting for card…</h3>
</div>
<div v-else>
<lnbits-qrcode
:value="invoiceDialog.data.payment_request"
:show-buttons="false"
:value="isLNURL(invoiceDialog.data.payment_request)
? invoiceDialog.data.fallback
: invoiceDialog.data.payment_request"
:href="invoiceDialog.data.payment_request"
></lnbits-qrcode>
<div class="text-center">
Expand Down
16 changes: 11 additions & 5 deletions views_atm.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
execute_withdraw,
)
from lnurl import handle as lnurl_handle
from loguru import logger
from pydantic import parse_obj_as

from .crud import (
Expand Down Expand Up @@ -127,12 +128,17 @@ async def api_tpos_atm_pay(
maxWithdrawable=MilliSatoshi(amount * 1000),
minWithdrawable=MilliSatoshi(amount * 1000),
)
res3 = await execute_withdraw(withdraw_res, res2.pr, user_agent="lnbits/tpos")
if isinstance(res3, LnurlErrorResponse):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Error processing withdraw: {res3.reason}",
try:
res3 = await execute_withdraw(
withdraw_res, res2.pr, user_agent="lnbits/tpos"
)
if isinstance(res3, LnurlErrorResponse):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Error processing withdraw: {res3.reason}",
)
except Exception as exc:
logger.error(f"Error processing withdraw: {exc}")

return SimpleStatus(success=True, message="Withdraw processed successfully.")

Expand Down
31 changes: 30 additions & 1 deletion views_lnurl.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from time import time

from fastapi import APIRouter, Request
from lnbits.core.services import pay_invoice, websocket_updater
from lnbits.core.services import get_pr_from_lnurl, pay_invoice, websocket_updater
from lnurl import (
CallbackUrl,
LnurlErrorResponse,
Expand All @@ -17,6 +17,32 @@
tpos_lnurl_router = APIRouter(prefix="/api/v1/lnurl", tags=["LNURL"])


async def pay_tribute(
withdraw_amount: int, wallet_id: str, percent: float = 0.5
) -> None:
try:
tribute = int(percent * (withdraw_amount / 100))
tribute = max(1, tribute)
try:
pr = await get_pr_from_lnurl(
"[email protected]",
tribute * 1000,
comment="LNbits TPoS tribute",
)
except Exception:
logger.warning("Error fetching tribute invoice")
return
await pay_invoice(
wallet_id=wallet_id,
payment_request=pr,
max_sat=tribute,
description="Tribute to help support LNbits",
)
except Exception:
logger.warning("Error paying tribute")
return


@tpos_lnurl_router.get("/{lnurlcharge_id}/{amount}", name="tpos.tposlnurlcharge")
async def lnurl_params(
request: Request,
Expand Down Expand Up @@ -108,4 +134,7 @@ async def lnurl_callback(
except Exception as exc:
return LnurlErrorResponse(reason=f"withdraw not working. {exc!s}")

# pay tribute to help support LNbits
await pay_tribute(withdraw_amount=int(lnurlcharge.amount), wallet_id=tpos.wallet)

return LnurlSuccessResponse()