mirror of
https://github.com/lnbits/lnbits.git
synced 2025-08-03 15:32:22 +02:00
feat(paywall): improved extension
- make remember cookie optional - improve database - improve type casting
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Paywall",
|
"name": "Paywall",
|
||||||
"short_description": "Create paywalls for content",
|
"short_description": "Create paywalls for content",
|
||||||
"icon": "vpn_lock",
|
"icon": "policy",
|
||||||
"contributors": ["eillarra"]
|
"contributors": ["eillarra"]
|
||||||
}
|
}
|
||||||
|
@@ -6,15 +6,17 @@ from lnbits.helpers import urlsafe_short_hash
|
|||||||
from .models import Paywall
|
from .models import Paywall
|
||||||
|
|
||||||
|
|
||||||
def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall:
|
def create_paywall(
|
||||||
|
*, wallet_id: str, url: str, memo: str, description: Optional[str] = None, amount: int = 0, remembers: bool = True
|
||||||
|
) -> Paywall:
|
||||||
with open_ext_db("paywall") as db:
|
with open_ext_db("paywall") as db:
|
||||||
paywall_id = urlsafe_short_hash()
|
paywall_id = urlsafe_short_hash()
|
||||||
db.execute(
|
db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO paywalls (id, wallet, secret, url, memo, amount)
|
INSERT INTO paywalls (id, wallet, url, memo, description, amount, remembers)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(paywall_id, wallet_id, urlsafe_short_hash(), url, memo, amount),
|
(paywall_id, wallet_id, url, memo, description, amount, int(remembers)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return get_paywall(paywall_id)
|
return get_paywall(paywall_id)
|
||||||
@@ -24,7 +26,7 @@ def get_paywall(paywall_id: str) -> Optional[Paywall]:
|
|||||||
with open_ext_db("paywall") as db:
|
with open_ext_db("paywall") as db:
|
||||||
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
|
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
|
||||||
|
|
||||||
return Paywall(**row) if row else None
|
return Paywall.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
|
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
|
||||||
@@ -35,7 +37,7 @@ def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
|
|||||||
q = ",".join(["?"] * len(wallet_ids))
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
|
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
|
||||||
|
|
||||||
return [Paywall(**row) for row in rows]
|
return [Paywall.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def delete_paywall(paywall_id: str) -> None:
|
def delete_paywall(paywall_id: str) -> None:
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
from sqlite3 import OperationalError
|
||||||
|
|
||||||
from lnbits.db import open_ext_db
|
from lnbits.db import open_ext_db
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +22,52 @@ def m001_initial(db):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def m002_redux(db):
|
||||||
|
"""
|
||||||
|
Creates an improved paywalls table and migrates the existing data.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
db.execute("SELECT remembers FROM paywalls")
|
||||||
|
|
||||||
|
except OperationalError:
|
||||||
|
db.execute("ALTER TABLE paywalls RENAME TO paywalls_old")
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS paywalls (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
memo TEXT NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
amount INTEGER DEFAULT 0,
|
||||||
|
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
remembers INTEGER DEFAULT 0,
|
||||||
|
extras TEXT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON paywalls (wallet)")
|
||||||
|
|
||||||
|
for row in [list(row) for row in db.fetchall("SELECT * FROM paywalls_old")]:
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO paywalls (
|
||||||
|
id,
|
||||||
|
wallet,
|
||||||
|
url,
|
||||||
|
memo,
|
||||||
|
amount,
|
||||||
|
time
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(row[0], row[1], row[3], row[4], row[5], row[6]),
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execute("DROP TABLE paywalls_old")
|
||||||
|
|
||||||
|
|
||||||
def migrate():
|
def migrate():
|
||||||
with open_ext_db("paywall") as db:
|
with open_ext_db("paywall") as db:
|
||||||
m001_initial(db)
|
m001_initial(db)
|
||||||
|
m002_redux(db)
|
||||||
|
@@ -1,11 +1,23 @@
|
|||||||
from typing import NamedTuple
|
import json
|
||||||
|
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
|
|
||||||
class Paywall(NamedTuple):
|
class Paywall(NamedTuple):
|
||||||
id: str
|
id: str
|
||||||
wallet: str
|
wallet: str
|
||||||
secret: str
|
|
||||||
url: str
|
url: str
|
||||||
memo: str
|
memo: str
|
||||||
|
description: str
|
||||||
amount: int
|
amount: int
|
||||||
time: int
|
time: int
|
||||||
|
remembers: bool
|
||||||
|
extras: Optional[dict]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "Paywall":
|
||||||
|
data = dict(row)
|
||||||
|
data["remembers"] = bool(data["remembers"])
|
||||||
|
data["extras"] = json.loads(data["extras"]) if data["extras"] else None
|
||||||
|
return cls(**data)
|
||||||
|
@@ -6,12 +6,106 @@
|
|||||||
>
|
>
|
||||||
<q-expansion-item group="api" dense expand-separator label="List paywalls">
|
<q-expansion-item group="api" dense expand-separator label="List paywalls">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section> </q-card-section>
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">GET</span> /paywall/api/v1/paywalls</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>[<paywall_object>, ...]</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X GET {{ request.url_root }}paywall/api/v1/paywalls -H
|
||||||
|
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-expansion-item group="api" dense expand-separator label="Create a paywall">
|
<q-expansion-item group="api" dense expand-separator label="Create a paywall">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section> </q-card-section>
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-green">POST</span>
|
||||||
|
/paywall/api/v1/paywalls</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
<code
|
||||||
|
>{"amount": <integer>, "description": <string>,
|
||||||
|
"memo": <string>, "remembers": <boolean>,
|
||||||
|
"url": <string>}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 201 CREATED (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>{"amount": <integer>, "description": <string>,
|
||||||
|
"id": <string>, "memo": <string>,
|
||||||
|
"remembers": <boolean>, "time": <int>,
|
||||||
|
"url": <string>, "wallet": <string>}</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X POST {{ request.url_root }}paywall/api/v1/paywalls -d
|
||||||
|
'{"url": <string>, "memo": <string>,
|
||||||
|
"description": <string>, "amount": <integer>,
|
||||||
|
"remembers": <boolean>}' -H
|
||||||
|
"Content-type: application/json" -H "X-Api-Key: {{
|
||||||
|
g.user.wallets[0].adminkey }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Create an invoice (public)">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-green">POST</span>
|
||||||
|
/paywall/api/v1/paywalls/<paywall_id>/invoice</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
<code
|
||||||
|
>{"amount": <integer>}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 201 CREATED (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>{"checking_id": <string>, "payment_request": <string>}</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X POST {{ request.url_root }}paywall/api/v1/paywalls/<paywall_id>/invoice -d
|
||||||
|
'{"amount": <integer>}' -H
|
||||||
|
"Content-type: application/json"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Check invoice status (public)">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-green">POST</span>
|
||||||
|
/paywall/api/v1/paywalls/<paywall_id>/check_invoice</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
<code
|
||||||
|
>{"checking_id": <string>}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>{"paid": false}</code><br>
|
||||||
|
<code>{"paid": true, "url": <string>, "remembers": <boolean>}</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X POST {{ request.url_root }}paywall/api/v1/paywalls/<paywall_id>/check_invoice -d
|
||||||
|
'{"checking_id": <string>}' -H
|
||||||
|
"Content-type: application/json"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
@@ -22,7 +116,22 @@
|
|||||||
class="q-pb-md"
|
class="q-pb-md"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section> </q-card-section>
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-pink">DELETE</span>
|
||||||
|
/paywall/api/v1/paywalls/<paywall_id></code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||||
|
<code></code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X DELETE {{ request.url_root
|
||||||
|
}}paywall/api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{
|
||||||
|
g.user.wallets[0].adminkey }}"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
@@ -1,31 +1,48 @@
|
|||||||
{% extends "public.html" %} {% block page %}
|
{% extends "public.html" %} {% block page %}
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md justify-center">
|
||||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
<div class="col-12 col-sm-8 col-md-5 col-lg-4">
|
||||||
<q-card class="q-pa-lg">
|
<q-card class="q-pa-lg">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h5 class="text-subtitle1 q-my-none">{{ paywall.memo }}</h5>
|
<h5 class="text-subtitle1 q-mt-none q-mb-sm">{{ paywall.memo }}</h5>
|
||||||
<strong class="text-purple"
|
{% if paywall.description %}
|
||||||
>Price:
|
<p>{{ paywall.description }}</p>
|
||||||
<lnbits-fsat :amount="{{ paywall.amount }}"></lnbits-fsat> sat</strong
|
{% endif %}
|
||||||
>
|
<div v-if="!this.redirectUrl" class="q-mt-lg">
|
||||||
<q-separator class="q-my-lg"></q-separator>
|
<q-form v-if="">
|
||||||
<div v-if="paymentReq">
|
<q-input
|
||||||
<a :href="'lightning:' + paymentReq">
|
filled
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
v-model.number="userAmount"
|
||||||
<qrcode
|
type="number"
|
||||||
:value="paymentReq"
|
:min="paywallAmount"
|
||||||
:options="{width: 800}"
|
suffix="sat"
|
||||||
class="rounded-borders"
|
label="Choose an amount *"
|
||||||
></qrcode>
|
:hint="'Minimum ' + paywallAmount + ' sat'"
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn outline color="grey" @click="copyText(paymentReq)"
|
|
||||||
>Copy invoice</q-btn
|
|
||||||
>
|
>
|
||||||
|
<template v-slot:after>
|
||||||
|
<q-btn round dense flat icon="check" color="deep-purple" type="submit" @click="createInvoice" :disabled="userAmount < paywallAmount"></q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</q-form>
|
||||||
|
<div v-if="paymentReq" class="q-mt-lg">
|
||||||
|
<a :href="'lightning:' + paymentReq">
|
||||||
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||||
|
<qrcode
|
||||||
|
:value="paymentReq"
|
||||||
|
:options="{width: 800}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
</a>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn outline color="grey" @click="copyText(paymentReq)"
|
||||||
|
>Copy invoice</q-btn
|
||||||
|
>
|
||||||
|
<q-btn @click="cancelPayment" flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="redirectUrl">
|
<div v-else>
|
||||||
|
<q-separator class="q-my-lg"></q-separator>
|
||||||
<p>
|
<p>
|
||||||
You can access the URL behind this paywall:<br />
|
You can access the URL behind this paywall:<br />
|
||||||
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong>
|
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong>
|
||||||
@@ -39,13 +56,6 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits paywall</h6>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %}
|
{% endblock %} {% block scripts %}
|
||||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||||
@@ -57,25 +67,46 @@
|
|||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
userAmount: {{ paywall.amount }},
|
||||||
|
paywallAmount: {{ paywall.amount }},
|
||||||
paymentReq: null,
|
paymentReq: null,
|
||||||
redirectUrl: null
|
redirectUrl: null,
|
||||||
|
paymentDialog: {
|
||||||
|
dismissMsg: null,
|
||||||
|
checker: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
amount: function () {
|
||||||
|
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getInvoice: function () {
|
cancelPayment: function () {
|
||||||
|
this.paymentReq = null
|
||||||
|
clearInterval(this.paymentDialog.checker)
|
||||||
|
if (this.paymentDialog.dismissMsg) {
|
||||||
|
this.paymentDialog.dismissMsg()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createInvoice: function () {
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.get('/paywall/api/v1/paywalls/{{ paywall.id }}/invoice')
|
.post(
|
||||||
|
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice',
|
||||||
|
{amount: this.amount}
|
||||||
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.paymentReq = response.data.payment_request
|
self.paymentReq = response.data.payment_request.toUpperCase()
|
||||||
|
|
||||||
dismissMsg = self.$q.notify({
|
self.paymentDialog.dismissMsg = self.$q.notify({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Waiting for payment...'
|
message: 'Waiting for payment...'
|
||||||
})
|
})
|
||||||
|
|
||||||
paymentChecker = setInterval(function () {
|
self.paymentDialog.checker = setInterval(function () {
|
||||||
axios
|
axios
|
||||||
.post(
|
.post(
|
||||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
|
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
|
||||||
@@ -83,13 +114,14 @@
|
|||||||
)
|
)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.data.paid) {
|
if (res.data.paid) {
|
||||||
clearInterval(paymentChecker)
|
self.cancelPayment()
|
||||||
dismissMsg()
|
|
||||||
self.redirectUrl = res.data.url
|
self.redirectUrl = res.data.url
|
||||||
self.$q.localStorage.set(
|
if (res.data.remembers) {
|
||||||
'lnbits.paywall.{{ paywall.id }}',
|
self.$q.localStorage.set(
|
||||||
res.data.url
|
'lnbits.paywall.{{ paywall.id }}',
|
||||||
)
|
res.data.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
self.$q.notify({
|
self.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
@@ -113,8 +145,6 @@
|
|||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
this.redirectUrl = url
|
this.redirectUrl = url
|
||||||
} else {
|
|
||||||
this.getInvoice()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -114,7 +114,21 @@
|
|||||||
dense
|
dense
|
||||||
v-model.trim="formDialog.data.url"
|
v-model.trim="formDialog.data.url"
|
||||||
type="url"
|
type="url"
|
||||||
label="Target URL *"
|
label="Redirect URL *"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.memo"
|
||||||
|
label="Title *"
|
||||||
|
placeholder="LNbits paywall"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
autogrow
|
||||||
|
v-model.trim="formDialog.data.description"
|
||||||
|
label="Description"
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
@@ -122,19 +136,31 @@
|
|||||||
v-model.number="formDialog.data.amount"
|
v-model.number="formDialog.data.amount"
|
||||||
type="number"
|
type="number"
|
||||||
label="Amount (sat) *"
|
label="Amount (sat) *"
|
||||||
|
hint="This is the minimum amount users can pay/donate."
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-input
|
<q-list>
|
||||||
filled
|
<q-item tag="label" class="rounded-borders">
|
||||||
dense
|
<q-item-section avatar>
|
||||||
v-model.trim="formDialog.data.memo"
|
<q-checkbox
|
||||||
label="Memo"
|
v-model="formDialog.data.remembers"
|
||||||
placeholder="LNbits invoice"
|
color="deep-purple"
|
||||||
></q-input>
|
></q-checkbox>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
>Remember payments</q-item-label
|
||||||
|
>
|
||||||
|
<q-item-label caption
|
||||||
|
>A succesful payment will be registered in the browser's storage, so the user doesn't need to pay again to access the URL.</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="deep-purple"
|
color="deep-purple"
|
||||||
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null"
|
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null || formDialog.data.memo == null"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Create paywall</q-btn
|
>Create paywall</q-btn
|
||||||
>
|
>
|
||||||
@@ -168,13 +194,6 @@
|
|||||||
columns: [
|
columns: [
|
||||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Date',
|
|
||||||
field: 'date',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'amount',
|
name: 'amount',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
@@ -184,6 +203,14 @@
|
|||||||
sort: function (a, b, rowA, rowB) {
|
sort: function (a, b, rowA, rowB) {
|
||||||
return rowA.amount - rowB.amount
|
return rowA.amount - rowB.amount
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{name: 'remembers', align: 'left', label: 'Remember', field: 'remembers'},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Date',
|
||||||
|
field: 'date',
|
||||||
|
sortable: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -192,7 +219,9 @@
|
|||||||
},
|
},
|
||||||
formDialog: {
|
formDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {
|
||||||
|
remembers: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -216,7 +245,9 @@
|
|||||||
var data = {
|
var data = {
|
||||||
url: this.formDialog.data.url,
|
url: this.formDialog.data.url,
|
||||||
memo: this.formDialog.data.memo,
|
memo: this.formDialog.data.memo,
|
||||||
amount: this.formDialog.data.amount
|
amount: this.formDialog.data.amount,
|
||||||
|
description: this.formDialog.data.description,
|
||||||
|
remembers: this.formDialog.data.remembers
|
||||||
}
|
}
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
@@ -231,7 +262,9 @@
|
|||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.paywalls.push(mapPaywall(response.data))
|
self.paywalls.push(mapPaywall(response.data))
|
||||||
self.formDialog.show = false
|
self.formDialog.show = false
|
||||||
self.formDialog.data = {}
|
self.formDialog.data = {
|
||||||
|
remembers: false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
|
@@ -27,7 +27,9 @@ def api_paywalls():
|
|||||||
schema={
|
schema={
|
||||||
"url": {"type": "string", "empty": False, "required": True},
|
"url": {"type": "string", "empty": False, "required": True},
|
||||||
"memo": {"type": "string", "empty": False, "required": True},
|
"memo": {"type": "string", "empty": False, "required": True},
|
||||||
|
"description": {"type": "string", "empty": True, "nullable": True, "required": False},
|
||||||
"amount": {"type": "integer", "min": 0, "required": True},
|
"amount": {"type": "integer", "min": 0, "required": True},
|
||||||
|
"remembers": {"type": "boolean", "required": True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def api_paywall_create():
|
def api_paywall_create():
|
||||||
@@ -52,18 +54,23 @@ def api_paywall_delete(paywall_id):
|
|||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["GET"])
|
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"])
|
||||||
def api_paywall_get_invoice(paywall_id):
|
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
|
||||||
|
def api_paywall_create_invoice(paywall_id):
|
||||||
paywall = get_paywall(paywall_id)
|
paywall = get_paywall(paywall_id)
|
||||||
|
|
||||||
|
if g.data["amount"] < paywall.amount:
|
||||||
|
return jsonify({"message": f"Minimum amount is {paywall.amount} sat."}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount
|
||||||
checking_id, payment_request = create_invoice(
|
checking_id, payment_request = create_invoice(
|
||||||
wallet_id=paywall.wallet, amount=paywall.amount, memo=f"#paywall {paywall.memo}"
|
wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
|
||||||
|
|
||||||
|
|
||||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
|
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
|
||||||
@@ -84,6 +91,6 @@ def api_paywal_check_invoice(paywall_id):
|
|||||||
payment = wallet.get_payment(g.data["checking_id"])
|
payment = wallet.get_payment(g.data["checking_id"])
|
||||||
payment.set_pending(False)
|
payment.set_pending(False)
|
||||||
|
|
||||||
return jsonify({"paid": True, "url": paywall.url}), HTTPStatus.OK
|
return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK
|
||||||
|
|
||||||
return jsonify({"paid": False}), HTTPStatus.OK
|
return jsonify({"paid": False}), HTTPStatus.OK
|
||||||
|
Reference in New Issue
Block a user