Compare commits

..
Author SHA1 Message Date
dni ⚡ f93efbfb3a chore: update to version 1.5.0 (#3813) 2026-02-25 07:54:35 +01:00
dni ⚡ 4429c0cdb6 chore: update to v1.5.0-rc2 (#3806) 2026-02-25 07:54:33 +01:00
Vlad Stananddni ⚡ 2bdb89b4b9 fix: paypal subscriptions (#3797) 2026-02-25 07:54:31 +01:00
dni ⚡ 75754598e1 feat: restructure docker images, add sparkl2 docker image (#3794) 2026-02-25 07:54:30 +01:00
dni ⚡ b25a3c2bb4 feat: hide first_install_token if is not required (#3795) 2026-02-25 07:54:28 +01:00
dni ⚡ e7c4de6506 chore: update lnbits to version v1.5.0-rc1 (#3793) 2026-02-25 07:54:27 +01:00
c102f10fab feat: Spark L2 (#3715)
Co-authored-by: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:54:18 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7c28b58aaa chore(deps): bump pillow from 12.1.0 to 12.1.1 (#3785)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:54:16 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Vlad Stan
2b27f13fa8 chore(deps): bump axios from 1.13.2 to 1.13.5 (#3786)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:54:15 +01:00
130f4fd0bc feat: modern theme stuff like rounded corners and card gradient (#3766)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:54:14 +01:00
dni ⚡ 4894b70bad fix: /first_install new superuser may duplicate an username (#3787) 2026-02-25 07:54:13 +01:00
Arcanddni ⚡ 05769240d3 fix: fail closed on PayPal webhook verification errors (#3769) 2026-02-25 07:54:11 +01:00
krvianddni ⚡ 7bd127d22c fix: use canonical values in rate limit unit select (#3753) 2026-02-25 07:54:10 +01:00
Vlad Stananddni ⚡ 6dc3df148d fix: validation of invitation code (#3767) 2026-02-25 07:54:09 +01:00
Vlad Stananddni ⚡ e286546b63 [feat] User invitation code (#3761) 2026-02-25 07:54:07 +01:00
Vlad Stananddni ⚡ eae1be1db8 [feat] Allow no exchange providers (#3763) 2026-02-25 07:54:06 +01:00
dni ⚡ 25ab676a3f fix: datetime in db.py added utc offset (#3762) 2026-02-25 07:54:05 +01:00
Vlad Stananddni ⚡ 2fd6648a9a fix: handle validation errors when the response is binary (#3765) 2026-02-25 07:54:03 +01:00
Vlad Stananddni ⚡ 8eda653e3f fix: extension paid/free label (#3764) 2026-02-25 07:54:01 +01:00
Vlad Stananddni ⚡ 7e327b2c6a [feat] User activation (#3749) 2026-02-25 07:54:00 +01:00
Arcanddni ⚡ 40c055e373 feat: FIRST_INSTALL_TOKEN to help services like Umbrel secure the first_install endpoint (#3751)
Co-authored-by: dni  <office@dnilabs.com>
2026-02-25 07:53:59 +01:00
dni ⚡andVlad Stan 1d9808b269 refactor: payment.check_status() into check_payment_status(payment) (#3747)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:53:57 +01:00
dni ⚡ c65ab2af87 fix: uiCustomisation JS Error (#3752) 2026-02-25 07:53:56 +01:00
dni ⚡ 15f093c136 refactor: g.settings with a PublicSettings class for mapping, for reactive WINDOW_SETTINGS (#3644) 2026-02-25 07:53:55 +01:00
Vlad Stananddni ⚡ 5311b0a393 feat: wallet featured button (#3740) 2026-02-25 07:53:54 +01:00
Vlad Stananddni ⚡ e72396fe31 [feat] persist user ui customization (#3743) 2026-02-25 07:53:52 +01:00
dni ⚡ d6d0ebb20e chore: remove ecdsa in favor of coincurve (#3746) 2026-02-25 07:53:51 +01:00
dni ⚡ 563dd5b499 CI: use uv for publishing on pypi (#3745) 2026-02-25 07:53:49 +01:00
Vlad Stananddni ⚡ 9be894da74 [feat] admin impersonate user (#3741) 2026-02-25 07:53:47 +01:00
Vlad Stananddni ⚡ 87b66c554a fix: DB migration conflict (#3739) 2026-02-25 07:53:46 +01:00
dni ⚡ 314b533e39 chore: update python packages (#3707) 2026-02-25 07:53:44 +01:00
Vlad Stananddni ⚡ af8f3382c1 feat: allow text upload (#3738) 2026-02-25 07:53:42 +01:00
Vlad Stananddni ⚡ 67bb86c1ce fix: better validation error messages (#3736) 2026-02-25 07:53:41 +01:00
dni ⚡ f0f9e41f99 chore: remove windowMixin from components and pages (#3632) 2026-02-25 07:53:39 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2fbaf57b5e chore(deps): bump cryptography from 42.0.8 to 44.0.1 (#3735)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:37 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
87afba2abb chore(deps): bump aiohttp from 3.12.15 to 3.13.3 (#3734)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:36 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c375bf1b15 chore(deps): bump urllib3 from 2.5.0 to 2.6.3 (#3733)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:34 +01:00
Djuri Baarsanddni ⚡ dbcc920d0a [fix] Fix missing timezone information 2026-02-25 07:53:32 +01:00
Vlad Stananddni ⚡ 2c61e219b8 [feat] Update extension builder for 1.4.1 (#3725)
Co-authored-by: dni  <office@dnilabs.com>
2026-02-25 07:53:31 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6cb9b7da1f chore(deps): bump werkzeug from 3.1.3 to 3.1.5 (#3732)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:30 +01:00
dni ⚡andGitHub 83334d67c8 HOTFIX: websocket with old balance was sent. (#3758) 2026-02-03 12:12:33 +01:00
10 changed files with 115 additions and 83 deletions
+35 -14
View File
@@ -24,7 +24,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
async def api_generic_webhook_handler( async def api_generic_webhook_handler(
provider_name: str, request: Request provider_name: str, request: Request
) -> SimpleStatus: ) -> SimpleStatus:
logger.info(f"Received callback from provider: '{provider_name}'.")
if provider_name.lower() == "stripe": if provider_name.lower() == "stripe":
payload = await request.body() payload = await request.body()
sig_header = request.headers.get("Stripe-Signature") sig_header = request.headers.get("Stripe-Signature")
@@ -181,8 +181,10 @@ async def _get_stripe_subscription_payment_options(
async def handle_paypal_event(event: dict): async def handle_paypal_event(event: dict):
event_id = event.get("id", "")
event_type = event.get("event_type", "") event_type = event.get("event_type", "")
resource = event.get("resource", {}) resource = event.get("resource", {})
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"): if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
payment_hash = _paypal_extract_payment_hash(resource) payment_hash = _paypal_extract_payment_hash(resource)
@@ -196,20 +198,17 @@ async def handle_paypal_event(event: dict):
await check_fiat_status(payment) await check_fiat_status(payment)
return return
if event_type in ( if event_type in ("PAYMENT.SALE.COMPLETED"):
"PAYMENT.SALE.COMPLETED",
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
):
await _handle_paypal_subscription_payment(resource) await _handle_paypal_subscription_payment(resource)
return return
logger.info(f"Unhandled PayPal event type: '{event_type}'.") logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
async def _handle_paypal_subscription_payment(resource: dict): async def _handle_paypal_subscription_payment(resource: dict):
amount_info = resource.get("amount") or {} amount_info = resource.get("amount") or {}
currency = (amount_info.get("currency_code") or "").upper() currency = (amount_info.get("currency") or "").upper()
total = amount_info.get("value") total = amount_info.get("total")
if not currency or total is None: if not currency or total is None:
raise ValueError("PayPal subscription event missing amount.") raise ValueError("PayPal subscription event missing amount.")
@@ -217,18 +216,14 @@ async def _handle_paypal_subscription_payment(resource: dict):
if not custom_id: if not custom_id:
raise ValueError("PayPal subscription event missing custom metadata.") raise ValueError("PayPal subscription event missing custom metadata.")
try: payment_options = _deserialize_paypal_metadata(custom_id)
metadata = json.loads(custom_id)
except json.JSONDecodeError:
metadata = {}
payment_options = FiatSubscriptionPaymentOptions(**metadata)
if not payment_options.wallet_id: if not payment_options.wallet_id:
raise ValueError("PayPal subscription event missing wallet_id.") raise ValueError("PayPal subscription event missing wallet_id.")
memo = payment_options.memo or "" memo = payment_options.memo or ""
extra = { extra = {
**(payment_options.extra or {}), **(payment_options.extra or {}),
"subscription_request_id": resource.get("billing_agreement_id"),
"fiat_method": "subscription", "fiat_method": "subscription",
"tag": payment_options.tag, "tag": payment_options.tag,
"subscription": { "subscription": {
@@ -259,3 +254,29 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
if pu.get("custom_id"): if pu.get("custom_id"):
return pu.get("custom_id") return pu.get("custom_id")
return None return None
def _deserialize_paypal_metadata(custom_id: str) -> FiatSubscriptionPaymentOptions:
try:
meta = json.loads(custom_id)
wallet_id = meta[0] if len(meta) > 0 else None
tag = meta[1] if len(meta) > 1 else None
subscription_request_id = meta[2] if len(meta) > 2 else None
extra_link = meta[3] if len(meta) > 3 else None
memo = meta[4] if len(meta) > 4 else None
extra = {
"link": extra_link,
"subscription_request_id": subscription_request_id,
}
return FiatSubscriptionPaymentOptions(
wallet_id=wallet_id,
tag=tag,
subscription_request_id=subscription_request_id,
extra=extra,
memo=memo,
)
except (json.JSONDecodeError, IndexError) as e:
logger.warning(f"Failed to deserialize PayPal metadata: {e}")
return FiatSubscriptionPaymentOptions()
+12
View File
@@ -1,6 +1,7 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from lnbits.core.models.misc import SimpleStatus from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.wallets import WalletTypeInfo from lnbits.core.models.wallets import WalletTypeInfo
@@ -30,6 +31,11 @@ async def create_subscription(
data: CreateFiatSubscription, data: CreateFiatSubscription,
key_type: WalletTypeInfo = Depends(require_admin_key), key_type: WalletTypeInfo = Depends(require_admin_key),
) -> FiatSubscriptionResponse: ) -> FiatSubscriptionResponse:
logger.debug(
f"Creating subscription with provider '{provider}. '"
f"Subscription ID '{data.subscription_id}'."
)
fiat_provider = await get_fiat_provider(provider) fiat_provider = await get_fiat_provider(provider)
if not fiat_provider: if not fiat_provider:
raise HTTPException(404, "Fiat provider not found") raise HTTPException(404, "Fiat provider not found")
@@ -47,6 +53,12 @@ async def create_subscription(
subscription_response = await fiat_provider.create_subscription( subscription_response = await fiat_provider.create_subscription(
data.subscription_id, data.quantity, data.payment_options data.subscription_id, data.quantity, data.payment_options
) )
if subscription_response.error_message:
logger.warning(
f"Failed to create subscription with provider '{provider}': "
f"{subscription_response.error_message}"
)
return subscription_response return subscription_response
+63 -18
View File
@@ -2,7 +2,7 @@ import asyncio
import json import json
import time import time
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Any from typing import Any, Literal
import httpx import httpx
from loguru import logger from loguru import logger
@@ -24,6 +24,8 @@ from .base import (
FiatSubscriptionResponse, FiatSubscriptionResponse,
) )
FiatMethod = Literal["checkout", "subscription"]
class PayPalCheckoutOptions(BaseModel): class PayPalCheckoutOptions(BaseModel):
class Config: class Config:
@@ -34,11 +36,21 @@ class PayPalCheckoutOptions(BaseModel):
metadata: dict[str, Any] = Field(default_factory=dict) metadata: dict[str, Any] = Field(default_factory=dict)
class PayPalSubscriptionOptions(BaseModel):
class Config:
extra = "ignore"
checking_id: str | None = None
payment_request: str | None = None
class PayPalCreateInvoiceOptions(BaseModel): class PayPalCreateInvoiceOptions(BaseModel):
class Config: class Config:
extra = "ignore" extra = "ignore"
fiat_method: FiatMethod = "checkout"
checkout: PayPalCheckoutOptions | None = None checkout: PayPalCheckoutOptions | None = None
subscription: PayPalSubscriptionOptions | None = None
class PayPalWallet(FiatProvider): class PayPalWallet(FiatProvider):
@@ -96,6 +108,14 @@ class PayPalWallet(FiatProvider):
opts = self._parse_create_opts(extra or {}) opts = self._parse_create_opts(extra or {})
if opts is None: if opts is None:
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options") return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
if opts.fiat_method == "subscription":
term = opts.subscription or PayPalSubscriptionOptions()
checking_id = term.checking_id or urlsafe_short_hash()
return FiatInvoiceResponse(
ok=True,
checking_id=f"subscription_{checking_id}",
payment_request=term.payment_request or "",
)
try: try:
await self._ensure_access_token() await self._ensure_access_token()
@@ -171,6 +191,7 @@ class PayPalWallet(FiatProvider):
or "https://lnbits.com" or "https://lnbits.com"
) )
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
if not payment_options.subscription_request_id: if not payment_options.subscription_request_id:
payment_options.subscription_request_id = urlsafe_short_hash() payment_options.subscription_request_id = urlsafe_short_hash()
payment_options.extra = payment_options.extra or {} payment_options.extra = payment_options.extra or {}
@@ -182,7 +203,6 @@ class PayPalWallet(FiatProvider):
await self._ensure_access_token() await self._ensure_access_token()
payload = { payload = {
"plan_id": subscription_id, "plan_id": subscription_id,
"quantity": str(quantity),
"custom_id": self._serialize_metadata(payment_options), "custom_id": self._serialize_metadata(payment_options),
"application_context": { "application_context": {
"return_url": success_url, "return_url": success_url,
@@ -205,7 +225,7 @@ class PayPalWallet(FiatProvider):
return FiatSubscriptionResponse( return FiatSubscriptionResponse(
ok=True, ok=True,
checkout_session_url=approval_url, checkout_session_url=approval_url,
subscription_request_id=payment_options.subscription_request_id, subscription_request_id=data.get("id"),
) )
except Exception as exc: except Exception as exc:
logger.warning(exc) logger.warning(exc)
@@ -219,6 +239,10 @@ class PayPalWallet(FiatProvider):
correlation_id: str, correlation_id: str,
**kwargs, **kwargs,
) -> FiatSubscriptionResponse: ) -> FiatSubscriptionResponse:
logger.debug(
f"Cancelling PayPal subscription '{subscription_id}'. "
f"Correlation ID '{correlation_id}'."
)
try: try:
await self._ensure_access_token() await self._ensure_access_token()
r = await self.client.post( r = await self.client.post(
@@ -240,12 +264,20 @@ class PayPalWallet(FiatProvider):
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus: async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try: try:
await self._ensure_access_token() await self._ensure_access_token()
order_id = self._normalize_paypal_id(checking_id) paypal_id = self._normalize_paypal_id(checking_id)
r = await self.client.get( if paypal_id.startswith("subscription_"):
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers() capture_id = paypal_id.replace("subscription_", "", 1)
) r = await self.client.get(
r.raise_for_status() f"v2/payments/captures/{capture_id}", headers=self._auth_headers()
return self._status_from_order(r.json()) )
r.raise_for_status()
return self._status_from_capture(r.json())
else:
r = await self.client.get(
f"/v2/checkout/orders/{paypal_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_order(r.json())
except Exception as exc: except Exception as exc:
logger.debug(f"Error getting PayPal order status: {exc}") logger.debug(f"Error getting PayPal order status: {exc}")
return FiatPaymentPendingStatus() return FiatPaymentPendingStatus()
@@ -270,6 +302,14 @@ class PayPalWallet(FiatProvider):
return FiatPaymentFailedStatus() return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus() return FiatPaymentPendingStatus()
def _status_from_capture(self, order: dict[str, Any]) -> FiatPaymentStatus:
status = (order.get("status") or "").upper()
if status in ["COMPLETED"]:
return FiatPaymentSuccessStatus()
if status in ["DECLINED", "FAILED", "CANCELLED", "CANCELED"]:
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _normalize_paypal_id(self, checking_id: str) -> str: def _normalize_paypal_id(self, checking_id: str) -> str:
return ( return (
checking_id.replace("fiat_paypal_", "", 1) checking_id.replace("fiat_paypal_", "", 1)
@@ -280,15 +320,20 @@ class PayPalWallet(FiatProvider):
def _serialize_metadata( def _serialize_metadata(
self, payment_options: FiatSubscriptionPaymentOptions self, payment_options: FiatSubscriptionPaymentOptions
) -> str: ) -> str:
md = { meta = [
"wallet_id": payment_options.wallet_id, payment_options.wallet_id,
"memo": payment_options.memo, payment_options.tag,
"extra": payment_options.extra, payment_options.subscription_request_id,
"tag": payment_options.tag, ]
"subscription_request_id": payment_options.subscription_request_id, if payment_options.extra:
} meta.append(payment_options.extra.get("link", None))
raw = json.dumps(md)
return raw[:127] # PayPal custom_id limit # Keep custom_id within PayPal's 127-char limit
memo_limit = 120 - len(json.dumps(meta))
if memo_limit > 0 and payment_options.memo:
meta.append(payment_options.memo[:memo_limit])
return json.dumps(meta)
def _parse_create_opts( def _parse_create_opts(
self, raw_opts: dict[str, Any] self, raw_opts: dict[str, Any]
File diff suppressed because one or more lines are too long
@@ -43,52 +43,7 @@ window.app.component('lnbits-qrcode', {
nfcSupported: typeof NDEFReader != 'undefined' nfcSupported: typeof NDEFReader != 'undefined'
} }
}, },
computed: {
qrValue() {
return this.normalizeQrValue(this.value)
}
},
methods: { methods: {
normalizeQrValue(value) {
if (typeof value !== 'string' || value.length === 0) {
return value
}
const lower = value.toLowerCase()
const upper = value.toUpperCase()
const isMixedCase = value !== lower && value !== upper
if (isMixedCase) {
return value
}
const prefix = 'lightning:'
if (lower.startsWith(prefix)) {
const prefixOriginal = value.slice(0, prefix.length)
const rest = value.slice(prefix.length)
const normalizedRest = this.normalizeBech32IfLower(rest)
if (normalizedRest !== rest) {
return prefixOriginal.toUpperCase() + normalizedRest
}
return value
}
return this.normalizeBech32IfLower(value)
},
normalizeBech32IfLower(value) {
if (typeof value !== 'string' || value.length === 0) {
return value
}
const lower = value.toLowerCase()
if (value !== lower) {
return value
}
// Bech32 strings are case-insensitive, but must be all upper or all lower.
// Uppercasing enables QR alphanumeric mode and reduces QR size for LN strings.
const bech32Like = /^ln[0-9a-z]*1[0-9ac-hj-np-z]+$/
return bech32Like.test(value) ? value.toUpperCase() : value
},
printQrCode() { printQrCode() {
const svg = this.$refs.qrCode.$el.outerHTML const svg = this.$refs.qrCode.$el.outerHTML
const printWindow = window.open('', '_blank') const printWindow = window.open('', '_blank')
@@ -408,7 +408,6 @@
<li><code>CHECKOUT.ORDER.APPROVED</code></li> <li><code>CHECKOUT.ORDER.APPROVED</code></li>
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li> <li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
<li><code>PAYMENT.SALE.COMPLETED</code></li> <li><code>PAYMENT.SALE.COMPLETED</code></li>
<li><code>BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED</code></li>
</ul> </ul>
</q-card-section> </q-card-section>
</q-expansion-item> </q-expansion-item>
@@ -12,7 +12,7 @@
> >
<qrcode-vue <qrcode-vue
ref="qrCode" ref="qrCode"
:value="qrValue" :value="value"
:margin="margin" :margin="margin"
:size="size" :size="size"
level="Q" level="Q"
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.5.0-rc1" version = "1.5.0"
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
+1 -1
View File
@@ -708,7 +708,7 @@ async def test_user_activation(
f"/users/api/v1/user/{user_id}/activate", f"/users/api/v1/user/{user_id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"}, headers={"Authorization": f"Bearer {superuser_token}"},
) )
print("### response", response.text)
assert response.status_code == 200 assert response.status_code == 200
assert response.json().get("message") == "User activated." assert response.json().get("message") == "User activated."
Generated
+1 -1
View File
@@ -1247,7 +1247,7 @@ wheels = [
[[package]] [[package]]
name = "lnbits" name = "lnbits"
version = "1.5.0rc1" version = "1.5.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },