Skip to content
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

[![OpenSats Supported](https://img.shields.io/badge/OpenSats-Supported-orange?logo=bitcoin&logoColor=white)](https://opensats.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-success?logo=open-source-initiative&logoColor=white)](./LICENSE)
[![Built for LNbits](https://img.shields.io/badge/Built%20for-LNbits-4D4DFF?logo=lightning&logoColor=white)](https://github.com/lnbits/lnbits)
[![Built for LNbits](https://img.shields.io/badge/Built%20for-LNbits-4D4DFF?logo=lightning&logoColor=white)](https://github.com/lnbits/lnbits) [![tip-hero](https://img.shields.io/badge/TipJar-LNBits%20Hero-9b5cff?labelColor=6b7280&logo=lightning&logoColor=white)](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
[![Explore LNbits TPoS](https://img.shields.io/badge/Explore-LNbits%20TPoS-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/tpos/)
[![Stripe Tap-to-Pay Wrapper](https://img.shields.io/badge/Stripe%20Tap--to--Pay-Wrapper-635BFF?logo=stripe&logoColor=white&labelColor=312E81)](https://github.com/lnbits/TPoS-Stripe-Tap-to-Pay-Wrapper)

Expand All @@ -29,9 +29,11 @@ _For video content about the TPoS extension, watch the [official demo](https://w
- [Overview](#overview)
- [Usage](#usage)
- [Receiving Tips](#receiving-tips)
- [LN Address Funding](#ln-address-funding)
- [Adding Items to PoS](#adding-items-to-pos)
- [OTC ATM Functionality](#otc-atm-functionality)
- [Tax Settings](#tax-settings)
- [Powered by LNbits](#powered-by-lnbits)

## Features

Expand Down Expand Up @@ -94,6 +96,36 @@ TPoS lets you take Lightning payments right from the browser. Every TPoS runs is

<img src="https://github.com/user-attachments/assets/b8fa8344-f164-4bd8-869d-6ca8d342ef9a" alt="Tip distribution" width="720">

## LN Address Funding

Some deployments require sharing revenue from every payment made through a TPoS.
This feature allows you to automatically forward a defined percentage of each received payment
to a specific Lightning Address.

This is especially useful when:

- **You host an LNbits server** and give a TPoS to a vendor, and you want to receive a **host fee / revenue share**, or
- **Two participants share one TPoS**, and a portion of each incoming payment should automatically go to a partner, co-owner, or collaborator.

In these cases, the function helps identify the **initial TPoS initiator** and forward their share without manual reconciliation.

### How it works

1. Open or edit a TPoS.
2. Enable **LN Address Funding**.
3. Enter:
- **Lightning Address** of the recipient (e.g., `[email protected]`)
- **Percentage share** (e.g., `10` for 10%)
<img width="504" height="426" alt="Bildschirmfoto 2025-11-25 um 04 02 20" src="https://github.com/user-attachments/assets/4e4eb317-96bc-4094-9a9d-8dea69748d55" />

When a customer pays:

- TPoS receives the full Lightning payment.
- The extension splits the amount automatically.
- The defined percentage is forwarded to the configured Lightning Address.

This happens instantly, with no extra setup and no additional wallets required.

## Adding Items to PoS

You can add items to a TPoS and use an item list for sales.
Expand Down Expand Up @@ -198,4 +230,4 @@ LNbits empowers developers and merchants with modular, open-source tools for bui
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)
[![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/) [![tip-hero](https://img.shields.io/badge/TipJar-LNBits%20Hero-9b5cff?labelColor=7c3aed&logo=lightning&logoColor=white)](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
2 changes: 1 addition & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"repo": "https://github.com/lnbits/tpos",
"short_description": "A shareable PoS terminal!",
"tile": "/tpos/static/image/tpos.png",
"min_lnbits_version": "1.3.0",
"min_lnbits_version": "1.4.0",
"contributors": [
{
"name": "Ben Arc",
Expand Down
36 changes: 32 additions & 4 deletions static/js/tpos.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ window.app = Vue.createApp({
addedAmount: 0,
enablePrint: false,
receiptData: null,
paymentDetails: null,
currency_choice: false,
_currencyResolver: null,
_withdrawing: false,
Expand Down Expand Up @@ -885,7 +886,11 @@ window.app = Vue.createApp({
obj.dateFrom = moment(obj.time).fromNow()
if (obj.currency != LNBITS_DENOMINATION) {
obj.amountFiat = this.formatAmount(
obj.amount / 1000 / this.exchangeRate,
obj.amount / 1000 / (obj.exchange_rate || this.exchangeRate),
this.currency
)
obj.amountFiatNow = this.formatAmount(
Math.abs(obj.amount) / 1000 / this.exchangeRate,
this.currency
)
}
Expand Down Expand Up @@ -956,22 +961,45 @@ window.app = Vue.createApp({
}
},
formatDate(timestamp) {
return LNbits.utils.formatDate(timestamp / 1000)
return LNbits.utils.formatTimestamp(timestamp / 1000)
},
showComplete() {
this.complete.show = true
if (this.$q.screen.lt.lg && this.cartDrawer) {
this.cartDrawer = false
}
},
async printReceipt(paymentHash) {
this.receiptData = null
async showDetails(paymentHash) {
try {
const {data} = await LNbits.api.request(
'GET',
`/tpos/api/v1/tposs/${this.tposId}/invoices/${paymentHash}?extra=true`
)
this.receiptData = data
if (data.extra && data.extra.details) {
this.paymentDetails = {}
this.paymentDetails.items = data.extra.details.items || []
this.paymentDetails.currency = data.extra.details.currency
this.paymentDetails.exchangeRate = data.extra.details.exchangeRate
}
return
} catch (error) {
console.error('Error fetching receipt data:', error)
Quasar.Notify.create({
type: 'negative',
message: 'Error fetching receipt data.'
})
}
},
async printReceipt(paymentHash) {
try {
if (!this.receiptData) {
const {data} = await LNbits.api.request(
'GET',
`/tpos/api/v1/tposs/${this.tposId}/invoices/${paymentHash}?extra=true`
)
this.receiptData = data
}

this.$q
.dialog({
Expand Down
121 changes: 87 additions & 34 deletions templates/tpos/dialogs.html
Original file line number Diff line number Diff line change
Expand Up @@ -152,41 +152,94 @@ <h5 class="q-mt-none q-mb-sm">
<q-item-label class="text-bold">No paid invoices</q-item-label>
</q-item-section>
</q-item>
<q-item v-for="(payment, idx) in lastPaymentsDialog.data" :key="idx">
<q-item-section>
<q-item-label
v-if="payment.amountFiat"
class="text-bold"
v-text="payment.amountFiat"
>
</q-item-label>
<q-item-label
class="text-bold"
v-text="formatBalance(payment.amount / 1000)"
>
</q-item-label>
<q-item-label caption>
<q-icon
class="q-mr-sm"
size="sm"
name="check"
color="green"
></q-icon>
<span v-text="payment.dateFrom"></span>
</q-item-label>
<q-item-label caption lines="2"
>Hash: ${payment.checking_id.slice(0, 30)}...</q-item-label
<q-expansion-item
@show="payment.amount > 0 ? showDetails(payment.checking_id) : null"
@before-hide="paymentDetails = null"
group="paymentList"
v-for="(payment, idx) in lastPaymentsDialog.data"
:key="idx"
dense
>
<template v-slot:header>
<q-item-section>
<q-item-label v-if="payment.amountFiat">
<span class="text-bold" v-text="payment.amountFiat"></span>
<span
class="text-italic q-ml-sm"
v-text="`(${payment.amountFiatNow})`"
>
</span>
<q-tooltip>
Amount at payment time vs current amount
</q-tooltip>
</q-item-label>
<q-item-label
class="text-bold"
v-text="formatBalance(payment.amount / 1000)"
>
</q-item-label>
<q-item-label caption>
<q-icon
class="q-mr-sm"
size="sm"
name="check"
color="green"
></q-icon>
<span v-text="payment.dateFrom"></span>
</q-item-label>
<q-item-label
caption
lines="2"
v-text="`Hash: ${payment.checking_id.slice(0, 30)}...`"
></q-item-label>
</q-item-section>
</template>
<q-card class="q-ma-md">
<q-card-section v-if="paymentDetails && paymentDetails.items">
<q-list separator dense>
<q-item
v-for="(item, index) in paymentDetails.items"
:key="index"
>
<q-item-section>
<q-item-label
v-text="`${item.quantity} x ${item.title}`"
></q-item-label>
<q-item-label
caption
v-text="formatAmount(item.price * item.quantity, currency)"
></q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label class="text-bold">Total</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label
class="text-bold"
v-text="payment.amountFiat"
></q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-inner-loading :showing="!paymentDetails">
<q-spinner-gears size="30px" color="primary"></q-spinner-gears>
</q-inner-loading>
<q-card-actions
align="right"
v-if="enablePrint && payment.amount > 0"
>
</q-item-section>
<q-item-section side v-if="enablePrint && payment.amount > 0">
<q-btn
round
icon="print"
color="primary"
@click="printReceipt(payment.checking_id)"
></q-btn>
</q-item-section>
</q-item>
<q-btn
round
icon="print"
color="primary"
@click.stop="printReceipt(payment.checking_id)"
></q-btn>
</q-card-actions>
</q-card>
</q-expansion-item>
</q-list>
</q-card>
</q-dialog>
Expand Down
32 changes: 15 additions & 17 deletions views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,24 +174,22 @@ async def api_tpos_create_invoice(tpos_id: str, data: CreateTposInvoice) -> Paym
@tpos_api_router.get("/api/v1/tposs/{tpos_id}/invoices")
async def api_tpos_get_latest_invoices(tpos_id: str):
payments = await get_latest_payments_by_extension(ext_name="tpos", ext_id=tpos_id)

details = payments[0].extra.get("details", None)
exchange_rate = None
currency = None
if details:
exchange_rate = details.get("exchange_rate", None)
result = []
for payment in payments:
details = payment.extra.get("details", {})
currency = details.get("currency", None)
return [
{
"checking_id": payment.checking_id,
"amount": payment.amount,
"time": payment.time,
"pending": payment.pending,
"currency": currency,
"exchange_rate": exchange_rate,
}
for payment in payments
]
exchange_rate = details.get("exchangeRate") or payment.extra.get("exchangeRate")
result.append(
{
"checking_id": payment.checking_id,
"amount": payment.amount,
"time": payment.time,
"pending": payment.pending,
"currency": currency,
"exchange_rate": exchange_rate,
}
)
return result


@tpos_api_router.post(
Expand Down