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
11 changes: 11 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,14 @@ async def m019_add_receipt_sats_only(db: Database):
ALTER TABLE tpos.pos ADD only_show_sats_on_bitcoin BOOLEAN DEFAULT true;
"""
)


async def m020_add_remote_mode_toggle(db: Database):
"""
Add enable_remote option for cross-device invoice triggering.
"""
await db.execute(
"""
ALTER TABLE tpos.pos ADD enable_remote BOOLEAN DEFAULT false;
"""
)
2 changes: 2 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class CreateTposData(BaseModel):
lnaddress: bool = Field(False)
lnaddress_cut: int | None = Field(0)
enable_receipt_print: bool = Query(False)
enable_remote: bool = Query(False)
business_name: str | None
business_address: str | None
business_vat_id: str | None
Expand Down Expand Up @@ -95,6 +96,7 @@ class TposClean(BaseModel):
inventory_omit_tags: str | None = None
tip_options: str | None = None
enable_receipt_print: bool
enable_remote: bool = False
business_name: str | None = None
business_address: str | None = None
business_vat_id: str | None = None
Expand Down
2 changes: 2 additions & 0 deletions static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ window.app = Vue.createApp({
lnaddress: false,
lnaddress_cut: 2,
enable_receipt_print: false,
enable_remote: false,
only_show_sats_on_bitcoin: true,
fiat: false,
stripe_card_payments: false,
Expand Down Expand Up @@ -243,6 +244,7 @@ window.app = Vue.createApp({
lnaddress: false,
lnaddress_cut: 2,
enable_receipt_print: false,
enable_remote: false,
only_show_sats_on_bitcoin: true,
fiat: false,
stripe_card_payments: false,
Expand Down
101 changes: 88 additions & 13 deletions static/js/tpos.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ window.app = Vue.createApp({
totalfsat: 0,
addedAmount: 0,
enablePrint: false,
enableRemote: false,
receiptData: null,
orderReceipt: false,
printDialog: {
Expand All @@ -139,6 +140,9 @@ window.app = Vue.createApp({
this.$q.localStorage.getItem('lnbits.tpos.header') !== 'shown',
categoryColors: {},
categoryColorIndex: 0,
remoteInvoiceWs: null,
remoteInvoiceReconnectTimer: null,
paymentWsByHash: {},
pastelColors: [
'blue-5',
'green-5',
Expand Down Expand Up @@ -261,6 +265,73 @@ window.app = Vue.createApp({
}
},
methods: {
connectRemoteInvoiceWS() {
if (!this.enableRemote || !this.tposId) return
if (this.remoteInvoiceWs) return

const url = new URL(window.location)
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
url.pathname = `/api/v1/ws/${this.tposId}`
const ws = new WebSocket(url)
this.remoteInvoiceWs = ws

ws.onmessage = ({data}) => this.handleRemoteInvoiceMessage(data)
ws.onclose = () => {
this.remoteInvoiceWs = null
if (this.enableRemote) {
this.remoteInvoiceReconnectTimer = setTimeout(() => {
this.connectRemoteInvoiceWS()
}, 2000)
}
}
ws.onerror = err => {
console.warn('Remote websocket error:', err)
}
},
disconnectRemoteInvoiceWS() {
if (this.remoteInvoiceReconnectTimer) {
clearTimeout(this.remoteInvoiceReconnectTimer)
this.remoteInvoiceReconnectTimer = null
}
if (this.remoteInvoiceWs) {
this.remoteInvoiceWs.close()
this.remoteInvoiceWs = null
}
},
handleRemoteInvoiceMessage(rawData) {
let payload = null
try {
payload = JSON.parse(rawData)
} catch {
// Ignore non-JSON events from other TPoS websocket usages.
return
}

if (payload.type !== 'invoice_created') return
if (!payload.payment_hash || !payload.payment_request) return
this.amount = payload.amount_fiat || this.amount
this.tipAmount = payload.tip_amount || this.tipAmount
this.exchangeRate = payload.exchange_rate || this.exchangeRate

this.openInvoiceDialog(payload.payment_hash, payload.payment_request)
this.subscribeToPaymentWS(payload.payment_hash)
},
openInvoiceDialog(paymentHash, paymentRequest) {
if (
this.invoiceDialog.show &&
this.invoiceDialog.data.payment_hash === paymentHash
) {
return
}
this.invoiceDialog.data.payment_hash = paymentHash
this.invoiceDialog.data.payment_request = paymentRequest
this.invoiceDialog.show = true
this.readNfcTag()
this.invoiceDialog.dismissMsg = Quasar.Notify.create({
timeout: 0,
message: 'Waiting for payment...'
})
},
setColor(category) {
if (!category || category.toLowerCase() === 'all') {
return 'primary'
Expand Down Expand Up @@ -338,6 +409,7 @@ window.app = Vue.createApp({
if (!cartItem) return
this.$q
.dialog({
position: 'top',
title: 'Set price',
message: 'Update item price for this cart line',
prompt: {
Expand Down Expand Up @@ -366,6 +438,7 @@ window.app = Vue.createApp({
if (!cartItem) return
this.$q
.dialog({
position: 'top',
title: 'Set note',
message: 'Add a note for this item',
prompt: {
Expand Down Expand Up @@ -838,40 +911,33 @@ window.app = Vue.createApp({
null,
params
)
let paymentRequest = 'lightning:' + data.bolt11.toUpperCase()
if (
data.extra.fiat_payment_request &&
!data.extra.fiat_payment_request.startsWith('pi_')
) {
this.invoiceDialog.data.payment_request =
data.extra.fiat_payment_request
paymentRequest = data.extra.fiat_payment_request
} else if (
data.extra.fiat_payment_request &&
data.extra.fiat_payment_request.startsWith('pi_')
) {
this.invoiceDialog.data.payment_request = 'tap_to_pay'
} else {
this.invoiceDialog.data.payment_request =
'lightning:' + data.bolt11.toUpperCase()
paymentRequest = 'tap_to_pay'
}
this.invoiceDialog.data.payment_hash = data.payment_hash
this.invoiceDialog.show = true
this.readNfcTag()
this.invoiceDialog.dismissMsg = Quasar.Notify.create({
timeout: 0,
message: 'Waiting for payment...'
})
this.openInvoiceDialog(data.payment_hash, paymentRequest)
this.subscribeToPaymentWS(data.payment_hash)
} catch (error) {
console.error(error)
LNbits.utils.notifyApiError(error)
}
},
subscribeToPaymentWS(paymentHash) {
if (this.paymentWsByHash[paymentHash]) return
try {
const url = new URL(window.location)
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
url.pathname = `/api/v1/ws/${paymentHash}`
const ws = new WebSocket(url)
this.paymentWsByHash[paymentHash] = ws
ws.onmessage = async ({data}) => {
const payment = JSON.parse(data)
if (payment.pending === false) {
Expand All @@ -889,6 +955,9 @@ window.app = Vue.createApp({
ws.close()
}
}
ws.onclose = () => {
delete this.paymentWsByHash[paymentHash]
}
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
Expand Down Expand Up @@ -1302,6 +1371,7 @@ window.app = Vue.createApp({
this.tposLNaddress = tpos.lnaddress
this.tposLNaddressCut = tpos.lnaddress_cut
this.enablePrint = tpos.enable_receipt_print
this.enableRemote = Boolean(tpos.enable_remote)
this.fiatProvider = tpos.fiat_provider

this.tip_options = tpos.tip_options == 'null' ? null : tpos.tip_options
Expand Down Expand Up @@ -1334,6 +1404,11 @@ window.app = Vue.createApp({
if (this.headerElement) {
this.headerElement.style.display = this.headerHidden ? 'none' : ''
}
this.connectRemoteInvoiceWS()
},
beforeUnmount() {
this.disconnectRemoteInvoiceWS()
Object.values(this.paymentWsByHash).forEach(ws => ws.close())
},
onMounted() {
if (!this.headerElement) {
Expand Down
8 changes: 8 additions & 0 deletions templates/tpos/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -488,12 +488,20 @@ <h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} TPoS extension</h6>
label="Enable selling BTC (ATM)"
></q-checkbox>
</div>
</div>
<div class="row">
<div class="col">
<q-checkbox
v-model="formDialog.data.enable_receipt_print"
label="Enable printing (experimental)"
></q-checkbox>
</div>
<div class="col">
<q-checkbox
v-model="formDialog.data.enable_remote"
label="Enable remote (to trigger payments on external device)"
></q-checkbox>
</div>
</div>
<template v-if="formDialog.data.enable_receipt_print">
<p class="text-caption">
Expand Down
24 changes: 22 additions & 2 deletions views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,20 +246,40 @@ async def api_tpos_create_invoice(
fiat_provider=tpos.fiat_provider if data.pay_in_fiat else None,
)
payment = await create_payment_request(tpos.wallet, invoice_data)
payment_request_for_display = "lightning:" + payment.bolt11.upper()
fiat_payment_request = payment.extra.get("fiat_payment_request")
if fiat_payment_request and not fiat_payment_request.startswith("pi_"):
payment_request_for_display = fiat_payment_request
elif fiat_payment_request and fiat_payment_request.startswith("pi_"):
payment_request_for_display = "tap_to_pay"

if tpos.enable_remote:
payload = {
"type": "invoice_created",
"tpos_id": tpos_id,
"payment_hash": payment.payment_hash,
"payment_request": payment_request_for_display,
"paid_in_fiat": data.pay_in_fiat,
"amount_fiat": data.amount_fiat,
"tip_amount": data.tip_amount,
"exchange_rate": data.exchange_rate if data.exchange_rate else None,
}
await websocket_updater(tpos_id, json.dumps(payload))

if (invoice_data.extra or {}).get("fiat_method") == "terminal":
pi_id = payment.extra.get("fiat_checking_id")
client_secret = payment.extra.get("fiat_payment_request")
if pi_id and client_secret:
amount_minor = round(amount * 100)
payload = TapToPay(
tap_to_pay_payload = TapToPay(
payment_intent_id=pi_id,
client_secret=client_secret,
currency=invoice_data.unit.lower(),
amount=amount_minor,
tpos_id=tpos_id,
payment_hash=payment.payment_hash,
)
await websocket_updater(tpos_id, str(payload))
await websocket_updater(tpos_id, json.dumps(tap_to_pay_payload.dict()))
return payment

except Exception as exc:
Expand Down
Loading