Merge branch 'main' of github.com:lnbits/lnbits-legend

This commit is contained in:
callebtc
2022-07-25 14:32:38 +02:00
16 changed files with 255 additions and 25 deletions

View File

@@ -30,7 +30,7 @@ async def create_tpos(wallet_id: str, data: CreateTposData) -> TPoS:
async def get_tpos(tpos_id: str) -> Optional[TPoS]: async def get_tpos(tpos_id: str) -> Optional[TPoS]:
row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,)) row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,))
return TPoS.from_row(row) if row else None return TPoS(**row) if row else None
async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]: async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
@@ -42,7 +42,7 @@ async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,) f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,)
) )
return [TPoS.from_row(row) for row in rows] return [TPoS(**row) for row in rows]
async def delete_tpos(tpos_id: str) -> None: async def delete_tpos(tpos_id: str) -> None:

View File

@@ -1,13 +1,15 @@
from sqlite3 import Row from sqlite3 import Row
from typing import Optional
from fastapi import Query
from pydantic import BaseModel from pydantic import BaseModel
class CreateTposData(BaseModel): class CreateTposData(BaseModel):
name: str name: str
currency: str currency: str
tip_options: str tip_options: str = Query(None)
tip_wallet: str tip_wallet: str = Query(None)
class TPoS(BaseModel): class TPoS(BaseModel):
@@ -15,8 +17,8 @@ class TPoS(BaseModel):
wallet: str wallet: str
name: str name: str
currency: str currency: str
tip_options: str tip_options: Optional[str]
tip_wallet: str tip_wallet: Optional[str]
@classmethod @classmethod
def from_row(cls, row: Row) -> "TPoS": def from_row(cls, row: Row) -> "TPoS":

View File

@@ -26,7 +26,6 @@ async def on_invoice_paid(payment: Payment) -> None:
# now we make some special internal transfers (from no one to the receiver) # now we make some special internal transfers (from no one to the receiver)
tpos = await get_tpos(payment.extra.get("tposId")) tpos = await get_tpos(payment.extra.get("tposId"))
tipAmount = payment.extra.get("tipAmount") tipAmount = payment.extra.get("tipAmount")
if tipAmount is None: if tipAmount is None:
@@ -34,6 +33,7 @@ async def on_invoice_paid(payment: Payment) -> None:
return return
tipAmount = tipAmount * 1000 tipAmount = tipAmount * 1000
amount = payment.amount - tipAmount
# mark the original payment with one extra key, "splitted" # mark the original payment with one extra key, "splitted"
# (this prevents us from doing this process again and it's informative) # (this prevents us from doing this process again and it's informative)
@@ -41,13 +41,13 @@ async def on_invoice_paid(payment: Payment) -> None:
await core_db.execute( await core_db.execute(
""" """
UPDATE apipayments UPDATE apipayments
SET extra = ?, amount = amount - ? SET extra = ?, amount = ?
WHERE hash = ? WHERE hash = ?
AND checking_id NOT LIKE 'internal_%' AND checking_id NOT LIKE 'internal_%'
""", """,
( (
json.dumps(dict(**payment.extra, tipSplitted=True)), json.dumps(dict(**payment.extra, tipSplitted=True)),
tipAmount, amount,
payment.payment_hash, payment.payment_hash,
), ),
) )
@@ -60,7 +60,7 @@ async def on_invoice_paid(payment: Payment) -> None:
payment_request="", payment_request="",
payment_hash=payment.payment_hash, payment_hash=payment.payment_hash,
amount=tipAmount, amount=tipAmount,
memo=payment.memo, memo=f"Tip for {payment.memo}",
pending=False, pending=False,
extra={"tipSplitted": True}, extra={"tipSplitted": True},
) )

View File

@@ -54,8 +54,8 @@
></q-btn> ></q-btn>
</q-td> </q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ (col.name == 'tip_options' ? JSON.parse(col.value).join(", ") {{ (col.name == 'tip_options' && col.value ?
: col.value) }} JSON.parse(col.value).join(", ") : col.value) }}
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
<q-btn <q-btn

View File

