Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2320018302 | ||
|
|
0dc0725a9b | ||
|
|
8c77f75cf1 | ||
|
|
21505471d5 | ||
|
|
baa9a35773 | ||
|
|
7f114ddcc0 | ||
|
|
92aad20dd7 | ||
|
|
0f4ae5da86 | ||
|
|
7a796c6510 | ||
|
|
0910687328 | ||
|
|
33e2fc2ea8 | ||
|
|
0c6e8394c8 | ||
|
|
d55e2a0e1f | ||
|
|
148ba9d275 | ||
|
|
d2ca774f6f | ||
|
|
233398b512 | ||
|
|
152c1dbb74 |
@@ -5,8 +5,9 @@
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||
|
||||
@@ -77,13 +78,11 @@ As well as working great in a browser, LNbits has native IoS and Android apps as
|
||||
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/) [](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||
[docs]: https://github.com/lnbits/lnbits/wiki
|
||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import get_wallets
|
||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
@@ -180,8 +180,13 @@ async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||
async def get_user_from_account(
|
||||
account: Account, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn)
|
||||
wallets = await get_wallets(account.id, False, conn=conn)
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
|
||||
wallets = await get_wallets(account.id, deleted=False, conn=conn)
|
||||
|
||||
if len(wallets) == 0:
|
||||
wallet = await create_wallet(user_id=account.id, conn=conn)
|
||||
wallets.append(wallet)
|
||||
|
||||
return User(
|
||||
id=account.id,
|
||||
email=account.email,
|
||||
|
||||
@@ -237,7 +237,7 @@ class AccountOverview(Account):
|
||||
|
||||
class AccountFilters(FilterModel):
|
||||
__search_fields__ = [
|
||||
"user",
|
||||
"id",
|
||||
"email",
|
||||
"username",
|
||||
"pubkey",
|
||||
@@ -245,17 +245,18 @@ class AccountFilters(FilterModel):
|
||||
"wallet_id",
|
||||
]
|
||||
__sort_fields__ = [
|
||||
"balance_msat",
|
||||
"id",
|
||||
"email",
|
||||
"username",
|
||||
"transaction_count",
|
||||
"wallet_count",
|
||||
"last_payment",
|
||||
"pubkey",
|
||||
"external_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
email: str | None = None
|
||||
user: str | None = None
|
||||
id: str | None = None
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
pubkey: str | None = None
|
||||
external_id: str | None = None
|
||||
wallet_id: str | None = None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,14 @@
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import Cookie, Depends, Query, Request
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from lnurl import url_decode
|
||||
from pydantic.types import UUID4
|
||||
|
||||
from lnbits.core.helpers import to_valid_user_id
|
||||
from lnbits.core.models import User
|
||||
@@ -25,12 +23,7 @@ from lnbits.decorators import (
|
||||
from lnbits.helpers import check_callback_url, template_renderer
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ...utils.exchange_rates import allowed_currencies
|
||||
from ..crud import (
|
||||
create_wallet,
|
||||
get_user,
|
||||
get_wallet,
|
||||
)
|
||||
from ..crud import get_user
|
||||
|
||||
generic_router = APIRouter(
|
||||
tags=["Core NON-API Website Routes"], include_in_schema=False
|
||||
@@ -42,60 +35,9 @@ async def favicon():
|
||||
return RedirectResponse(settings.lnbits_qr_logo)
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/wallet",
|
||||
response_class=HTMLResponse,
|
||||
description="show wallet page",
|
||||
)
|
||||
async def get_user_wallet(
|
||||
request: Request,
|
||||
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
wal: UUID4 | None = Query(None),
|
||||
):
|
||||
if wal:
|
||||
wallet = await get_wallet(wal.hex)
|
||||
elif len(user.wallets) == 0:
|
||||
wallet = await create_wallet(user_id=user.id)
|
||||
user.wallets.append(wallet)
|
||||
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
|
||||
wallet = await get_wallet(lnbits_last_active_wallet)
|
||||
else:
|
||||
wallet = user.wallets[0]
|
||||
|
||||
if not wallet or wallet.deleted:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Wallet not found",
|
||||
)
|
||||
if wallet.user != user.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Not your wallet.",
|
||||
)
|
||||
context = {
|
||||
"user": user.json(),
|
||||
"wallet": wallet.json(),
|
||||
"wallet_name": wallet.name,
|
||||
"currencies": allowed_currencies(),
|
||||
"service_fee": settings.lnbits_service_fee,
|
||||
"service_fee_max": settings.lnbits_service_fee_max,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
}
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/wallet.html",
|
||||
{**context, "ajax": _is_ajax_request(request)},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
||||
async def robots():
|
||||
data = """
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
"""
|
||||
data = "User-agent: *\nDisallow: /"
|
||||
return HTMLResponse(content=data, media_type="text/plain")
|
||||
|
||||
|
||||
@@ -254,6 +196,7 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
||||
|
||||
|
||||
@generic_router.get("/payments")
|
||||
@generic_router.get("/wallet")
|
||||
@generic_router.get("/wallets")
|
||||
@generic_router.get("/account")
|
||||
@generic_router.get("/extensions")
|
||||
|
||||
@@ -179,12 +179,18 @@ async def api_payments_daily_stats(
|
||||
)
|
||||
async def api_payments_paginated(
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||
recheck_pending: bool = Query(
|
||||
False, description="Force check and update of pending payments."
|
||||
),
|
||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||
):
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=key_info.wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
if not recheck_pending:
|
||||
return page
|
||||
|
||||
for payment in page.data:
|
||||
if payment.pending:
|
||||
await update_pending_payment(payment)
|
||||
|
||||
+29
-15
@@ -235,11 +235,14 @@ class Connection(Compat):
|
||||
table_name: if provided some optimisations can be applied.
|
||||
"""
|
||||
|
||||
if table_name and not _valid_sql_name(table_name):
|
||||
raise ValueError(f"Invalid table name: '{table_name}'.")
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
if table_name:
|
||||
if not _valid_sql_name(table_name):
|
||||
raise ValueError(f"Invalid table name: '{table_name}'.")
|
||||
filters.set_table_name(table_name)
|
||||
|
||||
clause = filters.where(where)
|
||||
parsed_values = filters.values(values)
|
||||
|
||||
@@ -491,6 +494,7 @@ class Page(BaseModel, Generic[T]):
|
||||
|
||||
|
||||
class Filter(BaseModel, Generic[TFilterModel]):
|
||||
table_name: str | None = None
|
||||
field: str
|
||||
op: Operator = Operator.EQ
|
||||
model: type[TFilterModel] | None
|
||||
@@ -518,7 +522,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
if field in model.__fields__:
|
||||
compare_field = model.__fields__[field]
|
||||
values: dict = {}
|
||||
if op in {Operator.EVERY, Operator.ANY}:
|
||||
if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
raw_values = [v for rv in raw_values for v in rv.split(",")]
|
||||
|
||||
for index, raw_value in enumerate(raw_values):
|
||||
@@ -533,20 +537,23 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
@property
|
||||
def statement(self) -> str:
|
||||
prefix = f"{self.table_name}." if self.table_name else ""
|
||||
stmt = []
|
||||
for key in self.values.keys() if self.values else []:
|
||||
clean_key = key.split("__")[0]
|
||||
if self.model and self.model.__fields__[clean_key].type_ == datetime:
|
||||
if self.model and self.model.__fields__[self.field].type_ == datetime:
|
||||
placeholder = compat_timestamp_placeholder(key)
|
||||
stmt.append(f"{clean_key} {self.op.as_sql} {placeholder}")
|
||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
|
||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
stmt.append(f":{key}")
|
||||
else:
|
||||
stmt.append(f"{clean_key} {self.op.as_sql} :{key}")
|
||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
|
||||
|
||||
if self.op == Operator.EVERY:
|
||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
|
||||
elif self.op == Operator.EVERY:
|
||||
statement = " AND ".join(stmt)
|
||||
else:
|
||||
statement = " OR ".join(stmt)
|
||||
|
||||
return f"({statement})"
|
||||
|
||||
|
||||
@@ -563,13 +570,14 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
search: str | None = None
|
||||
|
||||
offset: int | None = None
|
||||
limit: int | None = None
|
||||
|
||||
limit: int | None = 10
|
||||
sortby: str | None = None
|
||||
direction: Literal["asc", "desc"] | None = None
|
||||
|
||||
model: type[TFilterModel] | None = None
|
||||
|
||||
table_name: str | None = None
|
||||
|
||||
@root_validator(pre=True)
|
||||
def validate_sortby(cls, values):
|
||||
sortby = values.get("sortby")
|
||||
@@ -584,8 +592,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
def pagination(self) -> str:
|
||||
stmt = ""
|
||||
if self.limit:
|
||||
stmt += f"LIMIT {self.limit} "
|
||||
self.limit = self.limit or 10
|
||||
stmt += f"LIMIT {min(1000, self.limit)} "
|
||||
if self.offset:
|
||||
stmt += f"OFFSET {self.offset}"
|
||||
return stmt
|
||||
@@ -611,7 +619,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
def order_by(self) -> str:
|
||||
if self.sortby:
|
||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||
prefix = f"{self.table_name}." if self.table_name else ""
|
||||
return f"ORDER BY {prefix}{self.sortby} {self.direction or 'asc'}"
|
||||
return ""
|
||||
|
||||
def values(self, values: dict | None = None) -> dict:
|
||||
@@ -631,6 +640,11 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
values["search"] = f"%{self.search.lower()}%"
|
||||
return values
|
||||
|
||||
def set_table_name(self, table_name: str) -> None:
|
||||
self.table_name = table_name
|
||||
for page_filter in self.filters:
|
||||
page_filter.table_name = table_name
|
||||
|
||||
|
||||
class DbJsonEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
|
||||
@@ -18,6 +18,7 @@ from pydantic.schema import field_schema
|
||||
from lnbits.jinja2_templating import Jinja2Templates
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
|
||||
from .db import FilterModel
|
||||
|
||||
@@ -109,6 +110,8 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
|
||||
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
||||
"LNBITS_CURRENCIES": list(currencies.keys()),
|
||||
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
||||
}
|
||||
|
||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -716,11 +716,14 @@ window.localisation.en = {
|
||||
add_label: 'Add Label',
|
||||
label: 'Label',
|
||||
labels: 'Labels',
|
||||
label_filter: 'Label Filter',
|
||||
no_labels_defined: 'No labels defined yet',
|
||||
manage_labels: 'Manage Labels',
|
||||
update_label: 'Update Label',
|
||||
delete_label: 'Delete Label',
|
||||
add_remove_labels: 'Add or Remove Labels',
|
||||
payment_labels_updated: 'Payment labels updated',
|
||||
color: 'Color'
|
||||
color: 'Color',
|
||||
sort: 'Sort',
|
||||
sort_by: 'Sort by'
|
||||
}
|
||||
|
||||
@@ -2,18 +2,6 @@ window.LNbits = {
|
||||
g: window.g,
|
||||
utils: window._lnbitsUtils,
|
||||
api: window._lnbitsApi,
|
||||
events: {
|
||||
onInvoicePaid(wallet, cb) {
|
||||
ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||
ws.onmessage = ev => {
|
||||
const data = JSON.parse(ev.data)
|
||||
if (data.payment) {
|
||||
cb(data)
|
||||
}
|
||||
}
|
||||
return ws.onclose
|
||||
}
|
||||
},
|
||||
map: {
|
||||
user(data) {
|
||||
const obj = {
|
||||
@@ -27,16 +15,12 @@ window.LNbits = {
|
||||
extra: data.extra ?? {}
|
||||
}
|
||||
const mapWallet = this.wallet
|
||||
obj.wallets = obj.wallets
|
||||
.map(obj => {
|
||||
return mapWallet(obj)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.extra.pinned !== b.extra.pinned) {
|
||||
return a.extra.pinned ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||
if (a.extra.pinned !== b.extra.pinned) {
|
||||
return a.extra.pinned ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
obj.walletOptions = obj.wallets.map(obj => {
|
||||
return {
|
||||
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||
@@ -77,51 +61,6 @@ window.LNbits = {
|
||||
newWallet.url = `/wallet?&wal=${data.id}`
|
||||
newWallet.storedPaylinks = data.stored_paylinks.links
|
||||
return newWallet
|
||||
},
|
||||
payment(data) {
|
||||
obj = {
|
||||
checking_id: data.checking_id,
|
||||
status: data.status,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
memo: data.memo,
|
||||
time: data.time,
|
||||
bolt11: data.bolt11,
|
||||
preimage: data.preimage,
|
||||
payment_hash: data.payment_hash,
|
||||
expiry: data.expiry,
|
||||
extra: data.extra ?? {},
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
fiat_amount: data.fiat_amount,
|
||||
fiat_currency: data.fiat_currency,
|
||||
labels: data.labels
|
||||
}
|
||||
|
||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
||||
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
||||
|
||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
||||
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
||||
obj.msat = obj.amount
|
||||
obj.sat = obj.msat / 1000
|
||||
obj.tag = obj.extra?.tag
|
||||
obj.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
||||
obj.sat
|
||||
)
|
||||
obj.isIn = obj.amount > 0
|
||||
obj.isOut = obj.amount < 0
|
||||
obj.isPending = obj.status === 'pending'
|
||||
obj.isPaid = obj.status === 'success'
|
||||
obj.isFailed = obj.status === 'failed'
|
||||
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
||||
try {
|
||||
obj.details = JSON.parse(data.extra?.details || '{}')
|
||||
} catch {
|
||||
obj.details = {extraDetails: data.extra?.details}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
window.app.component('lnbits-admin-server', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-server',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
currencies: []
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.currencies = await LNbits.api.getCurrencies()
|
||||
}
|
||||
mixins: [window.windowMixin]
|
||||
})
|
||||
|
||||
@@ -9,10 +9,7 @@ window.app.component('lnbits-footer', {
|
||||
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
|
||||
},
|
||||
showFooter() {
|
||||
return (
|
||||
this.SITE_TITLE == 'LNbits' &&
|
||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
)
|
||||
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
window.app.component('lnbits-payment-list', {
|
||||
template: '#lnbits-payment-list',
|
||||
props: ['update', 'lazy', 'wallet', 'paymentFilter'],
|
||||
props: ['wallet', 'paymentFilter'],
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
@@ -147,6 +147,49 @@ window.app.component('lnbits-payment-list', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mapPayment(data) {
|
||||
const obj = {
|
||||
checking_id: data.checking_id,
|
||||
status: data.status,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
memo: data.memo,
|
||||
time: data.time,
|
||||
bolt11: data.bolt11,
|
||||
preimage: data.preimage,
|
||||
payment_hash: data.payment_hash,
|
||||
expiry: data.expiry,
|
||||
extra: data.extra ?? {},
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
fiat_amount: data.fiat_amount,
|
||||
fiat_currency: data.fiat_currency,
|
||||
labels: data.labels
|
||||
}
|
||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
||||
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
||||
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
||||
obj.msat = obj.amount
|
||||
obj.sat = obj.msat / 1000
|
||||
obj.tag = obj.extra?.tag
|
||||
obj.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
||||
obj.sat
|
||||
)
|
||||
obj.isIn = obj.amount > 0
|
||||
obj.isOut = obj.amount < 0
|
||||
obj.isPending = obj.status === 'pending'
|
||||
obj.isPaid = obj.status === 'success'
|
||||
obj.isFailed = obj.status === 'failed'
|
||||
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
||||
try {
|
||||
obj.details = JSON.parse(data.extra?.details || '{}')
|
||||
} catch {
|
||||
obj.details = {extraDetails: data.extra?.details}
|
||||
}
|
||||
return obj
|
||||
},
|
||||
searchByDate() {
|
||||
if (typeof this.searchDate === 'string') {
|
||||
this.searchDate = {
|
||||
@@ -195,9 +238,8 @@ window.app.component('lnbits-payment-list', {
|
||||
.then(response => {
|
||||
this.paymentsTable.loading = false
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
this.payments = response.data.data.map(this.mapPayment)
|
||||
this.recheckPendingPayments()
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
@@ -215,9 +257,7 @@ window.app.component('lnbits-payment-list', {
|
||||
.then(response => {
|
||||
this.paymentsTable.loading = false
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
this.payments = response.data.data.map(this.mapPayment)
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
@@ -244,6 +284,41 @@ window.app.component('lnbits-payment-list', {
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
recheckPendingPayments() {
|
||||
const pendingPayments = this.payments.filter(p => p.status === 'pending')
|
||||
if (pendingPayments.length === 0) return
|
||||
|
||||
const params = [
|
||||
'recheck_pending=true',
|
||||
'checking_id[in]=' + pendingPayments.map(p => p.checking_id).join(',')
|
||||
].join('&')
|
||||
|
||||
LNbits.api
|
||||
.getPayments(this.currentWallet, params)
|
||||
.then(response => {
|
||||
let updatedPayments = 0
|
||||
response.data.data.forEach(updatedPayment => {
|
||||
if (updatedPayment.status !== 'pending') {
|
||||
const index = this.payments.findIndex(
|
||||
p => p.checking_id === updatedPayment.checking_id
|
||||
)
|
||||
if (index !== -1) {
|
||||
this.payments.splice(index, 1, this.mapPayment(updatedPayment))
|
||||
updatedPayments += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
if (updatedPayments > 0) {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('payment_successful')
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn(err)
|
||||
})
|
||||
},
|
||||
showHoldInvoiceDialog(payment) {
|
||||
this.hodlInvoice.show = true
|
||||
this.hodlInvoice.preimage = ''
|
||||
@@ -287,7 +362,7 @@ window.app.component('lnbits-payment-list', {
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
LNbits.api.getPayments(this.g.wallet, params).then(response => {
|
||||
let payments = response.data.data.map(LNbits.map.payment)
|
||||
let payments = response.data.data.map(this.mapPayment)
|
||||
let columns = this.paymentsCSV.columns
|
||||
|
||||
if (detailed) {
|
||||
@@ -423,23 +498,11 @@ window.app.component('lnbits-payment-list', {
|
||||
this.fetchPayments()
|
||||
}
|
||||
},
|
||||
lazy(newVal) {
|
||||
if (newVal === true) this.fetchPayments()
|
||||
},
|
||||
update() {
|
||||
this.fetchPayments()
|
||||
},
|
||||
'g.updatePayments'() {
|
||||
this.fetchPayments()
|
||||
},
|
||||
'g.wallet': {
|
||||
handler(newWallet) {
|
||||
this.fetchPayments()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.lazy === undefined) this.fetchPayments()
|
||||
this.fetchPayments()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
window.app.component('lnbits-theme', {
|
||||
mixins: [window.windowMixin],
|
||||
watch: {
|
||||
'g.walletFlip'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.walletFlip', val)
|
||||
if (val === true && this.$q.screen.lt.md) {
|
||||
this.g.visibleDrawer = false
|
||||
}
|
||||
},
|
||||
'g.disclaimerShown'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
window.app.component('lnbits-wallet-extra', {
|
||||
template: '#lnbits-wallet-extra',
|
||||
mixins: [window.windowMixin],
|
||||
props: ['chartConfig'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
handleSendLnurl(lnurl) {
|
||||
this.$emit('send-lnurl', lnurl)
|
||||
},
|
||||
updateWallet(wallet) {
|
||||
this.$emit('update-wallet', wallet)
|
||||
},
|
||||
handleFiatTracking() {
|
||||
this.g.fiatTracking = !this.g.fiatTracking
|
||||
if (!this.g.fiatTracking) {
|
||||
this.g.isFiatPriority = false
|
||||
this.g.wallet.currency = ''
|
||||
this.updateWallet({currency: ''})
|
||||
} else {
|
||||
this.updateWallet({currency: this.g.wallet.currency})
|
||||
this.updateFiatBalance()
|
||||
}
|
||||
},
|
||||
deleteWallet() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.deleteWallet(this.g.wallet)
|
||||
.then(_ => {
|
||||
Quasar.Notify.create({
|
||||
timeout: 3000,
|
||||
message: `Wallet deleted!`,
|
||||
spinner: true
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
updateFiatBalance() {
|
||||
// set rate from local storage to avoid clunky api calls
|
||||
if (
|
||||
this.$q.localStorage.getItem(
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency
|
||||
)
|
||||
) {
|
||||
this.g.exchangeRate = this.$q.localStorage.getItem(
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency
|
||||
)
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
}
|
||||
LNbits.api
|
||||
.request('GET', `/api/v1/rate/` + this.g.wallet.currency, null)
|
||||
.then(response => {
|
||||
this.g.fiatBalance =
|
||||
(response.data.price / 100000000) * this.g.wallet.sat
|
||||
this.g.exchangeRate = response.data.price.toFixed(2)
|
||||
this.g.fiatTracking = true
|
||||
this.$q.localStorage.set(
|
||||
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||
this.g.exchangeRate
|
||||
)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.g.wallet.currency !== '' && this.g.isSatsDenomination) {
|
||||
this.g.fiatTracking = true
|
||||
this.updateFiatBalance()
|
||||
} else {
|
||||
this.g.fiatTracking = false
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -18,18 +18,19 @@ window.g = Vue.reactive({
|
||||
fiatBalance: 0,
|
||||
exchangeRate: 0,
|
||||
fiatTracking: false,
|
||||
wallets: [],
|
||||
payments: [],
|
||||
walletEventListeners: [],
|
||||
showNewWalletDialog: false,
|
||||
newWalletType: 'lightning',
|
||||
updatePayments: false,
|
||||
updatePaymentsHash: '',
|
||||
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
|
||||
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
|
||||
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
||||
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
||||
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
||||
mobileSimple: localStore('lnbits.mobileSimple', true),
|
||||
walletFlip: localStore('lnbits.walletFlip', false),
|
||||
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
|
||||
darkChoice: localStore('lnbits.darkMode', true),
|
||||
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
|
||||
borderChoice: localStore(
|
||||
|
||||
+23
-153
@@ -1,144 +1,12 @@
|
||||
const DynamicComponent = {
|
||||
props: {
|
||||
fetchUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
scripts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
const quasarConfig = {
|
||||
config: {
|
||||
loading: {
|
||||
spinner: Quasar.QSpinnerBars
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
keys: []
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadDynamicContent()
|
||||
},
|
||||
methods: {
|
||||
async loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const existingScript = document.querySelector(`script[src="${src}"]`)
|
||||
if (existingScript) {
|
||||
existingScript.remove()
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = src
|
||||
script.async = true
|
||||
script.onload = resolve
|
||||
script.onerror = () =>
|
||||
reject(new Error(`Failed to load script: ${src}`))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
},
|
||||
async loadDynamicContent() {
|
||||
this.$q.loading.show()
|
||||
try {
|
||||
const cleanUrl = this.fetchUrl.split('#')[0]
|
||||
//grab page content, need to be before loading scripts
|
||||
const response = await fetch(cleanUrl, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
|
||||
const html = await response.text()
|
||||
|
||||
// load window variables
|
||||
const parser = new DOMParser()
|
||||
const htmlDocument = parser.parseFromString(html, 'text/html')
|
||||
const inlineScript = htmlDocument.querySelector('#window-vars-script')
|
||||
if (inlineScript) {
|
||||
new Function(inlineScript.innerHTML)() // Execute the script
|
||||
}
|
||||
|
||||
//load scripts defined in the route
|
||||
await this.loadScript('/static/js/base.js')
|
||||
for (const script of this.scripts) {
|
||||
await this.loadScript(script)
|
||||
}
|
||||
|
||||
//housecleaning, remove old component
|
||||
const previousRouteName =
|
||||
this.$router.currentRoute.value.meta.previousRouteName
|
||||
if (
|
||||
previousRouteName &&
|
||||
window.app._context.components[previousRouteName]
|
||||
) {
|
||||
delete window.app._context.components[previousRouteName]
|
||||
}
|
||||
//load component logic
|
||||
const logicKey = `${this.$route.name}PageLogic`
|
||||
const componentLogic = window[logicKey]
|
||||
|
||||
if (!componentLogic) {
|
||||
throw new Error(
|
||||
`Component logic '${logicKey}' not found. Ensure it is defined in the script.`
|
||||
)
|
||||
}
|
||||
|
||||
//Add mixins
|
||||
componentLogic.mixins = componentLogic.mixins || []
|
||||
if (window.windowMixin) {
|
||||
componentLogic.mixins.push(window.windowMixin)
|
||||
}
|
||||
|
||||
//Build component
|
||||
window.app.component(this.$route.name, {
|
||||
...componentLogic,
|
||||
template: html // Use the fetched HTML as the template
|
||||
})
|
||||
delete window[logicKey] //dont need this anymore
|
||||
this.$forceUpdate()
|
||||
} catch (error) {
|
||||
console.error('Error loading dynamic content:', error)
|
||||
} finally {
|
||||
this.$q.loading.hide()
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
const validRouteNames = routes.map(route => route.name)
|
||||
if (validRouteNames.includes(to.name)) {
|
||||
this.$router.currentRoute.value.meta.previousRouteName = from.name
|
||||
this.loadDynamicContent()
|
||||
} else {
|
||||
console.log(
|
||||
`Route '${to.name}' is not valid. Leave this one to Fastapi.`
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<component :is="$route.name"></component>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'Wallet',
|
||||
component: DynamicComponent,
|
||||
props: route => {
|
||||
let fetchUrl = '/wallet'
|
||||
if (Object.keys(route.query).length > 0) {
|
||||
fetchUrl += '?'
|
||||
for (const [key, value] of Object.entries(route.query)) {
|
||||
fetchUrl += `${key}=${value}&`
|
||||
}
|
||||
fetchUrl = fetchUrl.slice(0, -1) // remove last &
|
||||
}
|
||||
return {
|
||||
fetchUrl,
|
||||
scripts: ['/static/js/wallet.js']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/node',
|
||||
name: 'Node',
|
||||
@@ -159,6 +27,11 @@ const routes = [
|
||||
name: 'Audit',
|
||||
component: PageAudit
|
||||
},
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'Wallet',
|
||||
component: PageWallet
|
||||
},
|
||||
{
|
||||
path: '/wallets',
|
||||
name: 'Wallets',
|
||||
@@ -206,7 +79,19 @@ window.router = VueRouter.createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
window.i18n = new VueI18n.createI18n({
|
||||
locale: window.g.locale,
|
||||
fallbackLocale: 'en',
|
||||
messages: window.localisation
|
||||
})
|
||||
|
||||
window.app.mixin({
|
||||
data() {
|
||||
return {
|
||||
g: window.g,
|
||||
...WINDOW_SETTINGS
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isVueRoute() {
|
||||
const currentPath = window.location.pathname
|
||||
@@ -218,23 +103,8 @@ window.app.mixin({
|
||||
})
|
||||
|
||||
window.app.use(VueQrcodeReader)
|
||||
window.app.use(Quasar, {
|
||||
config: {
|
||||
loading: {
|
||||
spinner: Quasar.QSpinnerBars
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.i18n = new VueI18n.createI18n({
|
||||
locale: window.g.locale,
|
||||
fallbackLocale: 'en',
|
||||
messages: window.localisation
|
||||
})
|
||||
window.app.use(Quasar, quasarConfig)
|
||||
|
||||
window.app.use(window.i18n)
|
||||
|
||||
window.app.provide('g', g)
|
||||
window.app.use(window.router)
|
||||
window.app.component('DynamicComponent', DynamicComponent)
|
||||
window.app.mount('#vue')
|
||||
|
||||
@@ -85,10 +85,10 @@ window.PageUsers = {
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'User Id',
|
||||
field: 'user',
|
||||
field: 'id',
|
||||
sortable: false
|
||||
},
|
||||
|
||||
@@ -119,7 +119,7 @@ window.PageUsers = {
|
||||
align: 'left',
|
||||
label: 'Balance',
|
||||
field: 'balance_msat',
|
||||
sortable: true
|
||||
sortable: false
|
||||
},
|
||||
|
||||
{
|
||||
@@ -127,7 +127,7 @@ window.PageUsers = {
|
||||
align: 'left',
|
||||
label: 'Payments',
|
||||
field: 'transaction_count',
|
||||
sortable: true
|
||||
sortable: false
|
||||
},
|
||||
|
||||
{
|
||||
@@ -135,16 +135,24 @@ window.PageUsers = {
|
||||
align: 'left',
|
||||
label: 'Last Payment',
|
||||
field: 'last_payment',
|
||||
sortable: true
|
||||
sortable: false
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
sortBy: 'balance_msat',
|
||||
sortBy: 'created_at',
|
||||
rowsPerPage: 10,
|
||||
page: 1,
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
sortFields: [
|
||||
{name: 'id', label: 'User ID'},
|
||||
{name: 'username', label: 'Username'},
|
||||
{name: 'email', label: 'Email'},
|
||||
{name: 'pubkey', label: 'Public Key'},
|
||||
{name: 'created_at', label: 'Creation Date'},
|
||||
{name: 'updated_at', label: 'Last Updated'}
|
||||
],
|
||||
search: null,
|
||||
hideEmpty: true,
|
||||
loading: false
|
||||
@@ -198,6 +206,16 @@ window.PageUsers = {
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
sortByColumn(columnName) {
|
||||
if (this.usersTable.pagination.sortBy === columnName) {
|
||||
this.usersTable.pagination.descending =
|
||||
!this.usersTable.pagination.descending
|
||||
} else {
|
||||
this.usersTable.pagination.sortBy = columnName
|
||||
this.usersTable.pagination.descending = false
|
||||
}
|
||||
this.fetchUsers()
|
||||
},
|
||||
createUser() {
|
||||
LNbits.api
|
||||
.request('POST', '/users/api/v1/user', null, this.activeUser.data)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
window.WalletPageLogic = {
|
||||
window.PageWallet = {
|
||||
template: '#page-wallet',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
origin: window.location.origin,
|
||||
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
||||
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
||||
parse: {
|
||||
show: false,
|
||||
invoice: null,
|
||||
@@ -34,7 +32,7 @@ window.WalletPageLogic = {
|
||||
amountMsat: null,
|
||||
minMax: [0, 2100000000000000],
|
||||
lnurl: null,
|
||||
units: ['sat'],
|
||||
units: ['sat', ...(this.currencies || [])],
|
||||
unit: 'sat',
|
||||
fiatProvider: '',
|
||||
data: {
|
||||
@@ -98,17 +96,6 @@ window.WalletPageLogic = {
|
||||
this.parse.show = true
|
||||
this.lnurlScan()
|
||||
},
|
||||
formatFiatAmount(amount, currency) {
|
||||
this.update.currency = currency
|
||||
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
||||
amount.toFixed(2),
|
||||
currency
|
||||
)
|
||||
this.formattedExchange = LNbits.utils.formatCurrency(
|
||||
this.g.exchangeRate,
|
||||
currency
|
||||
)
|
||||
},
|
||||
msatoshiFormat(value) {
|
||||
return LNbits.utils.formatSat(value / 1000)
|
||||
},
|
||||
@@ -118,9 +105,6 @@ window.WalletPageLogic = {
|
||||
showCamera() {
|
||||
this.parse.camera.show = true
|
||||
},
|
||||
focusInput(el) {
|
||||
this.$nextTick(() => this.$refs[el].focus())
|
||||
},
|
||||
showReceiveDialog() {
|
||||
this.receive.show = true
|
||||
this.receive.status = 'pending'
|
||||
@@ -135,7 +119,6 @@ window.WalletPageLogic = {
|
||||
: 'sat'
|
||||
this.receive.minMax = [0, 2100000000000000]
|
||||
this.receive.lnurl = null
|
||||
this.focusInput('setAmount')
|
||||
},
|
||||
onReceiveDialogHide() {
|
||||
if (this.hasNfc) {
|
||||
@@ -154,7 +137,6 @@ window.WalletPageLogic = {
|
||||
this.parse.data.internalMemo = null
|
||||
this.parse.data.paymentChecker = null
|
||||
this.parse.camera.show = false
|
||||
this.focusInput('textArea')
|
||||
},
|
||||
closeParseDialog() {
|
||||
setTimeout(() => {
|
||||
@@ -420,7 +402,7 @@ window.WalletPageLogic = {
|
||||
)
|
||||
.then(response => {
|
||||
dismissPaymentMsg()
|
||||
this.updatePayments = !this.updatePayments
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
this.parse.show = false
|
||||
if (response.data.status == 'success') {
|
||||
Quasar.Notify.create({
|
||||
@@ -548,51 +530,6 @@ window.WalletPageLogic = {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
deleteWallet() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.deleteWallet(this.g.wallet)
|
||||
.then(_ => {
|
||||
Quasar.Notify.create({
|
||||
timeout: 3000,
|
||||
message: `Wallet deleted!`,
|
||||
spinner: true
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
updateFiatBalance(currency) {
|
||||
// set rate from local storage to avoid clunky api calls
|
||||
if (this.$q.localStorage.getItem('lnbits.exchangeRate.' + currency)) {
|
||||
this.g.exchangeRate = this.$q.localStorage.getItem(
|
||||
'lnbits.exchangeRate.' + currency
|
||||
)
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
this.formatFiatAmount(this.g.fiatBalance, currency)
|
||||
}
|
||||
if (currency && this.g.wallet.currency == currency) {
|
||||
LNbits.api
|
||||
.request('GET', `/api/v1/rate/` + currency, null)
|
||||
.then(response => {
|
||||
this.g.fiatBalance =
|
||||
(response.data.price / 100000000) * this.g.wallet.sat
|
||||
this.g.exchangeRate = response.data.price.toFixed(2)
|
||||
this.g.fiatTracking = true
|
||||
this.formatFiatAmount(this.g.fiatBalance, this.g.wallet.currency)
|
||||
this.$q.localStorage.set(
|
||||
'lnbits.exchangeRate.' + currency,
|
||||
this.g.exchangeRate
|
||||
)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
}
|
||||
},
|
||||
pasteToTextArea() {
|
||||
this.$refs.textArea.focus()
|
||||
navigator.clipboard.readText().then(text => {
|
||||
@@ -694,36 +631,6 @@ window.WalletPageLogic = {
|
||||
dismissPaymentMsg()
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
swapBalancePriority() {
|
||||
this.g.isFiatPriority = !this.g.isFiatPriority
|
||||
this.receive.unit = this.g.isFiatPriority
|
||||
? this.g.wallet.currency || 'sat'
|
||||
: 'sat'
|
||||
},
|
||||
handleFiatTracking() {
|
||||
this.g.fiatTracking = !this.g.fiatTracking
|
||||
if (!this.g.fiatTracking) {
|
||||
this.g.isFiatPriority = false
|
||||
this.update.currency = ''
|
||||
this.g.wallet.currency = ''
|
||||
this.updateWallet({currency: ''})
|
||||
} else {
|
||||
this.g.wallet.currency = this.update.currency
|
||||
this.updateWallet({currency: this.update.currency})
|
||||
this.updateFiatBalance(this.update.currency)
|
||||
}
|
||||
},
|
||||
createdTasks() {
|
||||
this.update.name = this.g.wallet.name
|
||||
this.receive.units = ['sat', ...(window.currencies || [])]
|
||||
if (this.g.wallet.currency != '' && LNBITS_DENOMINATION == 'sats') {
|
||||
this.g.fiatTracking = true
|
||||
this.updateFiatBalance(this.g.wallet.currency)
|
||||
} else {
|
||||
this.update.currency = ''
|
||||
this.g.fiatTracking = false
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -734,15 +641,26 @@ window.WalletPageLogic = {
|
||||
this.decodeRequest()
|
||||
this.parse.show = true
|
||||
}
|
||||
this.createdTasks()
|
||||
if (urlParams.has('wal')) {
|
||||
const wallet = g.user.wallets.find(w => w.id === urlParams.get('wal'))
|
||||
if (wallet) {
|
||||
this.selectWallet(wallet)
|
||||
}
|
||||
} else {
|
||||
const wallet = g.user.wallets.find(w => w.id === this.g.lastActiveWallet)
|
||||
if (wallet) {
|
||||
this.selectWallet(wallet)
|
||||
} else if (g.user.wallets.length > 0) {
|
||||
this.selectWallet(g.user.wallets[0])
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'g.updatePayments'(newVal, oldVal) {
|
||||
'g.lastActiveWallet'(val) {
|
||||
this.$q.localStorage.setItem('lnbits.lastActiveWallet', val)
|
||||
},
|
||||
'g.updatePayments'() {
|
||||
this.parse.show = false
|
||||
if (this.receive.paymentHash === this.g.updatePaymentsHash) {
|
||||
this.receive.show = false
|
||||
this.receive.paymentHash = null
|
||||
}
|
||||
if (
|
||||
this.g.wallet.currency &&
|
||||
this.$q.localStorage.getItem(
|
||||
@@ -754,18 +672,36 @@ window.WalletPageLogic = {
|
||||
)
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
this.formatFiatAmount(this.g.fiatBalance, this.g.wallet.currency)
|
||||
}
|
||||
},
|
||||
'g.wallet': {
|
||||
handler() {
|
||||
try {
|
||||
this.createdTasks()
|
||||
} catch (error) {
|
||||
console.warn(`Chart creation failed: ${error}`)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
'g.wallet'() {
|
||||
if (this.g.wallet.currency) {
|
||||
this.g.fiatTracking = true
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
} else {
|
||||
this.g.fiatBalance = 0
|
||||
this.g.fiatTracking = false
|
||||
}
|
||||
},
|
||||
'g.isFiatPriority'() {
|
||||
this.receive.unit = this.g.isFiatPriority ? this.g.wallet.currency : 'sat'
|
||||
},
|
||||
'g.fiatBalance'() {
|
||||
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
||||
this.g.fiatBalance.toFixed(2),
|
||||
this.g.wallet.currency
|
||||
)
|
||||
},
|
||||
'g.exchangeRate'() {
|
||||
if (this.g.fiatTracking && this.g.wallet.currency) {
|
||||
this.g.fiatBalance =
|
||||
(this.g.exchangeRate / 100000000) * this.g.wallet.sat
|
||||
this.formattedExchange = LNbits.utils.formatCurrency(
|
||||
this.g.exchangeRate,
|
||||
this.g.wallet.currency
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ window.PageWallets = {
|
||||
}
|
||||
},
|
||||
goToWallet(walletId) {
|
||||
window.location = `/wallet?wal=${walletId}`
|
||||
this.$router.push({path: '/wallet', query: {wal: walletId}})
|
||||
},
|
||||
formattedFiatAmount(amount, currency) {
|
||||
return LNbits.utils.formatCurrency(Number(amount).toFixed(2), currency)
|
||||
|
||||
@@ -1,72 +1,67 @@
|
||||
window.windowMixin = {
|
||||
i18n: window.i18n,
|
||||
data() {
|
||||
return {
|
||||
api: window._lnbitsApi,
|
||||
utils: window._lnbitsUtils,
|
||||
g: window.g,
|
||||
...WINDOW_SETTINGS
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openNewWalletDialog(walletType = 'lightning') {
|
||||
this.g.newWalletType = walletType
|
||||
this.g.showNewWalletDialog = true
|
||||
},
|
||||
flipWallets(smallScreen) {
|
||||
this.g.walletFlip = !this.g.walletFlip
|
||||
if (this.g.walletFlip && smallScreen) {
|
||||
this.g.visibleDrawer = false
|
||||
onWebsocketMessage(ev) {
|
||||
const data = JSON.parse(ev.data)
|
||||
if (!data.payment) {
|
||||
console.error('ws message no payment', data)
|
||||
return
|
||||
}
|
||||
this.$q.localStorage.set('lnbits.walletFlip', this.g.walletFlip)
|
||||
},
|
||||
goToWallets() {
|
||||
this.$router.push({
|
||||
path: '/wallets'
|
||||
|
||||
// update sidebar wallet balances
|
||||
this.g.user.wallets.forEach(w => {
|
||||
if (w.id === data.payment.wallet_id) {
|
||||
w.sat = data.wallet_balance
|
||||
}
|
||||
})
|
||||
|
||||
// if current wallet, update balance and payments
|
||||
if (this.g.wallet.id === data.payment.wallet_id) {
|
||||
this.g.wallet.sat = data.wallet_balance
|
||||
// lnbits-payment-list is watching
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
}
|
||||
|
||||
// NOTE: react only on incoming payments for now
|
||||
if (data.payment.amount > 0) {
|
||||
eventReaction(data.wallet_balance * 1000)
|
||||
}
|
||||
},
|
||||
paymentEvents() {
|
||||
this.g.walletEventListeners = this.g.walletEventListeners || []
|
||||
let timeout
|
||||
this.g.user.wallets.forEach(wallet => {
|
||||
if (!this.g.walletEventListeners.includes(wallet.id)) {
|
||||
this.g.walletEventListeners.push(wallet.id)
|
||||
LNbits.events.onInvoicePaid(wallet, data => {
|
||||
const walletIndex = this.g.user.wallets.findIndex(
|
||||
w => w.id === wallet.id
|
||||
const ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||
ws.onmessage = this.onWebsocketMessage
|
||||
ws.onopen = () => console.log('ws connected for wallet', wallet.id)
|
||||
// onclose and onerror can both happen on their own or together,
|
||||
// so we add a clearTimeout to avoid multiple reconnections
|
||||
ws.onclose = () => {
|
||||
console.log('ws closed, reconnecting...', wallet.id)
|
||||
this.g.walletEventListeners = this.g.walletEventListeners.filter(
|
||||
id => id !== wallet.id
|
||||
)
|
||||
if (walletIndex !== -1) {
|
||||
//needed for balance being deducted
|
||||
let satBalance = data.wallet_balance
|
||||
if (data.payment.amount < 0) {
|
||||
satBalance = data.wallet_balance += data.payment.amount / 1000
|
||||
}
|
||||
//update the wallet
|
||||
Object.assign(this.g.user.wallets[walletIndex], {
|
||||
sat: satBalance,
|
||||
msat: data.wallet_balance * 1000,
|
||||
fsat: data.wallet_balance.toLocaleString()
|
||||
})
|
||||
//update the current wallet
|
||||
if (this.g.wallet.id === data.payment.wallet_id) {
|
||||
Object.assign(this.g.wallet, this.g.user.wallets[walletIndex])
|
||||
|
||||
//if on the wallet page and payment is incoming trigger the eventReaction
|
||||
if (
|
||||
data.payment.amount > 0 &&
|
||||
window.location.pathname === '/wallet'
|
||||
) {
|
||||
eventReaction(data.wallet_balance * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.g.updatePaymentsHash = data.payment.payment_hash
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(this.paymentEvents, 5000)
|
||||
}
|
||||
ws.onerror = () => {
|
||||
console.warn('ws error, reconnecting...', wallet.id)
|
||||
this.g.walletEventListeners = this.g.walletEventListeners.filter(
|
||||
id => id !== wallet.id
|
||||
)
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(this.paymentEvents, 5000)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
selectWallet(wallet) {
|
||||
this.g.wallet = wallet
|
||||
this.g.lastActiveWallet = wallet.id
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
this.balance = parseInt(wallet.balance_msat / 1000)
|
||||
const currentPath = this.$route.path
|
||||
@@ -112,19 +107,10 @@ window.windowMixin = {
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
if (window.user) {
|
||||
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
|
||||
}
|
||||
if (window.wallet) {
|
||||
this.g.wallet = Vue.reactive(window.LNbits.map.wallet(window.wallet))
|
||||
}
|
||||
if (window.extensions) {
|
||||
this.g.extensions = Vue.reactive(window.extensions)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.g.user) {
|
||||
created() {
|
||||
// map jinja variable once on pageload
|
||||
if (window.user && !this.g.user) {
|
||||
this.g.user = window.LNbits.map.user(window.user)
|
||||
this.paymentEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"js/pages/node.js",
|
||||
"js/pages/node-public.js",
|
||||
"js/pages/audit.js",
|
||||
"js/pages/wallet.js",
|
||||
"js/pages/wallets.js",
|
||||
"js/pages/users.js",
|
||||
"js/pages/account.js",
|
||||
@@ -73,6 +74,7 @@
|
||||
"js/components/lnbits-wallet-new.js",
|
||||
"js/components/lnbits-wallet-share.js",
|
||||
"js/components/lnbits-wallet-paylinks.js",
|
||||
"js/components/lnbits-wallet-extra.js",
|
||||
"js/components/lnbits-home-logos.js",
|
||||
"js/components/lnbits-qrcode.js",
|
||||
"js/components/lnbits-qrcode-lnurl.js",
|
||||
|
||||
@@ -26,7 +26,8 @@ include('components/lnbits-label-selector.vue') %} {%
|
||||
include('components/lnbits-wallet-api-docs.vue') %} {%
|
||||
include('components/lnbits-wallet-share.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') %}
|
||||
|
||||
<template id="lnbits-manage">
|
||||
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
multiple
|
||||
:hint="$t('allowed_currencies_hint')"
|
||||
:label="$t('allowed_currencies')"
|
||||
:options="currencies"
|
||||
:options="g.currencies"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
@@ -31,7 +31,7 @@
|
||||
:options="
|
||||
formData.lnbits_allowed_currencies?.length
|
||||
? formData.lnbits_allowed_currencies
|
||||
: currencies
|
||||
: g.allowedCurrencies
|
||||
"
|
||||
></q-select>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
>
|
||||
<q-scroll-area style="height: 100%">
|
||||
<q-item>
|
||||
<q-item-section class="cursor-pointer" @click="goToWallets()">
|
||||
<q-item-section
|
||||
class="cursor-pointer"
|
||||
@click="$router.push('/wallets')"
|
||||
>
|
||||
<q-item-label
|
||||
:style="$q.dark.isActive ? 'color:rgba(255, 255, 255, 0.64)' : ''"
|
||||
class="q-item__label q-item__label--header q-pa-none"
|
||||
@@ -22,7 +25,7 @@
|
||||
:icon="g.walletFlip ? 'view_list' : 'view_column'"
|
||||
color="grey"
|
||||
class=""
|
||||
@click="flipWallets($q.screen.lt.md)"
|
||||
@click="g.walletFlip = !g.walletFlip"
|
||||
>
|
||||
<q-tooltip
|
||||
><span
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
round
|
||||
color="primary"
|
||||
icon="more_horiz"
|
||||
@click="goToWallets()"
|
||||
@click="$router.push('/wallets')"
|
||||
>
|
||||
<q-tooltip
|
||||
><span
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<q-item
|
||||
v-if="g.user.hiddenWalletsCount > 0"
|
||||
clickable
|
||||
@click="goToWallets()"
|
||||
@click="$router.push('/wallets')"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="more_horiz" color="grey-5" size="md"></q-icon>
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -132,6 +133,7 @@
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -176,6 +178,7 @@
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -222,6 +225,7 @@
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
@@ -250,6 +254,7 @@
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
v-if="!HIDE_API"
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
<template id="lnbits-wallet-extra">
|
||||
<q-card class="wallet-extra">
|
||||
<q-card-section class="q-pb-xs">
|
||||
<div class="row items-center">
|
||||
<q-avatar
|
||||
size="lg"
|
||||
:icon="g.wallet.extra.icon"
|
||||
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
|
||||
:color="g.wallet.extra.color"
|
||||
>
|
||||
</q-avatar>
|
||||
<lnbits-wallet-icon @update-wallet="updateWallet"></lnbits-wallet-icon>
|
||||
<div class="text-subtitle1 q-mt-none q-mb-none">
|
||||
<span v-text="$t('wallet')"></span>
|
||||
<strong><em v-text="g.wallet.name"></em></strong>
|
||||
</div>
|
||||
<q-space></q-space>
|
||||
<div class="float-right">
|
||||
<q-btn
|
||||
@click="updateWallet({pinned: !g.wallet.extra.pinned})"
|
||||
round
|
||||
class="float-right"
|
||||
:color="g.wallet.extra.pinned ? 'primary' : 'grey-5'"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
icon="push_pin"
|
||||
style="transform: rotate(30deg)"
|
||||
>
|
||||
<q-tooltip><span v-text="$t('pin_wallet')"></span></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
<lnbits-wallet-paylinks
|
||||
@send-lnurl="handleSendLnurl"
|
||||
></lnbits-wallet-paylinks>
|
||||
<q-separator></q-separator>
|
||||
<lnbits-wallet-share></lnbits-wallet-share>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="phone_android"
|
||||
:label="$t('access_wallet_on_mobile')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
You can connect to this wallet from a mobile app:
|
||||
<ul>
|
||||
<li>
|
||||
Download
|
||||
<a class="text-secondary" href="https://zeusln.app">Zeus</a>
|
||||
or
|
||||
<a class="text-secondary" href="https://bluewallet.io/"
|
||||
>BlueWallet</a
|
||||
>
|
||||
from App Store or Google Play
|
||||
</li>
|
||||
<li>
|
||||
Enable the
|
||||
<a class="text-secondary" href="/lndhub">LndHub </a>
|
||||
extension for this account
|
||||
</li>
|
||||
<li>
|
||||
Scan the QR code in the
|
||||
<a class="text-secondary" href="/lndhub">LndHub </a>
|
||||
extensions with your mobile app
|
||||
</li>
|
||||
</ul>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
Or you can access the wallet directly from your mobile browser
|
||||
using:
|
||||
<q-expansion-item
|
||||
icon="mobile_friendly"
|
||||
:label="$t('export_to_phone')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p
|
||||
class="text-center"
|
||||
v-text="$t('export_to_phone_desc')"
|
||||
></p>
|
||||
<lnbits-qrcode
|
||||
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
|
||||
></lnbits-qrcode>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="settings"
|
||||
:label="$t('wallet_config')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<q-input filled v-model="g.wallet.name" label="Name" dense />
|
||||
</div>
|
||||
<div class="col-4 q-pl-sm">
|
||||
<q-btn
|
||||
:disable="!g.wallet.name.length"
|
||||
unelevated
|
||||
class="q-mt-xs full-width"
|
||||
color="primary"
|
||||
:label="$t('update_name')"
|
||||
dense
|
||||
@click="updateWallet({name: g.wallet.name})"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="g.isSatsDenomination">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="g.wallet.currency"
|
||||
@change="updateWallet({currency: g.wallet.currency})"
|
||||
type="text"
|
||||
:disable="g.fiatTracking"
|
||||
:options="g.allowedCurrencies"
|
||||
:label="$t('currency_settings')"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-4 q-pl-sm">
|
||||
<q-btn
|
||||
dense
|
||||
color="primary"
|
||||
class="q-mt-xs full-width"
|
||||
@click="handleFiatTracking()"
|
||||
:disable="g.wallet.currency == ''"
|
||||
:label="g.fiatTracking ? 'Remove' : 'Add'"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-btn
|
||||
v-if="g.user.admin"
|
||||
flat
|
||||
round
|
||||
icon="settings"
|
||||
class="float-right q-mb-lg"
|
||||
to="/admin#exchange_providers"
|
||||
><q-tooltip v-text="$t('exchange_providers')"></q-tooltip
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<p v-text="$t('delete_wallet_desc')"></p>
|
||||
</div>
|
||||
<div class="col-4 q-pl-sm">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="red-10"
|
||||
class="full-width"
|
||||
@click="deleteWallet()"
|
||||
:label="$t('delete_wallet')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="insights"
|
||||
:label="$t('wallet_charts')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="chartConfig.showBalanceChart"
|
||||
:label="$t('payments_balance_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="chartConfig.showBalanceInOutChart"
|
||||
:label="$t('payments_balance_in_out_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="chartConfig.showPaymentInOutChart"
|
||||
:label="$t('payments_count_in_out_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<lnbits-wallet-api-docs></lnbits-wallet-api-docs>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
@@ -1,29 +1,14 @@
|
||||
{% macro window_vars(user, wallet, extensions, extension_data) -%}
|
||||
{% macro window_vars(user) -%}
|
||||
<script id="window-vars-script">
|
||||
window.extensions = JSON.parse('{{ EXTENSIONS | tojson | safe }}');
|
||||
{% if extension_data %}
|
||||
window.extension_data = {{ extension_data | tojson | safe }};
|
||||
{% endif %}
|
||||
{% if currencies %}
|
||||
window.currencies = {{ currencies | tojson | safe }};
|
||||
{% endif %}
|
||||
{% if user %}
|
||||
window.user = JSON.parse({{ user | tojson | safe }});
|
||||
{% endif %}
|
||||
{% if wallet %}
|
||||
window.wallet = JSON.parse({{ wallet | tojson | safe }});
|
||||
{% endif %}
|
||||
</script>
|
||||
<script>
|
||||
//Needed for Vue to create the app on first load (although called on every page, its only loaded once)
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
updatePayments: false
|
||||
}
|
||||
}
|
||||
mixins: [window.windowMixin]
|
||||
})
|
||||
</script>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -3,4 +3,4 @@ include('pages/audit.vue') %} {% include('pages/wallets.vue') %} {%
|
||||
include('pages/users.vue') %} {% include('pages/admin.vue') %} {%
|
||||
include('pages/account.vue') %} {% include('pages/extensions_builder.vue') %} {%
|
||||
include('pages/extensions.vue') %} {% include('pages/first-install.vue') %} {%
|
||||
include('pages/home.vue') %}
|
||||
include('pages/home.vue') %} {% include('pages/wallet.vue') %}
|
||||
|
||||
@@ -100,8 +100,8 @@
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<q-splitter>
|
||||
<template v-slot:before>
|
||||
<q-splitter :separator-style="$q.screen.lt.md && 'display: none'">
|
||||
<template v-slot:before v-if="$q.screen.gt.sm">
|
||||
<q-tabs v-model="tab" vertical active-color="primary">
|
||||
<q-tab
|
||||
name="funding"
|
||||
|
||||
@@ -515,7 +515,44 @@
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width>
|
||||
<q-btn-dropdown color="primary" icon="sort" flat dense>
|
||||
<q-list>
|
||||
<template
|
||||
class="full-width"
|
||||
v-for="column in usersTable.sortFields"
|
||||
:key="column.name"
|
||||
>
|
||||
<q-item
|
||||
@click="sortByColumn(column.name)"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
dense
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="full-width"
|
||||
><span v-text="column.label"></span
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<template
|
||||
v-if="
|
||||
usersTable.pagination.sortBy === column.name
|
||||
"
|
||||
>
|
||||
<q-icon
|
||||
v-if="usersTable.pagination.descending"
|
||||
name="arrow_downward"
|
||||
></q-icon>
|
||||
<q-icon v-else name="arrow_upward"></q-icon>
|
||||
</template>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-input
|
||||
v-if="
|
||||
|
||||
@@ -0,0 +1,876 @@
|
||||
<template id="page-wallet">
|
||||
<div class="row q-col-gutter-md" style="margin-bottom: 6rem">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md wallet-wrapper">
|
||||
<q-card class="wallet-card">
|
||||
<q-card-section>
|
||||
<div class="row q-gutter-sm">
|
||||
<div v-if="g.fiatTracking" class="col-auto">
|
||||
<q-btn
|
||||
@click="g.isFiatPriority = !g.isFiatPriority"
|
||||
style="height: 50px"
|
||||
class="q-mt-lg"
|
||||
color="primary"
|
||||
flat
|
||||
dense
|
||||
icon="swap_vert"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div
|
||||
v-if="!g.isFiatPriority || !g.fiatTracking"
|
||||
class="column"
|
||||
:class="{
|
||||
'q-pt-sm': g.fiatTracking,
|
||||
'q-pt-lg': !g.fiatTracking
|
||||
}"
|
||||
style="height: 100px"
|
||||
>
|
||||
<div class="col-7">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<div class="text-h3 q-my-none full-width">
|
||||
<strong
|
||||
v-text="formatBalance(g.wallet.sat)"
|
||||
class="text-no-wrap"
|
||||
:style="{
|
||||
fontSize: 'clamp(0.75rem, 10vw, 3rem)',
|
||||
display: 'inline-block',
|
||||
maxWidth: '100%'
|
||||
}"
|
||||
></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<lnbits-update-balance
|
||||
v-if="$q.screen.lt.lg"
|
||||
:wallet_id="g.wallet.id"
|
||||
:callback="updateBalanceCallback"
|
||||
:small_btn="true"
|
||||
></lnbits-update-balance>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div v-if="g.fiatTracking">
|
||||
<span
|
||||
class="text-h5 text-italic"
|
||||
v-text="formattedFiatAmount"
|
||||
style="opacity: 0.75"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="column"
|
||||
v-if="g.isFiatPriority && g.fiatTracking"
|
||||
:class="{
|
||||
'q-pt-sm': g.fiatTracking,
|
||||
'q-pt-lg': !g.fiatTracking
|
||||
}"
|
||||
style="height: 100px"
|
||||
>
|
||||
<div class="col-7">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<div
|
||||
v-if="g.fiatTracking"
|
||||
class="text-h3 q-my-none text-no-wrap"
|
||||
>
|
||||
<strong v-text="formattedFiatAmount"></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<lnbits-update-balance
|
||||
v-if="$q.screen.lt.lg"
|
||||
:wallet_id="g.wallet.id"
|
||||
:callback="updateBalanceCallback"
|
||||
:small_btn="true"
|
||||
></lnbits-update-balance>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<span
|
||||
class="text-h5 text-italic"
|
||||
style="opacity: 0.75"
|
||||
v-text="formatBalance(g.wallet.sat)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute-right q-pa-md"
|
||||
v-if="$q.screen.gt.md && g.fiatTracking && g.isSatsDenomination"
|
||||
>
|
||||
<div class="text-bold text-italic">BTC Price</div>
|
||||
<span
|
||||
class="text-bold text-italic"
|
||||
v-text="formattedExchange"
|
||||
></span>
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="$q.screen.lt.md"
|
||||
@click="g.mobileSimple = !g.mobileSimple"
|
||||
color="primary"
|
||||
class="q-ml-xl absolute-right"
|
||||
dense
|
||||
size="sm"
|
||||
style="height: 20px; margin-top: 75px"
|
||||
flat
|
||||
:icon="g.mobileSimple ? 'unfold_more' : 'unfold_less'"
|
||||
:label="g.mobileSimple ? $t('more') : $t('less')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!g.mobileSimple || !$q.screen.lt.md"
|
||||
class="lnbits-wallet-buttons row q-gutter-md"
|
||||
>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
class="q-mr-md"
|
||||
@click="showReceiveDialog"
|
||||
:disable="!this.g.wallet.canReceivePayments"
|
||||
:label="$t('receive')"
|
||||
icon="file_download"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
class="q-mr-md"
|
||||
@click="showParseDialog"
|
||||
:disable="!this.g.wallet.canSendPayments"
|
||||
:label="$t('send')"
|
||||
icon="file_upload"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="secondary"
|
||||
icon="qr_code_scanner"
|
||||
:disable="
|
||||
!this.g.wallet.canReceivePayments &&
|
||||
!this.g.wallet.canSendPayments
|
||||
"
|
||||
@click="showCamera"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('camera_tooltip')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
<lnbits-update-balance
|
||||
v-if="$q.screen.gt.md"
|
||||
:wallet_id="this.g.wallet.id"
|
||||
:callback="updateBalanceCallback"
|
||||
:small_btn="false"
|
||||
></lnbits-update-balance>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="wallet-card">
|
||||
<q-card-section>
|
||||
<lnbits-payment-list
|
||||
:expand-details="expandDetails"
|
||||
:payment-filter="paymentFilter"
|
||||
></lnbits-payment-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-if="!g.mobileSimple || !$q.screen.lt.md"
|
||||
class="col-12 col-md-5 q-gutter-y-md"
|
||||
>
|
||||
<lnbits-wallet-extra
|
||||
@update-wallet="updateWallet"
|
||||
@send-lnurl="handleSendLnurl"
|
||||
:chart-config="chartConfig"
|
||||
></lnbits-wallet-extra>
|
||||
<q-card class="lnbits-wallet-ads" v-if="AD_SPACE_ENABLED">
|
||||
<q-card-section class="text-subtitle1">
|
||||
<span v-text="AD_SPACE_TITLE"></span>
|
||||
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.ads">
|
||||
<q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img>
|
||||
<q-img class="q-mb-xs" v-else :src="ad[2]"></q-img>
|
||||
</a>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<lnbits-wallet-charts
|
||||
:payment-filter="paymentFilter"
|
||||
:chart-config="chartConfig"
|
||||
></lnbits-wallet-charts>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" position="top" @hide="onReceiveDialogHide">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<q-form @submit="createInvoice" class="q-gutter-md">
|
||||
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
||||
<b v-text="receive.lnurl.domain"></b> is requesting an invoice:
|
||||
</p>
|
||||
<q-input
|
||||
v-if="!g.isSatsDenomination"
|
||||
filled
|
||||
dense
|
||||
v-model="receive.data.amount"
|
||||
:label="$t('amount') + '(' + denomination + ') *'"
|
||||
mask="#.##"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:min="receive.minMax[0]"
|
||||
:max="receive.minMax[1]"
|
||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||
></q-input>
|
||||
<div v-else>
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="receive.unit"
|
||||
type="text"
|
||||
:label="$t('unit')"
|
||||
:options="receive.units"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-btn
|
||||
v-if="g.fiatTracking"
|
||||
@click="g.isFiatPriority = !g.isFiatPriority"
|
||||
class="float-right"
|
||||
color="primary"
|
||||
flat
|
||||
dense
|
||||
icon="swap_vert"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
class="q-mt-md"
|
||||
ref="setAmount"
|
||||
filled
|
||||
:pattern="receive.unit === 'sat' ? '\\d*' : '\\d*\\.?\\d*'"
|
||||
inputmode="numeric"
|
||||
dense
|
||||
v-model.number="receive.data.amount"
|
||||
:label="$t('amount') + ' (' + receive.unit + ') *'"
|
||||
:min="receive.minMax[0]"
|
||||
:max="receive.minMax[1]"
|
||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||
></q-input>
|
||||
</div>
|
||||
<q-input
|
||||
v-if="has_holdinvoice"
|
||||
filled
|
||||
dense
|
||||
v-model="receive.data.payment_hash"
|
||||
:label="$t('hold_invoice_payment_hash')"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
type="textarea"
|
||||
rows="2"
|
||||
v-model="receive.data.memo"
|
||||
:label="$t('memo')"
|
||||
>
|
||||
<template v-if="receive.data.internalMemo === null" v-slot:append>
|
||||
<q-icon
|
||||
name="add_comment"
|
||||
@click.stop.prevent="receive.data.internalMemo = ''"
|
||||
class="cursor-pointer"
|
||||
></q-icon>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('internal_memo')"></span>
|
||||
</q-tooltip>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-if="receive.data.internalMemo !== null"
|
||||
autogrow
|
||||
filled
|
||||
dense
|
||||
v-model="receive.data.internalMemo"
|
||||
class="q-mb-lg"
|
||||
:label="$t('internal_memo')"
|
||||
:hint="$t('internal_memo_hint_receive')"
|
||||
:rules="[
|
||||
val =>
|
||||
!val || val.length <= 512 || 'Please use maximum 512 characters'
|
||||
]"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
name="cancel"
|
||||
@click.stop.prevent="receive.data.internalMemo = null"
|
||||
class="cursor-pointer"
|
||||
/> </template
|
||||
></q-input>
|
||||
<div v-if="g.user.fiat_providers?.length" class="q-mt-md">
|
||||
<q-list bordered dense class="rounded-borders">
|
||||
<q-item-label dense header>
|
||||
<span v-text="$t('select_payment_provider')"></span>
|
||||
</q-item-label>
|
||||
<q-separator></q-separator>
|
||||
<q-item
|
||||
:active="!receive.fiatProvider"
|
||||
@click="receive.fiatProvider = ''"
|
||||
active-class="bg-teal-1 text-grey-8 text-weight-bold"
|
||||
clickable
|
||||
v-ripple
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar square>
|
||||
<img src="/static/images/logos/lnbits.png" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span
|
||||
v-text="$t('pay_with', {provider: 'Lightning Network'})"
|
||||
></span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator></q-separator>
|
||||
<q-item
|
||||
:active="receive.fiatProvider === 'stripe'"
|
||||
@click="receive.fiatProvider = 'stripe'"
|
||||
active-class="bg-teal-1 text-grey-8 text-weight-bold"
|
||||
clickable
|
||||
v-ripple
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img src="/static/images/stripe_logo.ico" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span v-text="$t('pay_with', {provider: 'Stripe'})"></span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
|
||||
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
v-if="receive.lnurl"
|
||||
v-text="`${$t('withdraw_from')} ${receive.lnurl.domain}`"
|
||||
></span>
|
||||
<span v-else v-text="$t('create_invoice')"></span>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<q-spinner-bars
|
||||
v-if="receive.status == 'loading'"
|
||||
color="primary"
|
||||
size="2.55em"
|
||||
></q-spinner-bars>
|
||||
</q-form>
|
||||
</q-card>
|
||||
<q-card
|
||||
v-else-if="receive.paymentReq && receive.lnurl == null"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<lnbits-qrcode
|
||||
v-if="receive.fiatPaymentReq"
|
||||
:show-buttons="false"
|
||||
:href="receive.fiatPaymentReq"
|
||||
:value="receive.fiatPaymentReq"
|
||||
>
|
||||
</lnbits-qrcode>
|
||||
<lnbits-qrcode
|
||||
v-else
|
||||
:href="'lightning:' + receive.paymentReq"
|
||||
:value="'lightning:' + receive.paymentReq"
|
||||
>
|
||||
</lnbits-qrcode>
|
||||
<div class="text-center">
|
||||
<h3 class="q-my-md">
|
||||
<span v-text="formattedAmount"></span>
|
||||
</h3>
|
||||
<h5 v-if="receive.unit != 'sat'" class="q-mt-none q-mb-sm">
|
||||
<span v-text="formattedSatAmount"></span>
|
||||
</h5>
|
||||
<div v-if="!receive.fiatPaymentReq">
|
||||
<q-chip v-if="hasNfc" outline square color="positive">
|
||||
<q-avatar icon="nfc" color="positive" text-color="white"></q-avatar>
|
||||
<span v-text="$t('nfc_supported')"></span>
|
||||
</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="parse.show" @hide="closeParseDialog" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div v-if="parse.invoice">
|
||||
<div class="column content-center text-center q-mb-md">
|
||||
<div v-if="!g.isFiatPriority">
|
||||
<h4 class="q-my-none text-bold">
|
||||
<span v-text="formatBalance(parse.invoice.sat)"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4
|
||||
class="q-my-none text-bold"
|
||||
v-text="parse.invoice.fiatAmount"
|
||||
></h4>
|
||||
</div>
|
||||
<div class="q-my-md absolute">
|
||||
<q-btn
|
||||
v-if="g.fiatTracking"
|
||||
@click="g.isFiatPriority = !g.isFiatPriority"
|
||||
flat
|
||||
dense
|
||||
icon="swap_vert"
|
||||
color="primary"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div v-if="g.fiatTracking">
|
||||
<div v-if="g.isFiatPriority">
|
||||
<h5 class="q-my-none text-bold">
|
||||
<span v-text="formatBalance(parse.invoice.sat)"></span>
|
||||
</h5>
|
||||
</div>
|
||||
<div v-else style="opacity: 0.75">
|
||||
<div class="text-h5 text-italic">
|
||||
<span
|
||||
v-text="parse.invoice.fiatAmount"
|
||||
style="opacity: 0.75"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<h6 class="text-center" v-text="parse.invoice.description"></h6>
|
||||
<q-input
|
||||
autogrow
|
||||
filled
|
||||
dense
|
||||
v-model="parse.data.internalMemo"
|
||||
:label="$t('internal_memo')"
|
||||
:hint="$t('internal_memo_hint_pay')"
|
||||
class="q-mb-lg"
|
||||
:rules="[
|
||||
val =>
|
||||
!val || val.length <= 512 || 'Please use maximum 512 characters'
|
||||
]"
|
||||
><template v-if="parse.data.internalMemo" v-slot:append>
|
||||
<q-icon
|
||||
name="cancel"
|
||||
@click.stop.prevent="parse.data.internalMemo = null"
|
||||
class="cursor-pointer" /></template
|
||||
></q-input>
|
||||
<q-list separator bordered dense class="q-mb-md">
|
||||
<q-expansion-item expand-separator icon="info" label="Details">
|
||||
<q-list separator>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('created')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="parse.invoice.createdDate"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side top>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="parse.invoice.createdDateFrom"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('expire_date')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="parse.invoice.expireDate"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side top>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="parse.invoice.expireDateFrom"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('payment_hash')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="
|
||||
`${parse.invoice.hash.slice(0, 12)}...${parse.invoice.hash.slice(-12)}`
|
||||
"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-item-label>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(parse.invoice.hash)"
|
||||
size="1em"
|
||||
color="grey"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</q-item-label>
|
||||
<q-tooltip>
|
||||
<span v-text="parse.invoice.hash"></span>
|
||||
</q-tooltip>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('Invoice')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="
|
||||
`${parse.invoice.bolt11.slice(0, 12)}...${parse.invoice.bolt11.slice(-12)}`
|
||||
"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-item-label>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(parse.invoice.bolt11)"
|
||||
size="1em"
|
||||
color="grey"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</q-item-label>
|
||||
<q-tooltip>
|
||||
<span v-text="parse.invoice.bolt11"></span>
|
||||
</q-tooltip>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
<div v-if="canPay" class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="payInvoice"
|
||||
:label="$t('pay')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div v-else class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('not_enough_funds')"
|
||||
unelevated
|
||||
disabled
|
||||
color="yellow"
|
||||
text-color="black"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="parse.lnurlauth">
|
||||
<q-form @submit="authLnurl" class="q-gutter-md">
|
||||
<p class="q-my-none text-h6">
|
||||
Authenticate with <b v-text="parse.lnurlauth.domain"></b>?
|
||||
</p>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<p>
|
||||
For every website and for every LNbits wallet, a new keypair will be
|
||||
deterministically generated so your identity can't be tied to your
|
||||
LNbits wallet or linked across websites. No other data will be
|
||||
shared with
|
||||
<span v-text="parse.lnurlauth.domain"></span>.
|
||||
</p>
|
||||
<p>Your public key for <b v-text="parse.lnurlauth.domain"></b> is:</p>
|
||||
<p class="q-mx-xl">
|
||||
<code class="text-wrap" v-text="parse.lnurlauth.pubkey"></code>
|
||||
</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
:label="$t('login')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
:label="$t('cancel')"
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
<div v-else-if="parse.lnurlpay">
|
||||
<q-form @submit="payLnurl" class="q-gutter-md">
|
||||
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
||||
<b v-text="parse.lnurlpay.domain"></b> is requesting
|
||||
<span v-text="msatoshiFormat(parse.lnurlpay.maxSendable)"></span>
|
||||
<span v-text="denomination"></span>
|
||||
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||
<br />
|
||||
and a
|
||||
<span v-text="parse.lnurlpay.commentAllowed"></span>-char comment
|
||||
</span>
|
||||
</p>
|
||||
<p v-else class="q-my-none text-h6 text-center">
|
||||
<b v-text="parse.lnurlpay.targetUser || parse.lnurlpay.domain"></b>
|
||||
is requesting <br />
|
||||
between
|
||||
<b v-text="msatoshiFormat(parse.lnurlpay.minSendable)"></b> and
|
||||
<b v-text="msatoshiFormat(parse.lnurlpay.maxSendable)"></b>
|
||||
<span v-text="denomination"></span>
|
||||
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||
<br />
|
||||
and a
|
||||
<span v-text="parse.lnurlpay.commentAllowed"></span>-char comment
|
||||
</span>
|
||||
</p>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<div class="row">
|
||||
<p
|
||||
class="col text-justify text-italic"
|
||||
v-text="parse.lnurlpay.description"
|
||||
></p>
|
||||
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
|
||||
<q-img :src="parse.lnurlpay.image" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col q-mb-lg">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-if="!parse.lnurlpay.fixed"
|
||||
v-model="parse.data.unit"
|
||||
type="text"
|
||||
:label="$t('unit')"
|
||||
:options="receive.units"
|
||||
></q-select>
|
||||
<br />
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.number="parse.data.amount"
|
||||
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
||||
:mask="parse.data.unit == 'sat' ? '#' : ''"
|
||||
:step="parse.data.unit == 'sat' ? '1' : '0.01'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:min="parse.lnurlpay.minSendable / 1000"
|
||||
:max="parse.lnurlpay.maxSendable / 1000"
|
||||
:readonly="parse.lnurlpay && parse.lnurlpay.fixed"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="parse.data.comment"
|
||||
:type="
|
||||
parse.lnurlpay.commentAllowed > 512 ? 'textarea' : 'text'
|
||||
"
|
||||
label="Comment (optional)"
|
||||
:maxlength="parse.lnurlpay.commentAllowed"
|
||||
><template
|
||||
v-if="parse.data.internalMemo === null"
|
||||
v-slot:append
|
||||
>
|
||||
<q-icon
|
||||
name="add_comment"
|
||||
@click.stop.prevent="parse.data.internalMemo = ''"
|
||||
class="cursor-pointer"
|
||||
></q-icon>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('internal_memo')"></span>
|
||||
</q-tooltip> </template
|
||||
></q-input>
|
||||
<br />
|
||||
<q-input
|
||||
v-if="parse.data.internalMemo !== null"
|
||||
autogrow
|
||||
filled
|
||||
dense
|
||||
v-model="parse.data.internalMemo"
|
||||
:label="$t('internal_memo')"
|
||||
:hint="$t('internal_memo_hint_pay')"
|
||||
class=""
|
||||
:rules="[
|
||||
val =>
|
||||
!val ||
|
||||
val.length <= 512 ||
|
||||
'Please use maximum 512 characters'
|
||||
]"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
name="cancel"
|
||||
@click.stop.prevent="parse.data.internalMemo = null"
|
||||
class="cursor-pointer"
|
||||
/> </template
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" type="submit">Send</q-btn>
|
||||
<q-btn
|
||||
:label="$t('cancel')"
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-form
|
||||
v-if="!parse.camera.show"
|
||||
@submit="decodeRequest"
|
||||
class="q-gutter-md"
|
||||
>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="parse.data.request"
|
||||
type="textarea"
|
||||
:label="$t('paste_invoice_label')"
|
||||
ref="textArea"
|
||||
>
|
||||
</q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="parse.data.request == ''"
|
||||
type="submit"
|
||||
:label="$t('read')"
|
||||
></q-btn>
|
||||
<q-icon
|
||||
name="content_paste"
|
||||
color="grey"
|
||||
class="q-mt-xs q-ml-sm q-mr-auto"
|
||||
v-if="parse.copy.show"
|
||||
@click="pasteToTextArea"
|
||||
>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('paste_from_clipboard')"></span>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
<div v-else>
|
||||
<q-responsive :ratio="1">
|
||||
<qrcode-stream
|
||||
@detect="decodeQR"
|
||||
@camera-on="onInitQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</q-responsive>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('cancel')"
|
||||
@click="closeCamera"
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="parse.camera.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl">
|
||||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream
|
||||
@detect="decodeQR"
|
||||
@camera-on="onInitQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@click="closeCamera"
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<div
|
||||
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
|
||||
>
|
||||
<q-tabs active-class="px-0" indicator-color="transparent" align="justify">
|
||||
<q-tab
|
||||
icon="file_download"
|
||||
@click="showReceiveDialog"
|
||||
:label="$t('receive')"
|
||||
>
|
||||
</q-tab>
|
||||
|
||||
<q-tab @click="showParseDialog" icon="file_upload" :label="$t('send')">
|
||||
</q-tab>
|
||||
</q-tabs>
|
||||
<q-btn
|
||||
round
|
||||
size="35px"
|
||||
unelevated
|
||||
icon="qr_code_scanner"
|
||||
@click="showCamera"
|
||||
class="text-white bg-primary z-top vertical-bottom absolute-center absolute"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
@@ -103,6 +103,7 @@
|
||||
"js/pages/node.js",
|
||||
"js/pages/node-public.js",
|
||||
"js/pages/audit.js",
|
||||
"js/pages/wallet.js",
|
||||
"js/pages/wallets.js",
|
||||
"js/pages/users.js",
|
||||
"js/pages/account.js",
|
||||
@@ -125,6 +126,7 @@
|
||||
"js/components/lnbits-wallet-new.js",
|
||||
"js/components/lnbits-wallet-share.js",
|
||||
"js/components/lnbits-wallet-paylinks.js",
|
||||
"js/components/lnbits-wallet-extra.js",
|
||||
"js/components/lnbits-home-logos.js",
|
||||
"js/components/lnbits-qrcode.js",
|
||||
"js/components/lnbits-qrcode-lnurl.js",
|
||||
|
||||
+14
-1
@@ -418,9 +418,22 @@ async def test_get_payments_paginated(client, inkey_fresh_headers_to, fake_payme
|
||||
)
|
||||
assert response.status_code == 200
|
||||
paginated = response.json()
|
||||
assert len(paginated["data"]) == 2
|
||||
data = paginated["data"]
|
||||
assert len(data) == 2
|
||||
assert paginated["total"] == len(fake_data)
|
||||
|
||||
checking_id_list = [payment["checking_id"] for payment in data]
|
||||
params = {"checking_id[in]": ",".join(checking_id_list)}
|
||||
response = await client.get(
|
||||
"/api/v1/payments/paginated",
|
||||
params=params,
|
||||
headers=inkey_fresh_headers_to,
|
||||
)
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 2
|
||||
for payment in data:
|
||||
assert payment["checking_id"] in checking_id_list
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_payments_history(client, inkey_fresh_headers_to, fake_payments):
|
||||
|
||||
@@ -25,13 +25,6 @@ async def test_get_wallet_with_user_and_wallet(client, to_user, to_wallet):
|
||||
assert response.status_code == 200, f"{response.url} {response.status_code}"
|
||||
|
||||
|
||||
# check GET /wallet: wrong wallet and user, expect 400
|
||||
@pytest.mark.anyio
|
||||
async def test_get_wallet_with_user_and_wrong_wallet(client, to_user):
|
||||
response = await client.get("wallet", params={"usr": to_user.id, "wal": "1"})
|
||||
assert response.status_code == 400, f"{response.url} {response.status_code}"
|
||||
|
||||
|
||||
# check GET /extensions: extensions list
|
||||
@pytest.mark.anyio
|
||||
async def test_get_extensions(client, to_user):
|
||||
|
||||
+46
-1
@@ -1,10 +1,12 @@
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
import shortuuid
|
||||
from httpx import AsyncClient
|
||||
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.core.models.users import Account, User
|
||||
from lnbits.core.services.users import create_user_account
|
||||
from lnbits.settings import Settings
|
||||
from lnbits.utils.nostr import generate_keypair, hex_to_npub
|
||||
|
||||
@@ -465,3 +467,46 @@ async def test_create_user_invalid_npub(
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert create_resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_search_users(http_client: AsyncClient, superuser_token):
|
||||
namespace_id = shortuuid.uuid()[:8]
|
||||
users = []
|
||||
user_count = 15
|
||||
for index in range(user_count):
|
||||
username = f"u_{namespace_id}_{index:03d}"
|
||||
user = await create_user_account(
|
||||
Account(
|
||||
id=uuid4().hex,
|
||||
username=username,
|
||||
email=f"{username}@lnbits.com",
|
||||
pubkey="",
|
||||
external_id=None,
|
||||
)
|
||||
)
|
||||
users.append(user)
|
||||
|
||||
create_resp = await http_client.get(
|
||||
"/users/api/v1/user?sortby=id&direction=desc",
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert create_resp.status_code == 200
|
||||
create_resp = await http_client.get(
|
||||
"/users/api/v1/user"
|
||||
f"?sortby=username&direction=desc&username[like]=u_{namespace_id}",
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert create_resp.status_code == 200
|
||||
data = create_resp.json()
|
||||
assert data["total"] == user_count
|
||||
assert data["data"][0]["username"] == users[user_count - 1].username
|
||||
|
||||
create_resp = await http_client.get(
|
||||
"/users/api/v1/user" f"?sortby=username&direction=desc&id={users[0].id}",
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert create_resp.status_code == 200
|
||||
data = create_resp.json()
|
||||
assert data["total"] == 1
|
||||
assert data["data"][0]["username"] == users[0].username
|
||||
|
||||
@@ -36,37 +36,42 @@ async def test_crud_get_payments(app):
|
||||
await update_wallet_balance(wallet, -10)
|
||||
wallet.balance_msat += -10 * 1000
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id)
|
||||
filters = Filters(limit=100)
|
||||
payments = await get_payments(wallet_id=wallet.id, filters=filters)
|
||||
assert len(payments) == 22, "should return 22 successful payments"
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id, incoming=True)
|
||||
payments = await get_payments(wallet_id=wallet.id, incoming=True, filters=filters)
|
||||
assert len(payments) == 11, "should return 11 successful incoming payments"
|
||||
await update_payments(payments)
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id, outgoing=True)
|
||||
payments = await get_payments(wallet_id=wallet.id, outgoing=True, filters=filters)
|
||||
assert len(payments) == 11, "should return 11 successful outgoing payments"
|
||||
await update_payments(payments)
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id, pending=True)
|
||||
payments = await get_payments(wallet_id=wallet.id, pending=True, filters=filters)
|
||||
assert len(payments) == 4, "should return 4 pending payments"
|
||||
|
||||
# function signature should have Optional[bool] for complete and pending to make
|
||||
# this distinction possible
|
||||
payments = await get_payments(wallet_id=wallet.id, pending=False)
|
||||
payments = await get_payments(wallet_id=wallet.id, pending=False, filters=filters)
|
||||
assert len(payments) == 22, "should return all payments"
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id, complete=True, pending=True)
|
||||
payments = await get_payments(
|
||||
wallet_id=wallet.id, complete=True, pending=True, filters=filters
|
||||
)
|
||||
assert len(payments) == 20, "should return 4 pending and 16 complete payments"
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id, complete=True, outgoing=True)
|
||||
payments = await get_payments(
|
||||
wallet_id=wallet.id, complete=True, outgoing=True, filters=filters
|
||||
)
|
||||
assert (
|
||||
len(payments) == 10
|
||||
), "should return 8 complete outgoing payments and 2 pending outgoing payments"
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id)
|
||||
payments = await get_payments(wallet_id=wallet.id, filters=filters)
|
||||
assert len(payments) == 22, "should return all payments"
|
||||
|
||||
payments = await get_payments(wallet_id=wallet.id, complete=True)
|
||||
payments = await get_payments(wallet_id=wallet.id, complete=True, filters=filters)
|
||||
assert (
|
||||
len(payments) == 18
|
||||
), "should return 14 successful payment and 4 pending payments"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from lnbits.core.crud.users import get_user_from_account
|
||||
from lnbits.core.crud.wallets import delete_wallet, get_wallets
|
||||
from lnbits.core.models.users import Account
|
||||
from lnbits.core.services.users import create_user_account
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_user_from_account_is_wallet_created():
|
||||
|
||||
username = f"user_{uuid4().hex[:8]}"
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=username,
|
||||
email=f"{username}@lnbits.com",
|
||||
)
|
||||
account.hash_password("secret1234")
|
||||
user = await create_user_account(account)
|
||||
|
||||
assert user is not None
|
||||
assert (
|
||||
len(user.wallets) == 1
|
||||
), "A wallet should be created for the user if none exist"
|
||||
|
||||
await delete_wallet(user_id=account.id, wallet_id=user.wallets[0].id)
|
||||
|
||||
wallets = await get_wallets(account.id, deleted=False)
|
||||
assert len(wallets) == 0, "User should have no wallets after deletion"
|
||||
|
||||
user = await get_user_from_account(account)
|
||||
|
||||
assert user is not None
|
||||
assert (
|
||||
len(user.wallets) == 1
|
||||
), "A new wallet should be created for the user if none exist after deletion"
|
||||
@@ -9,6 +9,7 @@ from lnbits.core.crud.wallets import (
|
||||
get_wallets,
|
||||
update_wallet,
|
||||
)
|
||||
from lnbits.core.models.payments import PaymentFilters
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.core.models.wallets import (
|
||||
Wallet,
|
||||
@@ -29,6 +30,7 @@ from lnbits.core.services.wallets import (
|
||||
reject_wallet_invitation,
|
||||
update_wallet_share_permissions,
|
||||
)
|
||||
from lnbits.db import Filters
|
||||
from lnbits.exceptions import InvoiceError, PaymentError
|
||||
from tests.conftest import new_user
|
||||
|
||||
@@ -563,7 +565,10 @@ async def test_shared_wallet_view_permissions(from_wallet: Wallet):
|
||||
await pay_invoice(wallet_id=from_wallet.id, payment_request=payment.bolt11)
|
||||
wallet_balance += payment.sat
|
||||
|
||||
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||
filters = Filters(limit=100, model=PaymentFilters)
|
||||
shared_wallet_payments = await get_payments(
|
||||
wallet_id=mirror_wallet.id, filters=filters
|
||||
)
|
||||
assert len(shared_wallet_payments) == payment_count
|
||||
mirror_wallet = await get_wallet(mirror_wallet.id)
|
||||
assert mirror_wallet is not None
|
||||
|
||||
Reference in New Issue
Block a user