Compare commits

...
Author SHA1 Message Date
Tiago Vasconcelos 9a9e638c1e fix: separate concerns 2026-03-19 11:13:32 +00:00
Tiago Vasconcelos 59a2fefbf3 chore: lint & bundle 2026-03-19 09:59:17 +00:00
Tiago Vasconcelos 4725eb95c9 users page dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos a5aaced420 node page dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 87a6514b62 some extensions dialogs
some need a bit more work
2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 32c893027d audit detail dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 751598e0f8 remove margin in title 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos e11ccd9757 account dialogs 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 7e344de10e funding sources qr 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 08fdeedffc exchange ticker dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 73ba9d94a5 wallet icon dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 1640e23f75 new wallet dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 9145c13435 qr scanner dialog 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos 41048b8cc7 icon prop takes real estate 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos b46e2c549d hodl invoice 2026-03-19 09:54:27 +00:00
Tiago Vasconcelos b382ccfdf3 add optional secondary action button 2026-03-19 09:53:22 +00:00
Tiago Vasconcelos 1687972d89 payment details 2026-03-19 09:53:22 +00:00
Tiago Vasconcelos d621a6f81f . 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos b9d00d900e prettier 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos 0f06b7ba52 chore: bundle 2026-03-19 09:48:45 +00:00
Tiago Vasconcelos 220ece6027 fix mixed bindings 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 1663845468 remove action slot 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 5b7d1e63b5 test fixes 2026-03-19 09:47:57 +00:00
Tiago Vasconcelos 5bd8bf2f56 init 2026-03-19 09:47:57 +00:00
Vlad StanandGitHub a3d7b463ae fix: increase lndrest timeout (#3799) 2026-03-19 11:29:54 +02:00
dni ⚡andGitHub e1e2112461 test: on_paid listeners using a mock (#3802) 2026-03-19 11:29:05 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Vlad Stan
92fc9a130f chore(deps): bump underscore from 1.13.7 to 1.13.8 (#3843)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-03-19 11:11:06 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e15f3cd207 chore(deps-dev): bump immutable from 5.1.4 to 5.1.5 (#3853)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 10:52:08 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
041b358310 chore(deps): bump tornado from 6.5.2 to 6.5.5 (#3866)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 10:38:14 +02:00
dni ⚡andGitHub cbd3de88c4 refactor: remove unused Jinja2Templates super class (#3803) 2026-03-18 16:28:51 +02:00
blackcoffeexbtandGitHub 156ab10ad6 fix: Sanitise lightning addresses by casing in lnurlscan (#3856) 2026-03-18 16:15:38 +02:00
28 changed files with 1137 additions and 1160 deletions
+2
View File
@@ -36,6 +36,8 @@ lnurl_router = APIRouter(tags=["LNURL"])
async def _handle(lnurl: str) -> LnurlResponseModel: async def _handle(lnurl: str) -> LnurlResponseModel:
try: try:
if "@" in lnurl: # lower case lightning addresses
lnurl = lnurl.lower()
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5) res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
if isinstance(res, LnurlErrorResponse): if isinstance(res, LnurlErrorResponse):
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason) raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
+2 -3
View File
@@ -7,15 +7,14 @@ from typing import Any
from urllib import request from urllib import request
from urllib.parse import urlparse from urllib.parse import urlparse
import jinja2
import jwt import jwt
import shortuuid import shortuuid
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from loguru import logger from loguru import logger
from packaging import version from packaging import version
from pydantic.schema import field_schema from pydantic.schema import field_schema
from starlette.templating import Jinja2Templates
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher from lnbits.utils.crypto import AESCipher
from lnbits.utils.exchange_rates import currencies from lnbits.utils.exchange_rates import currencies
@@ -66,7 +65,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
for f in additional_folders for f in additional_folders
] ]
folders.extend(additional_folders) folders.extend(additional_folders)
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders)) t = Jinja2Templates(directory=folders)
t.env.globals["static_url_for"] = static_url_for t.env.globals["static_url_for"] = static_url_for
t.env.globals["normalize_path"] = normalize_path t.env.globals["normalize_path"] = normalize_path
-28
View File
@@ -1,28 +0,0 @@
import typing
from jinja2 import BaseLoader, Environment, pass_context
from starlette.datastructures import QueryParams
from starlette.requests import Request
from starlette.templating import Jinja2Templates as SuperJinja2Templates
class Jinja2Templates(SuperJinja2Templates):
def __init__(self, loader: BaseLoader) -> None:
self.env = self.get_environment(loader)
super().__init__(env=self.env)
def get_environment(self, loader: BaseLoader) -> Environment:
@pass_context
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
request: Request = context["request"]
return request.app.url_path_for(name, **path_params)
def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams:
values = dict(init)
values.update(new)
return QueryParams(**values)
env = Environment(loader=loader, autoescape=True)
env.globals["url_for"] = url_for
env.globals["url_params_update"] = url_params_update
return env
+5 -2
View File
@@ -76,7 +76,7 @@ class LndRestNode(Node):
return response.json() return response.json()
def get(self, path: str, **kwargs): def get(self, path: str, **kwargs):
return self.request("GET", path, **kwargs) return self.request("GET", path, timeout=30, **kwargs)
async def _get_id(self) -> str: async def _get_id(self) -> str:
info = await self.get("/v1/getinfo") info = await self.get("/v1/getinfo")
@@ -99,11 +99,12 @@ class LndRestNode(Node):
"perm": True, "perm": True,
"timeout": 30, "timeout": 30,
}, },
timeout=30,
) )
async def disconnect_peer(self, peer_id: str): async def disconnect_peer(self, peer_id: str):
try: try:
await self.request("DELETE", "/v1/peers/" + peer_id) await self.request("DELETE", "/v1/peers/" + peer_id, timeout=30)
except HTTPException as exc: except HTTPException as exc:
if "unable to disconnect" in exc.detail: if "unable to disconnect" in exc.detail:
raise HTTPException( raise HTTPException(
@@ -141,6 +142,7 @@ class LndRestNode(Node):
"local_funding_amount": local_amount, "local_funding_amount": local_amount,
"push_sat": push_amount, "push_sat": push_amount,
}, },
timeout=30,
) )
return ChannelPoint( return ChannelPoint(
# WHY IS THIS REVERSED?! # WHY IS THIS REVERSED?!
@@ -214,6 +216,7 @@ class LndRestNode(Node):
# 'min_htlc_msat': <uint64>, # 'min_htlc_msat': <uint64>,
# 'inbound_fee': <InboundFee>, # 'inbound_fee': <InboundFee>,
}, },
timeout=30,
) )
async def get_channel(self, channel_id: str) -> NodeChannel | None: async def get_channel(self, channel_id: str) -> NodeChannel | None:
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,105 @@
window.app.component('lnbits-dialog', {
template: '#lnbits-dialog',
props: {
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
position: {
type: String,
default: 'top'
},
persistent: {
type: Boolean,
default: false
},
showCancel: {
type: Boolean,
default: true
},
cancelLabel: {
type: String,
default: 'Close'
},
cancelColor: {
type: String,
default: 'grey'
},
action: {
type: Object,
default: null
},
secondaryAction: {
type: Object,
default: null
}
},
emits: ['update:show', 'hide', 'cancel', 'action', 'secondary-action'],
computed: {
hasAction() {
return !!(this.action && this.action.label)
},
hasSecondaryAction() {
return !!(this.secondaryAction && this.secondaryAction.label)
},
actionProps() {
const action = this.action || {}
const obj = {
label: action.label || 'Action',
color: action.color || 'primary',
loading: !!action.loading,
disable: !!action.disable,
closePopup: !!action.closePopup
}
if (action.icon) {
obj.icon = action.icon
}
return obj
},
secondaryActionProps() {
const action = this.secondaryAction || {}
const obj = {
label: action.label || 'Secondary',
color: action.color || 'secondary',
loading: !!action.loading,
disable: !!action.disable,
closePopup: !!action.closePopup
}
if (action.icon) {
obj.icon = action.icon
}
return obj
}
},
methods: {
handleModelUpdate(value) {
this.$emit('update:show', value)
},
handleHide() {
this.handleModelUpdate(false)
this.$emit('hide')
},
handleCancel() {
this.$emit('cancel')
},
handleAction() {
this.$emit('action')
},
handleSecondaryAction() {
this.$emit('secondary-action')
}
}
})
+1
View File
@@ -70,6 +70,7 @@
"js/components/admin/lnbits-admin-site-customisation.js", "js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-assets-config.js", "js/components/admin/lnbits-admin-assets-config.js",
"js/components/admin/lnbits-admin-audit.js", "js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-dialog.js",
"js/components/lnbits-wallet-charts.js", "js/components/lnbits-wallet-charts.js",
"js/components/lnbits-wallet-api-docs.js", "js/components/lnbits-wallet-api-docs.js",
"js/components/lnbits-wallet-icon.js", "js/components/lnbits-wallet-icon.js",
+69 -52
View File
@@ -7,13 +7,13 @@
exports.noConflict = function () { global._ = current; return exports; }; exports.noConflict = function () { global._ = current; return exports; };
}())); }()));
}(this, (function () { }(this, (function () {
// Underscore.js 1.13.7 // Underscore.js 1.13.8
// https://underscorejs.org // https://underscorejs.org
// (c) 2009-2024 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors // (c) 2009-2026 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license. // Underscore may be freely distributed under the MIT license.
// Current version. // Current version.
var VERSION = '1.13.7'; var VERSION = '1.13.8';
// Establish the root object, `window` (`self`) in the browser, `global` // Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self` // on the server, or `this` in some virtual machines. We use `self`
@@ -357,23 +357,41 @@
// We use this string twice, so give it a name for minification. // We use this string twice, so give it a name for minification.
var tagDataView = '[object DataView]'; var tagDataView = '[object DataView]';
// Internal recursive comparison function for `_.isEqual`. // Perform a deep comparison to check if two objects are equal.
function eq(a, b, aStack, bStack) { function isEqual(a, b) {
var todo = [{a: a, b: b}];
// Initializing stacks of traversed objects for cycle detection.
var aStack = [], bStack = [];
while (todo.length) {
var frame = todo.pop();
if (frame === true) {
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
continue;
}
a = frame.a;
b = frame.b;
// Identical objects are equal. `0 === -0`, but they aren't identical. // Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a === 1 / b; if (a === b) {
if (a !== 0 || 1 / a === 1 / b) continue;
return false;
}
// `null` or `undefined` only equal to itself (strict comparison). // `null` or `undefined` only equal to itself (strict comparison).
if (a == null || b == null) return false; if (a == null || b == null) return false;
// `NaN`s are equivalent, but non-reflexive. // `NaN`s are equivalent, but non-reflexive.
if (a !== a) return b !== b; if (a !== a) {
if (b !== b) continue;
return false;
}
// Exhaust primitive checks // Exhaust primitive checks
var type = typeof a; var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
return deepEq(a, b, aStack, bStack);
}
// Internal recursive comparison function for `_.isEqual`. // Internal recursive comparison function for `_.isEqual`.
function deepEq(a, b, aStack, bStack) {
// Unwrap any wrapped objects. // Unwrap any wrapped objects.
if (a instanceof _$1) a = a._wrapped; if (a instanceof _$1) a = a._wrapped;
if (b instanceof _$1) b = b._wrapped; if (b instanceof _$1) b = b._wrapped;
@@ -392,32 +410,33 @@
case '[object String]': case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`. // equivalent to `new String("5")`.
return '' + a === '' + b; if ('' + a === '' + b) continue;
return false;
case '[object Number]': case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. todo.push({a: +a, b: +b});
// Object(NaN) is equivalent to NaN. continue;
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]': case '[object Date]':
case '[object Boolean]': case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their // Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations // millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent. // of `NaN` are not equivalent.
return +a === +b; if (+a === +b) continue;
return false;
case '[object Symbol]': case '[object Symbol]':
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); if (SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b)) continue;
return false;
case '[object ArrayBuffer]': case '[object ArrayBuffer]':
case tagDataView: case tagDataView:
// Coerce to typed array so we can fall through. // Coerce to typed array so we can fall through.
return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); todo.push({a: toBufferView(a), b: toBufferView(b)});
continue;
} }
var areArrays = className === '[object Array]'; var areArrays = className === '[object Array]';
if (!areArrays && isTypedArray$1(a)) { if (!areArrays && isTypedArray$1(a)) {
var byteLength = getByteLength(a); var byteLength = getByteLength(a);
if (byteLength !== getByteLength(b)) return false; if (byteLength !== getByteLength(b)) return false;
if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) continue;
areArrays = true; areArrays = true;
} }
if (!areArrays) { if (!areArrays) {
@@ -432,23 +451,25 @@
return false; return false;
} }
} }
// Assume equality for cyclic structures. The algorithm for detecting cyclic // Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// Initializing stack of traversed objects.
// It's done here since we only need them for objects and arrays comparison.
aStack = aStack || [];
bStack = bStack || [];
var length = aStack.length; var length = aStack.length;
while (length--) { while (length--) {
// Linear search. Performance is inversely proportional to the number of // Linear search. Performance is inversely proportional to the number of
// unique nested structures. // unique nested structures.
if (aStack[length] === a) return bStack[length] === b; if (aStack[length] === a) {
if (bStack[length] === b) break;
return false;
} }
}
if (length >= 0) continue;
// Add the first object to the stack of traversed objects. // Add the first object to the stack of traversed objects.
aStack.push(a); aStack.push(a);
bStack.push(b); bStack.push(b);
todo.push(true);
// Recursively compare objects and arrays. // Recursively compare objects and arrays.
if (areArrays) { if (areArrays) {
@@ -457,7 +478,7 @@
if (length !== b.length) return false; if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties. // Deep compare the contents, ignoring non-numeric properties.
while (length--) { while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false; todo.push({a: a[length], b: b[length]});
} }
} else { } else {
// Deep compare objects. // Deep compare objects.
@@ -468,20 +489,14 @@
while (length--) { while (length--) {
// Deep compare each member // Deep compare each member
key = _keys[length]; key = _keys[length];
if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; if (!has$1(b, key)) return false;
todo.push({a: a[key], b: b[key]});
}
} }
} }
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
return true; return true;
} }
// Perform a deep comparison to check if two objects are equal.
function isEqual(a, b) {
return eq(a, b);
}
// Retrieve all the enumerable property names of an object. // Retrieve all the enumerable property names of an object.
function allKeys(obj) { function allKeys(obj) {
if (!isObject(obj)) return []; if (!isObject(obj)) return [];
@@ -1032,25 +1047,27 @@
var isArrayLike = createSizePropertyCheck(getLength); var isArrayLike = createSizePropertyCheck(getLength);
// Internal implementation of a recursive `flatten` function. // Internal implementation of a recursive `flatten` function.
function flatten$1(input, depth, strict, output) { function flatten$1(input, depth, strict) {
output = output || []; if (!depth && depth !== 0) depth = Infinity;
if (!depth && depth !== 0) { var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = [];
depth = Infinity; while (true) {
} else if (depth <= 0) { if (i >= length) {
return output.concat(input); if (!stack.length) break;
var frame = stack.pop();
i = frame.i;
input = frame.v;
length = getLength(input);
continue;
} }
var idx = output.length; var value = input[i++];
for (var i = 0, length = getLength(input); i < length; i++) { if (stack.length >= depth) {
var value = input[i]; output[idx++] = value;
if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { } else if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
// Flatten current level of array or arguments object. // Flatten current level of array or arguments object.
if (depth > 1) { stack.push({i: i, v: input});
flatten$1(value, depth - 1, strict, output); i = 0;
idx = output.length; input = value;
} else { length = getLength(input);
var j = 0, len = value.length;
while (j < len) output[idx++] = value[j++];
}
} else if (!strict) { } else if (!strict) {
output[idx++] = value; output[idx++] = value;
} }
+2 -1
View File
@@ -30,7 +30,8 @@ include('components/lnbits-wallet-share.vue') %} {%
include('components/lnbits-wallet-charts.vue') %} {% include('components/lnbits-wallet-charts.vue') %} {%
include('components/lnbits-wallet-paylinks.vue') %} {% include('components/lnbits-wallet-paylinks.vue') %} {%
include('components/lnbits-wallet-extra.vue') %} {% include('components/lnbits-wallet-extra.vue') %} {%
include('components/lnbits-error.vue') %} include('components/lnbits-error.vue') %} {%
include('components/lnbits-dialog.vue') %}
<template id="lnbits-manage"> <template id="lnbits-manage">
<q-list v-if="g.user" dense class="lnbits-drawer__q-list"> <q-list v-if="g.user" dense class="lnbits-drawer__q-list">
@@ -199,12 +199,16 @@
</div> </div>
<div class="col-md-8 col-sm-12"></div> <div class="col-md-8 col-sm-12"></div>
</div> </div>
<lnbits-dialog
<q-dialog v-model="exchangeData.showTickerConversion" position="top"> :show="exchangeData.showTickerConversion"
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :title="$t('create_ticker_converter')"
<div class="q-mb-md"> :action="{
<strong v-text="$t('create_ticker_converter')"></strong> label: 'Add Ticker Conversion'
</div> }"
:cancel-label="$t('close')"
@update:show="exchangeData.showTickerConversion = $event"
@action="addExchangeTickerConversion()"
>
<div class="row"> <div class="row">
<div class="col-12 q-mb-md"> <div class="col-12 q-mb-md">
<q-select <q-select
@@ -226,20 +230,5 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="addExchangeTickerConversion()"
label="Add Ticker Conversion"
color="primary"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
@@ -125,15 +125,14 @@
</q-expansion-item> </q-expansion-item>
</div> </div>
</q-list> </q-list>
<q-dialog v-model="showQRDialog"> <lnbits-dialog
<q-card class="q-pa-md"> :show="showQRDialog"
:position="'standard'"
@update:show="showQRDialog = $event"
>
<q-card-section> <q-card-section>
<lnbits-qrcode :value="qrValue"></lnbits-qrcode> <lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-card-section> </q-card-section>
<q-card-actions align="right"> </lnbits-dialog>
<q-btn flat :label="$t('close')" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div> </div>
</template> </template>
@@ -0,0 +1,61 @@
<template id="lnbits-dialog">
<q-dialog
:model-value="show"
:position="position"
:persistent="persistent"
@update:model-value="handleModelUpdate"
@hide="handleHide"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<!-- Header with optional title -->
<q-card-section v-if="title">
<div class="text-h6" v-text="title"></div>
</q-card-section>
<!-- Main content slot -->
<q-card-section>
<slot></slot>
</q-card-section>
<q-card-actions
v-if="showCancel || hasAction || hasSecondaryAction"
class="q-mt-lg"
align="right"
>
<q-btn
v-if="showCancel"
v-close-popup
flat
:color="cancelColor"
:label="cancelLabel"
@click="handleCancel"
></q-btn>
<q-space></q-space>
<q-btn
v-if="hasSecondaryAction"
:icon="secondaryActionProps.icon"
:color="secondaryActionProps.color"
:label="secondaryActionProps.label"
:loading="secondaryActionProps.loading"
:disable="secondaryActionProps.disable"
@click="handleSecondaryAction"
v-close-popup="secondaryActionProps.closePopup"
class="q-mr-sm"
></q-btn>
<q-btn
v-if="hasAction"
:icon="actionProps.icon"
:color="actionProps.color"
:label="actionProps.label"
:loading="actionProps.loading"
:disable="actionProps.disable"
@click="handleAction"
v-close-popup="actionProps.closePopup"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</template>
@@ -394,9 +394,16 @@
></span> ></span>
</i> </i>
</q-td> </q-td>
<q-dialog v-model="props.expand" :props="props" position="top"> <lnbits-dialog
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card"> :show="props.expand"
<q-card-section> :cancel-label="$t('close')"
:action="{
label: $t('check_payment'),
icon: 'refresh'
}"
@update:show="props.expand = $event"
@action="checkPayment(props.row.payment_hash)"
>
<q-list bordered separator> <q-list bordered separator>
<q-expansion-item <q-expansion-item
expand-separator expand-separator
@@ -461,9 +468,7 @@
</q-list> </q-list>
<div <div
v-if=" v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
props.row.isIn && props.row.isPending && props.row.bolt11
"
> >
<div v-if="props.row.extra.fiat_payment_request"> <div v-if="props.row.extra.fiat_payment_request">
<lnbits-qrcode <lnbits-qrcode
@@ -479,27 +484,24 @@
></lnbits-qrcode> ></lnbits-qrcode>
</div> </div>
</div> </div>
<div class="row q-mt-md"> </lnbits-dialog>
<q-btn <lnbits-dialog
outline :show="hodlInvoice.show"
color="grey" :cancel-label="$t('close')"
@click="checkPayment(props.row.payment_hash)" :action="{
icon="refresh" label: $t('cancel_invoice'),
:label="$t('payment_check')" color: 'grey',
></q-btn> closePopup: true
<q-btn }"
v-close-popup :secondary-action="{
flat label: $t('settle_invoice'),
color="grey" color: 'grey',
class="q-ml-auto" closePopup: true
:label="$t('close')" }"
></q-btn> @update:show="hodlInvoice.show = $event"
</div> @action="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
</q-card-section> @secondary-action="settleHoldInvoice(hodlInvoice.preimage)"
</q-card> >
</q-dialog>
<q-dialog v-model="hodlInvoice.show" position="top">
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
<q-card-section> <q-card-section>
<q-item-label class="text-h6"> <q-item-label class="text-h6">
<span v-text="$t('hold_invoice')"></span> <span v-text="$t('hold_invoice')"></span>
@@ -520,33 +522,7 @@
> >
</q-input> </q-input>
</q-card-section> </q-card-section>
<q-card-section class="row q-gutter-x-sm"> </lnbits-dialog>
<q-btn
@click="settleHoldInvoice(hodlInvoice.preimage)"
outline
v-close-popup
color="grey"
:label="$t('settle_invoice')"
>
</q-btn>
<q-btn
v-close-popup
outline
color="grey"
class="q-ml-sm"
@click="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
:label="$t('cancel_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</q-card-section>
</q-card>
</q-dialog>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
@@ -1,6 +1,10 @@
<template id="lnbits-qrcode-scanner"> <template id="lnbits-qrcode-scanner">
<q-dialog v-model="showScanner" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl"> :show="showScanner"
:cancel-label="$t('cancel')"
@update:show="showScanner = $event"
@cancel="reset"
>
<div class="text-center q-mb-lg"> <div class="text-center q-mb-lg">
<qrcode-stream <qrcode-stream
@detect="detect" @detect="detect"
@@ -8,15 +12,5 @@
class="rounded-borders" class="rounded-borders"
></qrcode-stream> ></qrcode-stream>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="reset"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
@@ -8,9 +8,17 @@
icon="edit" icon="edit"
style="position: relative; left: -15px; bottom: -10px" style="position: relative; left: -15px; bottom: -10px"
></q-btn> ></q-btn>
<q-dialog v-model="icon.show" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="icon.show"
<q-form @submit="setIcon" class="q-gutter-md"> :action="{
label: 'Save Icon',
disable: !icon.data.icon
}"
@update:show="icon.show = $event"
@action="setIcon"
@hide="icon.data = {}"
>
<q-form class="q-gutter-md">
<div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center"> <div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center">
<q-btn <q-btn
v-for="(thisIcon, index) in icon.options" v-for="(thisIcon, index) in icon.options"
@@ -41,16 +49,7 @@
class="q-mr-xs" class="q-mr-xs"
></q-btn> ></q-btn>
</div> </div>
<q-btn
unelevated
color="primary"
:disable="!icon.data.icon"
type="submit"
>
Save Icon
</q-btn>
</div> </div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
</template> </template>
@@ -1,11 +1,24 @@
<template id="lnbits-wallet-new"> <template id="lnbits-wallet-new">
<q-dialog v-model="showNewWalletDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-md lnbits__dialog-card"> :show="showNewWalletDialog"
<q-card-section> :title="$t('add_new_wallet')"
<div class="text-h6"> :action="{
<span v-text="$t('add_new_wallet')"></span> label: $t('add_wallet'),
</div> closePopup: true
</q-card-section> }"
:secondary-action="
isLightningShared
? {
label: $t('reject_wallet'),
color: 'negative',
closePopup: true
}
: null
"
@update:show="showNewWalletDialog = $event"
@action="submitAddWallet"
@secondary-action="submitRejectWalletInvitation"
>
<q-card-section v-if="g.user.walletInvitesCount"> <q-card-section v-if="g.user.walletInvitesCount">
<q-badge <q-badge
@click="g.newWalletType = 'lightning-shared'" @click="g.newWalletType = 'lightning-shared'"
@@ -55,40 +68,5 @@
<span v-text="$t('shared_wallet_desc')" class="q-mt-lg"></span> <span v-text="$t('shared_wallet_desc')" class="q-mt-lg"></span>
</div> </div>
</q-card-section> </q-card-section>
</lnbits-dialog>
<q-card-actions class="text-primary">
<div class="row full-width">
<div class="col-md-4">
<q-btn
flat
:label="$t('cancel')"
class="float-left"
v-close-popup
></q-btn>
</div>
<div class="col-md-4">
<q-btn
v-if="isLightningShared"
:disabled="!wallet.sharedWalletId"
flat
:label="$t('reject_wallet')"
v-close-popup
color="negative"
@click="submitRejectWalletInvitation()"
></q-btn>
<span v-else></span>
</div>
<div class="col-md-4">
<q-btn
flat
:label="$t('add_wallet')"
v-close-popup
@click="submitAddWallet()"
class="float-right"
></q-btn>
</div>
</div>
</q-card-actions>
</q-card>
</q-dialog>
</template> </template>
+60 -99
View File
@@ -1231,10 +1231,16 @@
</div> </div>
</div> </div>
<q-dialog v-model="apiAcl.showPasswordDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="apiAcl.showPasswordDialog"
<strong>User Password</strong> title="Password Required"
<div class="row q-mt-md q-col-gutter-md"> :action="{
label: $t('ok')
}"
@update:show="apiAcl.showPasswordDialog = $event"
@action="runPasswordGuardedFunction()"
>
<div class="row q-col-gutter-md">
<div class="col-12"> <div class="col-12">
<q-input <q-input
v-model="apiAcl.password" v-model="apiAcl.password"
@@ -1247,55 +1253,39 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
<q-btn
@click="runPasswordGuardedFunction()"
:label="$t('ok')"
color="primary"
></q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="apiAcl.showNewAclDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="apiAcl.showNewAclDialog"
<strong>New API Access Control List</strong> title="New API Access Control List"
<div class="row q-mt-md q-col-gutter-md"> :action="{
label: 'Create'
}"
:cancel-label="$t('close')"
@update:show="apiAcl.showNewAclDialog = $event"
@action="addApiACL()"
>
<div class="row q-col-gutter-md">
<div class="col-12"> <div class="col-12">
<q-input v-model="apiAcl.newAclName" dense filled label="ACL Name"> <q-input v-model="apiAcl.newAclName" dense filled label="ACL Name">
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn @click="addApiACL()" label="Create" color="primary"></q-btn>
<q-btn <lnbits-dialog
v-close-popup :show="apiAcl.showNewTokenDialog"
flat title="New API Token"
color="grey" :action="{
class="q-ml-auto" label: 'Create'
v-text="$t('close')" }"
></q-btn> :cancel-label="$t('close')"
</div> @update:show="apiAcl.showNewTokenDialog = $event"
</q-card> @action="generateApiToken()"
</q-dialog>
<q-dialog v-model="apiAcl.showNewTokenDialog" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong>New API Token</strong>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12">
<q-input
v-model="apiAcl.newTokenName"
dense
filled
label="Token Name"
> >
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input v-model="apiAcl.newTokenName" dense filled label="Token Name">
</q-input> </q-input>
</div> </div>
<div class="col-12"> <div class="col-12">
@@ -1313,10 +1303,7 @@
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
> >
<q-date <q-date v-model="apiAcl.newTokenExpiry" mask="YYYY-MM-DD HH:mm">
v-model="apiAcl.newTokenExpiry"
mask="YYYY-MM-DD HH:mm"
>
<div class="row items-center justify-end"> <div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat /> <q-btn v-close-popup label="Close" color="primary" flat />
</div> </div>
@@ -1347,26 +1334,28 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="generateApiToken()" <lnbits-dialog
label="Create" :show="labelsDialog.show"
color="primary" :title="$t('label')"
></q-btn> :action="{
<q-btn label: g.user.extra.labels.some(
v-close-popup label => label.name === labelsDialog.data.name
flat )
color="grey" ? $t('update_label')
class="q-ml-auto" : $t('add_label'),
v-text="$t('close')" disable: !labelsDialog.data.name || !labelsDialog.data.color
></q-btn> }"
</div> :cancel-label="$t('cancel')"
</q-card> @update:show="labelsDialog.show = $event"
</q-dialog> @action="
<q-dialog v-model="labelsDialog.show" position="top"> g.user.extra.labels.some(label => label.name === labelsDialog.data.name)
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> ? updateUserLabel()
<strong v-text="$t('label')"></strong> : addUserLabel()
<div class="row q-mt-md q-col-gutter-md"> "
>
<div class="row q-col-gutter-md">
<div class="col-12"> <div class="col-12">
<q-input <q-input
v-model="labelsDialog.data.name" v-model="labelsDialog.data.name"
@@ -1411,33 +1400,5 @@
</q-input> </q-input>
</div> </div>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
<q-btn
v-if="
g.user.extra.labels.some(
label => label.name === labelsDialog.data.name
)
"
@click="updateUserLabel()"
:disable="!labelsDialog.data.name || !labelsDialog.data.color"
:label="$t('update_label')"
color="primary"
></q-btn>
<q-btn
v-else
@click="addUserLabel()"
:disable="!labelsDialog.data.name || !labelsDialog.data.color"
:label="$t('add_label')"
color="primary"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
+12 -22
View File
@@ -170,9 +170,17 @@
</div> </div>
</div> </div>
<q-dialog v-model="auditDetailsDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="auditDetailsDialog.show"
<strong v-text="$t('http_request_details')"></strong> :title="$t('http_request_details')"
:action="{
label: $t('copy'),
icon: 'content_copy'
}"
:cancel-label="$t('close')"
@update:show="auditDetailsDialog.show = $event"
@action="utils.copyText(auditDetailsDialog.data)"
>
<q-input <q-input
filled filled
dense dense
@@ -180,23 +188,5 @@
type="textarea" type="textarea"
rows="25" rows="25"
></q-input> ></q-input>
</lnbits-dialog>
<div class="row q-mt-lg">
<q-btn
@click="utils.copyText(auditDetailsDialog.data)"
icon="content_copy"
color="grey"
flat
v-text="$t('copy')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
+44 -59
View File
@@ -366,9 +366,16 @@
</div> </div>
</div> </div>
<q-dialog v-model="showUninstallDialog" position="top"> <lnbits-dialog
<q-card class="q-pa-lg"> :show="showUninstallDialog"
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6> :title="$t('warning')"
:action="{
label: $t('uninstall_confirm')
}"
:cancel-label="$t('cancel')"
@update:show="showUninstallDialog = $event"
@action="uninstallExtension()"
>
<p> <p>
<span v-text="$t('extension_uninstall_warning')"></span><br /> <span v-text="$t('extension_uninstall_warning')"></span><br />
<span v-text="$t('confirm_continue')"></span> <span v-text="$t('confirm_continue')"></span>
@@ -385,27 +392,21 @@
</q-tooltip> </q-tooltip>
</q-checkbox> </q-checkbox>
</div> </div>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
outline
color="grey"
@click="uninstallExtension()"
v-text="$t('uninstall_confirm')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="showDropDbDialog" position="top"> <lnbits-dialog
<q-card v-if="selectedExtension" class="q-pa-lg"> v-if="selectedExtension"
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6> :show="showDropDbDialog"
:title="$t('warning')"
:action="{
label: $t('confirm'),
color: 'red',
disable: dropDbExtensionId !== selectedExtension.id
}"
:cancel-label="$t('cancel')"
@update:show="showDropDbDialog = $event"
@action="dropExtensionDb()"
>
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p> <p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
<q-input <q-input
v-model="dropDbExtensionId" v-model="dropDbExtensionId"
@@ -430,8 +431,7 @@
v-text="$t('cancel')" v-text="$t('cancel')"
></q-btn> ></q-btn>
</div> </div>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="showManageExtensionDialog" position="top"> <q-dialog v-model="showManageExtensionDialog" position="top">
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card"> <q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
@@ -1244,9 +1244,11 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="paymentDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-md lnbits__dialog-card"> :show="paymentDialog.show"
<q-card-section> :cancel-label="$t('close')"
@update:show="paymentDialog.show = $event"
>
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl"> <q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
<lnbits-qrcode <lnbits-qrcode
:value="paymentDialog.invoice" :value="paymentDialog.invoice"
@@ -1254,21 +1256,20 @@
class="rounded-borders" class="rounded-borders"
></lnbits-qrcode> ></lnbits-qrcode>
</q-responsive> </q-responsive>
</q-card-section> </lnbits-dialog>
<q-card-actions align="between">
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn> <lnbits-dialog
</q-card-actions> :show="showUpdateAllDialog"
</q-card> :title="$t('update')"
</q-dialog> :action="{
<q-dialog v-model="showUpdateAllDialog" position="top"> label: $t('update')
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> }"
<div class="row"> :cancel-label="$t('cancel')"
<div class="col-12"> @update:show="showUpdateAllDialog = $event"
<h6 class="q-my-md" v-text="$t('update')"></h6> @action="updateSelectedExtensions()"
</div> >
</div>
<div v-if="updatableExtensions?.length > 1" class="row"> <div v-if="updatableExtensions?.length > 1" class="row">
<div class="col-12"> <div class="col-12 q-mb-md">
<q-btn <q-btn
outline outline
color="grey" color="grey"
@@ -1299,21 +1300,5 @@
</div> </div>
</template> </template>
</q-virtual-scroll> </q-virtual-scroll>
<div class="row q-mt-lg"> </lnbits-dialog>
<q-btn
@click="updateSelectedExtensions()"
outline
color="grey"
v-text="$t('update')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template> </template>
+43 -80
View File
@@ -103,8 +103,15 @@
</q-card-section> </q-card-section>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="channels"> <q-tab-panel name="channels">
<q-dialog v-model="connectPeerDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="connectPeerDialog.show"
:action="{
label: $t('connect')
}"
:cancel-label="$t('cancel')"
@update:show="connectPeerDialog.show = $event"
@action="connectPeer"
>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
<q-input <q-input
dense dense
@@ -114,28 +121,19 @@
label="Node URI" label="Node URI"
hint="pubkey@host:port" hint="pubkey@host:port"
></q-input> ></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('connect')"
color="primary"
@click="connectPeer"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="setFeeDialog.show"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="setFeeDialog.show"
<label class="text-h6">Set Channel Fee</label> title="Set Channel Fee"
:action="{
label: $t('set')
}"
:cancel-label="$t('cancel')"
@update:show="setFeeDialog.show = $event"
@action="setChannelFee(setFeeDialog.channel_id)"
>
<p class="text-caption" v-text="setFeeDialog.channel_id"></p> <p class="text-caption" v-text="setFeeDialog.channel_id"></p>
<q-separator></q-separator> <q-separator></q-separator>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
@@ -153,27 +151,18 @@
v-model.number="setFeeDialog.data.fee_base_msat" v-model.number="setFeeDialog.data.fee_base_msat"
label="Fee Base msat" label="Fee Base msat"
></q-input> ></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('set')"
color="primary"
@click="setChannelFee(setFeeDialog.channel_id)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="openChannelDialog.show"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="openChannelDialog.show"
:action="{
label: $t('open')
}"
:cancel-label="$t('cancel')"
@update:show="openChannelDialog.show = $event"
@action="openChannel"
>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
<q-input <q-input
dense dense
@@ -197,9 +186,7 @@
dense dense
type="number" type="number"
filled filled
v-model.number=" v-model.number="openChannelDialog.data.push_amount"
openChannelDialog.data.push_amount
"
label="Push Amount" label="Push Amount"
hint="This gifts sats to the other side!" hint="This gifts sats to the other side!"
></q-input> ></q-input>
@@ -215,27 +202,19 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<div class="row q-mt-lg">
<q-btn
:label="$t('open')"
color="primary"
@click="openChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-dialog v-model="closeChannelDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> :show="closeChannelDialog.show"
title="Close Channel"
:action="{
label: $t('close')
}"
:cancel-label="$t('cancel')"
@update:show="closeChannelDialog.show = $event"
@action="closeChannel"
>
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
<div> <div>
<q-checkbox <q-checkbox
@@ -243,24 +222,8 @@
label="Force" label="Force"
/> />
</div> </div>
<div class="row q-mt-lg">
<q-btn
:label="$t('close')"
color="primary"
@click="closeChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form> </q-form>
</q-card> </lnbits-dialog>
</q-dialog>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<div class="row q-col-gutter-lg"> <div class="row q-col-gutter-lg">
+13 -19
View File
@@ -446,9 +446,17 @@
</div> </div>
</div> </div>
<div v-else-if="activeWallet.show"> <div v-else-if="activeWallet.show">
<q-dialog v-model="createWalletDialog.show" position="top"> <lnbits-dialog
<q-card class="q-pa-md q-pt-md lnbits__dialog-card"> :show="createWalletDialog.show"
<strong>Create Wallet</strong> title="Create Wallet"
:action="{
label: 'Create',
closePopup: true
}"
cancel-label="Cancel"
@update:show="createWalletDialog.show = $event"
@action="createWallet"
>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="row q-mt-lg"> <div class="row q-mt-lg">
@@ -469,27 +477,13 @@
filled filled
dense dense
v-model="createWalletDialog.data.currency" v-model="createWalletDialog.data.currency"
:options="{{ currencies | safe }}" :options="g.allowedCurrencies"
></q-select> ></q-select>
</div> </div>
</div> </div>
<div class="row q-mt-lg">
<q-btn
v-close-popup
@click="createWallet()"
unelevated
color="primary"
type="submit"
>Create</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div> </div>
</div> </div>
</div> </lnbits-dialog>
</q-card>
</q-dialog>
</div> </div>
<div v-else> <div v-else>
<div class="row q-col-gutter-md q-mb-md"> <div class="row q-col-gutter-md q-mb-md">
+8 -7
View File
@@ -13,7 +13,7 @@
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"quasar": "2.18.6", "quasar": "2.18.6",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"underscore": "^1.13.7", "underscore": "^1.13.8",
"vue": "3.5.25", "vue": "3.5.25",
"vue-i18n": "^11.2.2", "vue-i18n": "^11.2.2",
"vue-qrcode-reader": "^5.7.3", "vue-qrcode-reader": "^5.7.3",
@@ -1260,9 +1260,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.4", "version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -1722,9 +1722,10 @@
} }
}, },
"node_modules/underscore": { "node_modules/underscore": {
"version": "1.13.7", "version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.25", "version": "3.5.25",
+2 -1
View File
@@ -28,7 +28,7 @@
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"quasar": "2.18.6", "quasar": "2.18.6",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"underscore": "^1.13.7", "underscore": "^1.13.8",
"vue": "3.5.25", "vue": "3.5.25",
"vue-i18n": "^11.2.2", "vue-i18n": "^11.2.2",
"vue-qrcode-reader": "^5.7.3", "vue-qrcode-reader": "^5.7.3",
@@ -123,6 +123,7 @@
"js/components/admin/lnbits-admin-site-customisation.js", "js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-assets-config.js", "js/components/admin/lnbits-admin-assets-config.js",
"js/components/admin/lnbits-admin-audit.js", "js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-dialog.js",
"js/components/lnbits-wallet-charts.js", "js/components/lnbits-wallet-charts.js",
"js/components/lnbits-wallet-api-docs.js", "js/components/lnbits-wallet-api-docs.js",
"js/components/lnbits-wallet-icon.js", "js/components/lnbits-wallet-icon.js",
-4
View File
@@ -6,10 +6,6 @@ from pydantic import BaseModel
from lnbits.wallets import get_funding_source, set_funding_source from lnbits.wallets import get_funding_source, set_funding_source
class FakeError(Exception):
pass
class DbTestModel(BaseModel): class DbTestModel(BaseModel):
id: int id: int
name: str name: str
+24 -26
View File
@@ -2,6 +2,7 @@ import asyncio
import hashlib import hashlib
import pytest import pytest
from pytest_mock.plugin import MockerFixture
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.crud import get_standalone_payment, update_payment from lnbits.core.crud import get_standalone_payment, update_payment
@@ -18,7 +19,7 @@ from lnbits.exceptions import PaymentError
from lnbits.tasks import create_task, wait_for_paid_invoices from lnbits.tasks import create_task, wait_for_paid_invoices
from lnbits.wallets import get_funding_source from lnbits.wallets import get_funding_source
from ..helpers import FakeError, is_fake, is_regtest from ..helpers import is_fake, is_regtest
from .helpers import ( from .helpers import (
cancel_invoice, cancel_invoice,
get_real_invoice, get_real_invoice,
@@ -138,7 +139,9 @@ async def test_pay_real_invoice_mainnet(
@pytest.mark.anyio @pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest") @pytest.mark.skipif(is_fake, reason="this only works in regtest")
async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_from): async def test_create_real_invoice(
client, adminkey_headers_from, inkey_headers_from, mocker: MockerFixture
):
prev_balance = await get_node_balance_sats() prev_balance = await get_node_balance_sats()
create_invoice = CreateInvoice(out=False, amount=1000, memo="test") create_invoice = CreateInvoice(out=False, amount=1000, memo="test")
response = await client.post( response = await client.post(
@@ -156,7 +159,15 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
payment_status = response.json() payment_status = response.json()
assert not payment_status["paid"] assert not payment_status["paid"]
async def on_paid(payment: Payment): on_paid_mock = mocker.AsyncMock()
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
pay_real_invoice(invoice["bolt11"])
await asyncio.sleep(1)
assert on_paid_mock.call_count == 1
payment = on_paid_mock.call_args_list[0][0][0]
assert payment.payment_hash == invoice["payment_hash"] assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"] assert payment.checking_id == invoice["checking_id"]
@@ -175,16 +186,6 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
assert payment_status.get("preimage") is not None assert payment_status.get("preimage") is not None
# exit out of infinite loop
raise FakeError()
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
pay_real_invoice(invoice["bolt11"])
# wait for the task to exit
with pytest.raises(FakeError):
await task
@pytest.mark.anyio @pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest") @pytest.mark.skipif(is_fake, reason="this only works in regtest")
@@ -367,7 +368,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
@pytest.mark.anyio @pytest.mark.anyio
@pytest.mark.skipif(is_fake, reason="this only works in regtest") @pytest.mark.skipif(is_fake, reason="this only works in regtest")
async def test_receive_real_invoice_set_pending_and_check_state( async def test_receive_real_invoice_set_pending_and_check_state(
client, adminkey_headers_from, inkey_headers_from client, adminkey_headers_from, inkey_headers_from, mocker: MockerFixture
): ):
""" """
1. We create a real invoice 1. We create a real invoice
@@ -391,7 +392,15 @@ async def test_receive_real_invoice_set_pending_and_check_state(
payment_status = response.json() payment_status = response.json()
assert not payment_status["paid"] assert not payment_status["paid"]
async def on_paid(payment: Payment): on_paid_mock = mocker.AsyncMock()
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
pay_real_invoice(invoice["bolt11"])
await asyncio.sleep(1)
assert on_paid_mock.call_count == 1
payment = on_paid_mock.call_args_list[0][0][0]
assert payment.payment_hash == invoice["payment_hash"] assert payment.payment_hash == invoice["payment_hash"]
assert payment.checking_id == invoice["checking_id"] assert payment.checking_id == invoice["checking_id"]
@@ -403,8 +412,6 @@ async def test_receive_real_invoice_set_pending_and_check_state(
payment_status = response.json() payment_status = response.json()
assert payment_status["paid"] assert payment_status["paid"]
assert payment
# set the incoming invoice to pending # set the incoming invoice to pending
payment.status = PaymentState.PENDING payment.status = PaymentState.PENDING
await update_payment(payment) await update_payment(payment)
@@ -416,15 +423,6 @@ async def test_receive_real_invoice_set_pending_and_check_state(
assert payment_pending.success is False assert payment_pending.success is False
assert payment_pending.failed is False assert payment_pending.failed is False
# exit out of infinite loop
raise FakeError()
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
pay_real_invoice(invoice["bolt11"])
with pytest.raises(FakeError):
await task
@pytest.mark.anyio @pytest.mark.anyio
async def test_check_fee_reserve(client, adminkey_headers_from): async def test_check_fee_reserve(client, adminkey_headers_from):
+11 -17
View File
@@ -10,16 +10,12 @@ from pytest_mock.plugin import MockerFixture
from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet
from lnbits.core.crud.payments import get_payment, get_payments_paginated from lnbits.core.crud.payments import get_payment, get_payments_paginated
from lnbits.core.models import Payment, PaymentState, Wallet from lnbits.core.models import PaymentState, Wallet
from lnbits.core.services import create_invoice, create_user_account, pay_invoice from lnbits.core.services import create_invoice, create_user_account, pay_invoice
from lnbits.core.services.payments import update_wallet_balance from lnbits.core.services.payments import update_wallet_balance
from lnbits.exceptions import InvoiceError, PaymentError from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import Settings from lnbits.settings import Settings
from lnbits.tasks import ( from lnbits.tasks import create_task, wait_for_paid_invoices
create_permanent_task,
internal_invoice_listener,
register_invoice_listener,
)
from lnbits.wallets.base import PaymentResponse from lnbits.wallets.base import PaymentResponse
from lnbits.wallets.fake import FakeWallet from lnbits.wallets.fake import FakeWallet
@@ -230,13 +226,13 @@ async def test_pay_for_extension(to_wallet: Wallet, settings: Settings):
@pytest.mark.anyio @pytest.mark.anyio
async def test_notification_for_internal_payment(to_wallet: Wallet): async def test_notification_for_internal_payment(
to_wallet: Wallet, mocker: MockerFixture
):
test_name = "test_notification_for_internal_payment" test_name = "test_notification_for_internal_payment"
create_permanent_task(internal_invoice_listener) on_paid_mock = mocker.AsyncMock()
invoice_queue: asyncio.Queue = asyncio.Queue() create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
register_invoice_listener(invoice_queue, test_name)
payment = await create_invoice( payment = await create_invoice(
wallet_id=to_wallet.id, wallet_id=to_wallet.id,
amount=123, amount=123,
@@ -248,18 +244,16 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
) )
await asyncio.sleep(1) await asyncio.sleep(1)
while True: assert on_paid_mock.call_count == 1
_payment: Payment = invoice_queue.get_nowait() # raises if queue empty _payment = on_paid_mock.call_args_list[0][0][0]
assert _payment
if _payment.memo == test_name: assert _payment.memo == test_name
assert _payment.status == PaymentState.SUCCESS.value assert _payment.status == PaymentState.SUCCESS.value
assert _payment.bolt11 == payment.bolt11 assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000 assert _payment.amount == 123_000
updated_payment = await get_payment(_payment.checking_id) updated_payment = await get_payment(_payment.checking_id)
assert updated_payment.webhook_status == "404" assert updated_payment.webhook_status == "404"
break # we found our payment, success
@pytest.mark.anyio @pytest.mark.anyio
async def test_pay_failed( async def test_pay_failed(
Generated
+11 -13
View File
@@ -2537,21 +2537,19 @@ wheels = [
[[package]] [[package]]
name = "tornado" name = "tornado"
version = "6.5.2" version = "6.5.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" },
{ url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" },
{ url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" },
{ url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" },
{ url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" },
{ url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" },
{ url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" },
{ url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" },
{ url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
] ]
[[package]] [[package]]