@@ -167,7 +167,12 @@
<div class="text-center"> <div class="text-center">
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3> <h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none"> <h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small> {% raw %}{{ fsat }}
<small>sat</small>
<span v-show="tip_options" style="font-size: 0.75rem"
>( + {{ tipAmountSat }} tip)</span
>
{% endraw %}
</h5> </h5>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
@@ -272,7 +277,7 @@
return { return {
tposId: '{{ tpos.id }}', tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}', currency: '{{ tpos.currency }}',
tip_options: JSON.parse('{{ tpos.tip_options }}'), tip_options: null,
exchangeRate: null, exchangeRate: null,
stack: [], stack: [],
tipAmount: 0.0, tipAmount: 0.0,
@@ -310,7 +315,6 @@
return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000) return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000)
}, },
fsat: function () { fsat: function () {
console.log('sat', this.sat, LNbits.utils.formatSat(this.sat))
return LNbits.utils.formatSat(this.sat) return LNbits.utils.formatSat(this.sat)
} }
}, },
@@ -350,7 +354,7 @@
this.showInvoice() this.showInvoice()
}, },
submitForm: function () { submitForm: function () {
if (this.tip_options.length) { if (this.tip_options) {
this.showTipModal() this.showTipModal()
} else { } else {
this.showInvoice() this.showInvoice()
@@ -362,7 +366,6 @@
showInvoice: function () { showInvoice: function () {
var self = this var self = this
var dialog = this.invoiceDialog var dialog = this.invoiceDialog
console.log(this.sat, this.tposId)
axios axios
.post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, { .post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, {
params: { params: {
@@ -416,6 +419,11 @@
created: function () { created: function () {
var getRates = this.getRates var getRates = this.getRates
getRates() getRates()
this.tip_options =
'{{ tpos.tip_options | tojson }}' == 'null'
? null
: JSON.parse('{{ tpos.tip_options }}')
console.log(typeof this.tip_options, this.tip_options)
setInterval(function () { setInterval(function () {
getRates() getRates()
}, 20000) }, 20000)

View File

@@ -17,7 +17,7 @@ from .models import CreateTposData
@tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK) @tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK)
async def api_tposs( async def api_tposs(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
): ):
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
if all_wallets: if all_wallets:
@@ -63,6 +63,9 @@ async def api_tpos_create_invoice(
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
) )
if tipAmount:
amount += tipAmount
try: try:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=tpos.wallet, wallet_id=tpos.wallet,

View File

@@ -26,6 +26,8 @@ LNBits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes t
- on details you can print the vouchers\ - on details you can print the vouchers\
![printable vouchers](https://i.imgur.com/2xLHbob.jpg) ![printable vouchers](https://i.imgur.com/2xLHbob.jpg)
- every printed LNURLw QR code is unique, it can only be used once - every printed LNURLw QR code is unique, it can only be used once
3. Bonus: you can use an LNbits themed voucher, or use a custom one. There's a _template.svg_ file in `static/images` folder if you want to create your own.\
![voucher](https://i.imgur.com/qyQoHi3.jpg)
#### Advanced #### Advanced

View File

@@ -26,9 +26,10 @@ async def create_withdraw_link(
k1, k1,
open_time, open_time,
usescsv, usescsv,
webhook_url webhook_url,
custom_url
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
link_id, link_id,
@@ -44,6 +45,7 @@ async def create_withdraw_link(
int(datetime.now().timestamp()) + data.wait_time, int(datetime.now().timestamp()) + data.wait_time,
usescsv, usescsv,
data.webhook_url, data.webhook_url,
data.custom_url,
), ),
) )
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)

View File

@@ -115,3 +115,10 @@ async def m004_webhook_url(db):
Adds webhook_url Adds webhook_url
""" """
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_url TEXT;") await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_url TEXT;")
async def m005_add_custom_print_design(db):
"""
Adds custom print design
"""
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;")

View File

@@ -16,6 +16,7 @@ class CreateWithdrawData(BaseModel):
wait_time: int = Query(..., ge=1) wait_time: int = Query(..., ge=1)
is_unique: bool is_unique: bool
webhook_url: str = Query(None) webhook_url: str = Query(None)
custom_url: str = Query(None)
class WithdrawLink(BaseModel): class WithdrawLink(BaseModel):
@@ -34,6 +35,7 @@ class WithdrawLink(BaseModel):
usescsv: str = Query(None) usescsv: str = Query(None)
number: int = Query(0) number: int = Query(0)
webhook_url: str = Query(None) webhook_url: str = Query(None)
custom_url: str = Query(None)
@property @property
def is_spent(self) -> bool: def is_spent(self) -> bool:

View File

@@ -20,9 +20,12 @@ var mapWithdrawLink = function (obj) {
obj.uses_left = obj.uses - obj.used obj.uses_left = obj.uses - obj.used
obj.print_url = [locationPath, 'print/', obj.id].join('') obj.print_url = [locationPath, 'print/', obj.id].join('')
obj.withdraw_url = [locationPath, obj.id].join('') obj.withdraw_url = [locationPath, obj.id].join('')
obj._data.use_custom = Boolean(obj.custom_url)
return obj return obj
} }
const CUSTOM_URL = '/static/images/default_voucher.png'
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
@@ -59,13 +62,15 @@ new Vue({
secondMultiplier: 'seconds', secondMultiplier: 'seconds',
secondMultiplierOptions: ['seconds', 'minutes', 'hours'], secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
data: { data: {
is_unique: false is_unique: false,
use_custom: false
} }
}, },
simpleformDialog: { simpleformDialog: {
show: false, show: false,
data: { data: {
is_unique: true, is_unique: true,
use_custom: true,
title: 'Vouchers', title: 'Vouchers',
min_withdrawable: 0, min_withdrawable: 0,
wait_time: 1 wait_time: 1
@@ -106,12 +111,14 @@ new Vue({
}, },
closeFormDialog: function () { closeFormDialog: function () {
this.formDialog.data = { this.formDialog.data = {
is_unique: false is_unique: false,
use_custom: false
} }
}, },
simplecloseFormDialog: function () { simplecloseFormDialog: function () {
this.simpleformDialog.data = { this.simpleformDialog.data = {
is_unique: false is_unique: false,
use_custom: false
} }
}, },
openQrCodeDialog: function (linkId) { openQrCodeDialog: function (linkId) {
@@ -133,6 +140,9 @@ new Vue({
id: this.formDialog.data.wallet id: this.formDialog.data.wallet
}) })
var data = _.omit(this.formDialog.data, 'wallet') var data = _.omit(this.formDialog.data, 'wallet')
if (data.use_custom && !data?.custom_url) {
data.custom_url = CUSTOM_URL
}
data.wait_time = data.wait_time =
data.wait_time * data.wait_time *
@@ -141,7 +151,6 @@ new Vue({
minutes: 60, minutes: 60,
hours: 3600 hours: 3600
}[this.formDialog.secondMultiplier] }[this.formDialog.secondMultiplier]
if (data.id) { if (data.id) {
this.updateWithdrawLink(wallet, data) this.updateWithdrawLink(wallet, data)
} else { } else {
@@ -159,6 +168,10 @@ new Vue({
data.title = 'vouchers' data.title = 'vouchers'
data.is_unique = true data.is_unique = true
if (data.use_custom && !data?.custom_url) {
data.custom_url = '/static/images/default_voucher.png'
}
if (data.id) { if (data.id) {
this.updateWithdrawLink(wallet, data) this.updateWithdrawLink(wallet, data)
} else { } else {
@@ -181,7 +194,8 @@ new Vue({
'uses', 'uses',
'wait_time', 'wait_time',
'is_unique', 'is_unique',
'webhook_url' 'webhook_url',
'custom_url'
) )
) )
.then(function (response) { .then(function (response) {

View File

@@ -217,6 +217,32 @@
label="Webhook URL (optional)" label="Webhook URL (optional)"
hint="A URL to be called whenever this link gets used." hint="A URL to be called whenever this link gets used."
></q-input> ></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.use_custom"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Use a custom voucher design </q-item-label>
<q-item-label caption
>You can use an LNbits voucher design or a custom
one</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<q-input
v-if="formDialog.data.use_custom"
filled
dense
v-model="formDialog.data.custom_url"
type="text"
label="Custom design .png (optional)"
hint="Enter a URL if you want to use a custom design or leave blank for showing only the QR"
></q-input>
<q-list> <q-list>
<q-item tag="label" class="rounded-borders"> <q-item tag="label" class="rounded-borders">
<q-item-section avatar> <q-item-section avatar>
@@ -303,6 +329,32 @@
:default="1" :default="1"
label="Number of vouchers" label="Number of vouchers"
></q-input> ></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="simpleformDialog.data.use_custom"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Use a custom voucher design </q-item-label>
<q-item-label caption
>You can use an LNbits voucher design or a custom
one</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<q-input
v-if="simpleformDialog.data.use_custom"
filled
dense
v-model="simpleformDialog.data.custom_url"
type="text"
label="Custom design .png (optional)"
hint="Enter a URL if you want to use a custom design or leave blank for showing only the QR"
></q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn

View File

@@ -0,0 +1,110 @@
{% extends "print.html" %} {% block page %}
<div class="row">
<div class="" id="vue">
{% for page in link %}
<page size="A4" id="pdfprint">
{% for one in page %}
<div class="wrapper">
<img src="{{custom_url}}" alt="..." />
<span>{{ amt }} sats</span>
<div class="lnurlw">
<qrcode :value="'{{one}}'" :options="{width: 95, margin: 1}"></qrcode>
</div>
</div>
{% endfor %}
</page>
{% endfor %}
</div>
</div>
{% endblock %} {% block styles %}
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400');
body {
background: rgb(204, 204, 204);
}
page {
background: white;
display: block;
margin: 0 auto;
margin-bottom: 0.5cm;
box-shadow: 0 0 0.5cm rgba(0, 0, 0, 0.5);
}
page[size='A4'] {
width: 21cm;
height: 29.7cm;
}
.wrapper {
position: relative;
margin-bottom: 1rem;
padding: 1rem;
width: fit-content;
}
.wrapper span {
display: block;
position: absolute;
font-family: 'Inter';
font-size: 0.75rem;
color: #fff;
top: calc(3.2mm + 1rem);
right: calc(4mm + 1rem);
}
.wrapper img {
display: block;
width: 187mm;
height: auto;
}
.wrapper .lnurlw {
display: block;
position: absolute;
top: calc(7.3mm + 1rem);
left: calc(7.5mm + 1rem);
transform: rotate(45deg);
}
@media print {
body,
page {
margin: 0px !important;
box-shadow: none !important;
}
.q-page,
.wrapper {
padding: 0px !important;
}
.wrapper span {
top: 3mm;
right: 4mm;
}
.wrapper .lnurlw {
display: block;
position: absolute;
top: 7.3mm;
left: 7.5mm;
transform: rotate(45deg);
}
}
</style>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
data: function () {
return {
theurl: location.protocol + '//' + location.host,
printDialog: {
show: true,
data: null
},
links: []
}
},
created() {
this.links = '{{ link | tojson }}'
}
})
</script>
{% endblock %}

View File

@@ -99,6 +99,18 @@ async def print_qr(request: Request, link_id):
page_link = list(chunks(links, 2)) page_link = list(chunks(links, 2))
linked = list(chunks(page_link, 5)) linked = list(chunks(page_link, 5))
if link.custom_url:
return withdraw_renderer().TemplateResponse(
"withdraw/print_qr_custom.html",
{
"request": request,
"link": page_link,
"unique": True,
"custom_url": link.custom_url,
"amt": link.max_withdrawable,
},
)
return withdraw_renderer().TemplateResponse( return withdraw_renderer().TemplateResponse(
"withdraw/print_qr.html", {"request": request, "link": linked, "unique": True} "withdraw/print_qr.html", {"request": request, "link": linked, "unique": True}
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -0,0 +1,16 @@
<svg width="2000" height="1422" viewBox="0 0 2000 1422" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2_2)">
<rect width="2000" height="1422" fill="#F0F0F0"/>
<line x1="-0.707107" y1="710.293" x2="710.293" y2="-0.707106" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line x1="0.707107" y1="710.293" x2="711.707" y2="1421.29" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line x1="710" y1="-0.00140647" x2="712" y2="1422" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line y1="710" x2="2000" y2="710" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line x1="709.707" y1="-0.707107" x2="1420.71" y2="710.293" stroke="#696969" stroke-opacity="0.5" stroke-width="2"/>
<rect x="26" y="216.454" width="275" height="275" transform="rotate(-45 26 216.454)" fill="white" fill-opacity="0.5"/>
</g>
<defs>
<clipPath id="clip0_2_2">
<rect width="2000" height="1422" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 985 B