Now you can have multiple themes

This commit is contained in:
ben
2022-11-24 14:54:19 +00:00
committed by dni ⚡
parent b2e8bed24c
commit 700196a5a9
10 changed files with 305 additions and 95 deletions

View File

@@ -2,7 +2,5 @@
"name": "SatsPay Server", "name": "SatsPay Server",
"short_description": "Create onchain and LN charges", "short_description": "Create onchain and LN charges",
"icon": "payment", "icon": "payment",
"contributors": [ "contributors": ["arcbtc"]
"arcbtc"
]
} }

View File

@@ -10,7 +10,8 @@ from lnbits.helpers import urlsafe_short_hash
from ..watchonly.crud import get_config, get_fresh_address from ..watchonly.crud import get_config, get_fresh_address
from . import db from . import db
from .helpers import fetch_onchain_balance from .helpers import fetch_onchain_balance
from .models import Charges, CreateCharge, SatsPaySettings from .models import Charges, CreateCharge, SatsPayThemes
###############CHARGES########################## ###############CHARGES##########################
@@ -53,7 +54,8 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
time, time,
amount, amount,
balance, balance,
extra extra,
custom_css,
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
@@ -71,6 +73,7 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
data.completelinktext, data.completelinktext,
data.time, data.time,
data.amount, data.amount,
data.custom_css,
0, 0,
data.extra, data.extra,
), ),
@@ -124,41 +127,51 @@ async def check_address_balance(charge_id: str) -> Optional[Charges]:
################## SETTINGS ################### ################## SETTINGS ###################
async def save_settings(user_id: str, data: SatsPaySettings):
async def save_theme(data: SatsPayThemes, css_id: str = None):
# insert or update # insert or update
row = await db.fetchone( if css_id:
"""SELECT user_id FROM satspay.settings WHERE user_id = ?""", (user_id,)
)
if row:
await db.execute( await db.execute(
""" """
UPDATE satspay.settings SET custom_css = ? WHERE user_id = ? UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
""", """,
(data.custom_css, user_id), (data.custom_css, data.title, css_id),
) )
else: else:
css_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
INSERT INTO satspay.settings ( INSERT INTO satspay.themes (
user_id, css_id,
title,
user,
custom_css custom_css
) )
VALUES (?, ?) VALUES (?, ?, ?, ?)
""", """,
( (
user_id, css_id,
data.title,
data.user,
data.custom_css, data.custom_css,
), ),
) )
return True return await get_theme(css_id)
async def get_settings(user_id: str) -> SatsPaySettings: async def get_theme(css_id: str) -> SatsPayThemes:
row = await db.fetchone( row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
"""SELECT * FROM satspay.settings WHERE user_id = ?""", return SatsPayThemes.from_row(row) if row else None
async def get_themes(user_id: str) -> List[SatsPayThemes]:
rows = await db.fetchall(
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "timestamp" DESC """,
(user_id,), (user_id,),
) )
if row: return [SatsPayThemes.from_row(row) for row in rows]
return SatsPaySettings.from_row(row)
else:
return None async def delete_theme(theme_id: str) -> None:
await db.execute("DELETE FROM satspay.themes WHERE css_id = ?", (theme_id,))

View File

@@ -38,16 +38,26 @@ async def m002_add_charge_extra_data(db):
""" """
) )
async def m002_add_settings_table(db): async def m002_add_themes_table(db):
""" """
Settings table Themes table
""" """
await db.execute( await db.execute(
""" """
CREATE TABLE satspay.settings ( CREATE TABLE satspay.themes (
user_id TEXT, css_id TEXT,
user TEXT,
title TEXT,
custom_css TEXT custom_css TEXT
); );
""" """
) )
async def m003_add_custom_css_to_charges(db):
"""
Add custom css option column to the 'charges' table
"""
await db.execute("ALTER TABLE satspay.charges ADD COLUMN custom_css TEXT;")

View File

@@ -14,6 +14,7 @@ class CreateCharge(BaseModel):
webhook: str = Query(None) webhook: str = Query(None)
completelink: str = Query(None) completelink: str = Query(None)
completelinktext: str = Query(None) completelinktext: str = Query(None)
custom_css: Optional[str]
time: int = Query(..., ge=1) time: int = Query(..., ge=1)
amount: int = Query(..., ge=1) amount: int = Query(..., ge=1)
extra: str = "{}" extra: str = "{}"
@@ -38,6 +39,7 @@ class Charges(BaseModel):
completelink: Optional[str] completelink: Optional[str]
completelinktext: Optional[str] = "Back to Merchant" completelinktext: Optional[str] = "Back to Merchant"
extra: str = "{}" extra: str = "{}"
custom_css: Optional[str]
time: int time: int
amount: int amount: int
balance: int balance: int
@@ -73,9 +75,12 @@ class Charges(BaseModel):
def must_call_webhook(self): def must_call_webhook(self):
return self.webhook and self.paid and self.config.webhook_success == False return self.webhook and self.paid and self.config.webhook_success == False
class SatsPaySettings(BaseModel): class SatsPayThemes(BaseModel):
css_id: str = Query(None)
title: str = Query(None)
custom_css: str = Query(None) custom_css: str = Query(None)
user: Optional[str]
@classmethod @classmethod
def from_row(cls, row: Row) -> "SatsPaySettings": def from_row(cls, row: Row) -> "SatsPayThemes":
return cls(**dict(row)) return cls(**dict(row))

View File

@@ -26,5 +26,10 @@ const mapCharge = (obj, oldObj = {}) => {
return charge return charge
} }
const mapCSS = (obj, oldObj = {}) => {
const theme = _.clone(obj)
return theme
}
const minutesToTime = min => const minutesToTime = min =>
min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : '' min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : ''

View File

@@ -5,7 +5,13 @@
WatchOnly extension, we highly reccomend using a fresh extended public Key WatchOnly extension, we highly reccomend using a fresh extended public Key
specifically for SatsPayServer!<br /> specifically for SatsPayServer!<br />
<small> <small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small Created by, <a href="https://github.com/benarc">Ben Arc</a>,
<a
target="_blank"
style="color: unset"
href="https://github.com/motorina0"
>motorina0</a
></small
> >
</p> </p>
<br /> <br />

View File

@@ -299,7 +299,7 @@
</div> </div>
{% endblock %} {% block styles %} {% endblock %} {% block styles %}
<link <link
href="/satspay/css/{{ charge_data.id }}" href="/satspay/css/{{ charge_data.custom_css }}"
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
/> />
@@ -324,6 +324,7 @@
charge: JSON.parse('{{charge_data | tojson}}'), charge: JSON.parse('{{charge_data | tojson}}'),
mempoolEndpoint: '{{mempool_endpoint}}', mempoolEndpoint: '{{mempool_endpoint}}',
network: '{{network}}', network: '{{network}}',
css_id: '{{ charge_data.css_id }}',
pendingFunds: 0, pendingFunds: 0,
ws: null, ws: null,
newProgress: 0.4, newProgress: 0.4,
@@ -462,8 +463,10 @@
await this.getCustomCss() await this.getCustomCss()
if (this.charge.payment_request) this.payInvoice() if (this.charge.payment_request) this.payInvoice()
// Remove a user defined theme // Remove a user defined theme
document.body.setAttribute('data-theme', '') console.log(this.charge.custom_css)
if (this.charge.custom_css) {
document.body.setAttribute('data-theme', '')
}
if (this.charge.lnbitswallet) this.payInvoice() if (this.charge.lnbitswallet) this.payInvoice()
else this.payOnchain() else this.payOnchain()

View File

@@ -12,8 +12,8 @@
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
@click="getSettings();formDialogSettings.show = true" @click="getThemes();formDialogThemes.show = true"
>SatsPay settings >New CSS Theme
</q-btn> </q-btn>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -266,6 +266,63 @@
</q-table> </q-table>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Themes</h5>
</div>
</div>
<q-table
dense
flat
:data="themeLinks"
row-key="id"
:columns="customCSSTable.columns"
:pagination.sync="customCSSTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateformDialog(props.row.css_id)"
icon="edit"
color="light-blue"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTheme(props.row.css_id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div> </div>
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-5 q-gutter-y-md">
@@ -384,6 +441,15 @@
label="Wallet *" label="Wallet *"
> >
</q-select> </q-select>
<q-select
filled
dense
emit-value
v-model="formDialogCharge.data.custom_css"
:options="themeOptions"
label="Custom CSS theme (optional)"
>
</q-select>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
unelevated unelevated
@@ -402,23 +468,36 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="formDialogSettings.show" position="top"> <q-dialog v-model="formDialogThemes.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDataSettings" class="q-gutter-md"> <q-form @submit="sendFormDataThemes" class="q-gutter-md">
<q-input <q-input
filled filled
dense dense
v-model.trim="formDialogSettings.data.custom_css" v-model.trim="formDialogThemes.data.title"
type="text"
label="*Title"
></q-input>
<q-input
filled
dense
v-model.trim="formDialogThemes.data.custom_css"
type="textarea" type="textarea"
label="Custom CSS" label="Custom CSS"
> >
<q-tooltip
>Custom CSS to apply styles to your SatsPay invoice</q-tooltip
>
</q-input> </q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Save Settings</q-btn> <q-btn
<q-btn @click="cancelSettings" flat color="grey" class="q-ml-auto" v-if="formDialogThemes.data.css_id"
unelevated
color="primary"
type="submit"
>Update CSS theme</q-btn
>
<q-btn v-else unelevated color="primary" type="submit"
>Save CSS theme</q-btn
>
<q-btn @click="cancelThemes" flat color="grey" class="q-ml-auto"
>Cancel</q-btn >Cancel</q-btn
> >
</div> </div>
@@ -446,7 +525,9 @@
balance: null, balance: null,
walletLinks: [], walletLinks: [],
chargeLinks: [], chargeLinks: [],
onchainwallet: null, themeLinks: [],
themeOptions: [],
onchainwallet: '',
rescanning: false, rescanning: false,
mempool: { mempool: {
endpoint: '', endpoint: '',
@@ -526,7 +607,25 @@
rowsPerPage: 10 rowsPerPage: 10
} }
}, },
customCSSTable: {
columns: [
{
name: 'css_id',
align: 'left',
label: 'ID',
field: 'css_id'
},
{
name: 'title',
align: 'left',
label: 'Title',
field: 'title'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialogCharge: { formDialogCharge: {
show: false, show: false,
data: { data: {
@@ -534,11 +633,12 @@
onchainwallet: '', onchainwallet: '',
lnbits: false, lnbits: false,
description: '', description: '',
custom_css: '',
time: null, time: null,
amount: null amount: null
} }
}, },
formDialogSettings: { formDialogThemes: {
show: false, show: false,
data: { data: {
custom_css: '' custom_css: ''
@@ -547,9 +647,9 @@
} }
}, },
methods: { methods: {
cancelSettings: function (data) { cancelThemes: function (data) {
this.formDialogCharge.data.custom_css = '' this.formDialogCharge.data.custom_css = ''
this.formDialogSettings.show = false this.formDialogThemes.show = false
}, },
cancelCharge: function (data) { cancelCharge: function (data) {
this.formDialogCharge.data.description = '' this.formDialogCharge.data.description = ''
@@ -559,6 +659,7 @@
this.formDialogCharge.data.time = null this.formDialogCharge.data.time = null
this.formDialogCharge.data.amount = null this.formDialogCharge.data.amount = null
this.formDialogCharge.data.webhook = '' this.formDialogCharge.data.webhook = ''
this.formDialogCharge.data.custom_css = ''
this.formDialogCharge.data.completelink = '' this.formDialogCharge.data.completelink = ''
this.formDialogCharge.show = false this.formDialogCharge.show = false
}, },
@@ -623,30 +724,38 @@
} }
}, },
getSettings: async function () { getThemes: async function () {
try { try {
const {data} = await LNbits.api.request( const {data} = await LNbits.api.request(
'GET', 'GET',
'/satspay/api/v1/settings', '/satspay/api/v1/themes',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
if (data) { console.log(data)
this.formDialogSettings.data.custom_css = data.custom_css this.themeLinks = data.map(c =>
} mapCSS(
c,
this.themeLinks.find(old => old.css_id === c.css_id)
)
)
this.themeOptions = data.map(w => ({
id: w.css_id,
label: w.title + ' - ' + w.css_id
}))
} catch (error) { } catch (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
} }
}, },
sendFormDataSettings: function () { sendFormDataThemes: function () {
const wallet = this.g.user.wallets[0].inkey const wallet = this.g.user.wallets[0].inkey
const data = this.formDialogSettings.data const data = this.formDialogThemes.data
data.custom_css = data.custom_css this.createTheme(wallet, data)
this.saveSettings(wallet, data)
}, },
sendFormDataCharge: function () { sendFormDataCharge: function () {
const wallet = this.g.user.wallets[0].inkey this.formDialogCharge.data.custom_css = this.formDialogCharge.data.custom_css.id
const data = this.formDialogCharge.data const data = this.formDialogCharge.data
const wallet = this.g.user.wallets[0].inkey
data.amount = parseInt(data.amount) data.amount = parseInt(data.amount)
data.time = parseInt(data.time) data.time = parseInt(data.time)
data.lnbitswallet = data.lnbits ? data.lnbitswallet : null data.lnbitswallet = data.lnbits ? data.lnbitswallet : null
@@ -715,23 +824,68 @@
this.rescanning = false this.rescanning = false
} }
}, },
saveSettings: async function (wallet, data) { updateformDialog: function (themeId) {
const theme = _.findWhere(this.themeLinks, {css_id: themeId})
console.log(theme.css_id)
this.formDialogThemes.data.css_id = theme.css_id
this.formDialogThemes.data.title = theme.title
this.formDialogThemes.data.custom_css = theme.custom_css
this.formDialogThemes.show = true
},
createTheme: async function (wallet, data) {
console.log(data.css_id)
try { try {
const resp = await LNbits.api.request( if (data.css_id) {
'POST', const resp = await LNbits.api.request(
'/satspay/api/v1/settings', 'POST',
wallet, '/satspay/api/v1/themes/' + data.css_id,
data wallet,
) data
)
this.formDialogSettings.show = false this.themeLinks = _.reject(this.themeLinks, function (obj) {
this.formDialogSettings.data = { return obj.css_id === data.css_id
})
this.themeLinks.unshift(mapCSS(resp.data))
} else {
const resp = await LNbits.api.request(
'POST',
'/satspay/api/v1/themes',
wallet,
data
)
this.themeLinks.unshift(mapCSS(resp.data))
}
this.formDialogThemes.show = false
this.formDialogThemes.data = {
title: '',
custom_css: '' custom_css: ''
} }
} catch (error) { } catch (error) {
console.log('cun')
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
} }
}, },
deleteTheme: function (themeId) {
const theme = _.findWhere(this.themeLinks, {id: themeId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this theme?')
.onOk(async () => {
try {
const response = await LNbits.api.request(
'DELETE',
'/satspay/api/v1/themes/' + themeId,
this.g.user.wallets[0].adminkey
)
this.themeLinks = _.reject(this.themeLinks, function (obj) {
return obj.css_id === themeId
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
})
},
createCharge: async function (wallet, data) { createCharge: async function (wallet, data) {
try { try {
const resp = await LNbits.api.request( const resp = await LNbits.api.request(
@@ -784,7 +938,7 @@
} }
}, },
created: async function () { created: async function () {
await this.getSettings() await this.getThemes()
await this.getCharges() await this.getCharges()
await this.getWalletConfig() await this.getWalletConfig()
await this.getWalletLinks() await this.getWalletLinks()

View File

@@ -12,7 +12,7 @@ from lnbits.decorators import check_user_exists
from lnbits.extensions.satspay.helpers import public_charge from lnbits.extensions.satspay.helpers import public_charge
from . import satspay_ext, satspay_renderer from . import satspay_ext, satspay_renderer
from .crud import get_charge, get_charge_config, get_settings from .crud import get_charge, get_charge_config, get_themes, get_theme
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@@ -43,16 +43,10 @@ async def display(request: Request, charge_id: str):
) )
@satspay_ext.get("/css/{charge_id}") @satspay_ext.get("/css/{css_id}")
async def display(charge_id: str, response: Response): async def display(css_id: str, response: Response):
charge = await get_charge(charge_id) theme = await get_theme(css_id)
if not charge: if theme:
raise HTTPException( return Response(content=theme.custom_css, media_type="text/css")
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
)
wallet = await get_wallet(charge.lnbitswallet)
settings = await get_settings(wallet.user)
if settings:
return Response(content=settings.custom_css, media_type="text/css")
return None return None

View File

@@ -21,13 +21,15 @@ from .crud import (
delete_charge, delete_charge,
get_charge, get_charge,
get_charges, get_charges,
get_settings, get_theme,
save_settings, get_themes,
delete_theme,
save_theme,
update_charge, update_charge,
) )
from .helpers import call_webhook, public_charge from .helpers import call_webhook, public_charge
from .helpers import compact_charge from .helpers import compact_charge
from .models import CreateCharge, SatsPaySettings from .models import CreateCharge, SatsPayThemes
#############################CHARGES########################## #############################CHARGES##########################
@@ -145,22 +147,42 @@ async def api_charge_balance(charge_id):
return {**public_charge(charge)} return {**public_charge(charge)}
#############################CHARGES########################## #############################THEMES##########################
@satspay_ext.post("/api/v1/settings") @satspay_ext.post("/api/v1/themes")
async def api_settings_save( @satspay_ext.post("/api/v1/themes/{css_id}")
data: SatsPaySettings, wallet: WalletTypeInfo = Depends(require_invoice_key) async def api_themes_save(
data: SatsPayThemes,
wallet: WalletTypeInfo = Depends(require_invoice_key),
css_id: str = None,
): ):
await save_settings(user_id=wallet.wallet.user, data=data) if css_id:
return True theme = await save_theme(css_id=css_id, data=data)
else:
data.user = wallet.wallet.user
theme = await save_theme(data=data)
return theme
@satspay_ext.get("/api/v1/settings") @satspay_ext.get("/api/v1/themes")
async def api_settings_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_themes_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
try: try:
return await get_settings(wallet.wallet.user) return await get_themes(wallet.wallet.user)
except HTTPException: except HTTPException:
logger.error("Error loading satspay settings") logger.error("Error loading satspay themes")
logger.error(HTTPException) logger.error(HTTPException)
return "" return ""
@satspay_ext.delete("/api/v1/themes/{theme_id}")
async def api_charge_delete(theme_id, wallet: WalletTypeInfo = Depends(get_key_type)):
theme = await get_theme(theme_id)
if not theme:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Theme does not exist."
)
await delete_theme(theme_id)
return "", HTTPStatus.NO_CONTENT