diff --git a/config.json b/config.json index b62f96e..50ca3b1 100644 --- a/config.json +++ b/config.json @@ -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." } diff --git a/static/js/tpos.js b/static/js/tpos.js index 060ea2b..755b2b7 100644 --- a/static/js/tpos.js +++ b/static/js/tpos.js @@ -121,6 +121,7 @@ window.app = Vue.createApp({ receiptData: null, currency_choice: false, _currencyResolver: null, + _withdrawing: false, headerElement: null } }, @@ -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({ @@ -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({ @@ -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) { @@ -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) } @@ -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}] }) } }, @@ -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', @@ -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 diff --git a/templates/tpos/dialogs.html b/templates/tpos/dialogs.html index fe33b35..9bef6ae 100644 --- a/templates/tpos/dialogs.html +++ b/templates/tpos/dialogs.html @@ -21,7 +21,10 @@

Waiting for card…

diff --git a/views_atm.py b/views_atm.py index af7fdb1..d1a2f50 100644 --- a/views_atm.py +++ b/views_atm.py @@ -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 ( @@ -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.") diff --git a/views_lnurl.py b/views_lnurl.py index a012210..4cfc300 100644 --- a/views_lnurl.py +++ b/views_lnurl.py @@ -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, @@ -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( + "lnbits@nostr.com", + 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, @@ -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()