Add voucher design (#704)

* allow for custom print of vouchers

* lnbits defaul voucher

* reduce png size

* add template for custom design

* rename lnbits voucher

* send voucher amount to UI

* add some tweaks for default voucher

* added bonus in readme

* blacked

* make default string as constant
This commit is contained in:
Tiago Vasconcelos 2022-07-25 10:22:21 +01:00 committed by GitHub
parent ff5b1189b5
commit 7fbea79e62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 224 additions and 7 deletions

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