Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8069c531d | ||
|
|
a9374d509c | ||
|
|
3e72788fc8 | ||
|
|
f48d24aca8 | ||
|
|
015262c9b3 | ||
|
|
e5e481f836 | ||
|
|
6c32ebf7e6 |
@@ -15,8 +15,8 @@ Note that by default LNbits uses SQLite as its database, which is simple and eff
|
|||||||
Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppImage, or:
|
Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppImage, or:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt-get install libfuse2
|
sudo apt-get install jq libfuse2
|
||||||
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
|
wget $(curl -s https://api.github.com/repos/arcbtc/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
|
||||||
chmod +x LNbits-latest.AppImage
|
chmod +x LNbits-latest.AppImage
|
||||||
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
|
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ExplicitRelease(BaseModel):
|
|||||||
info_notification: str | None
|
info_notification: str | None
|
||||||
critical_notification: str | None
|
critical_notification: str | None
|
||||||
details_link: str | None
|
details_link: str | None
|
||||||
|
paid_features: str | None
|
||||||
pay_link: str | None
|
pay_link: str | None
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
@@ -187,6 +188,7 @@ class ExtensionRelease(BaseModel):
|
|||||||
icon: str | None = None
|
icon: str | None = None
|
||||||
details_link: str | None = None
|
details_link: str | None = None
|
||||||
|
|
||||||
|
paid_features: str | None = None
|
||||||
pay_link: str | None = None
|
pay_link: str | None = None
|
||||||
cost_sats: int | None = None
|
cost_sats: int | None = None
|
||||||
paid_sats: int | None = 0
|
paid_sats: int | None = 0
|
||||||
@@ -256,6 +258,7 @@ class ExtensionRelease(BaseModel):
|
|||||||
html_url=e.html_url,
|
html_url=e.html_url,
|
||||||
details_link=e.details_link,
|
details_link=e.details_link,
|
||||||
pay_link=e.pay_link,
|
pay_link=e.pay_link,
|
||||||
|
paid_features=e.paid_features,
|
||||||
repo=e.repo,
|
repo=e.repo,
|
||||||
icon=e.icon,
|
icon=e.icon,
|
||||||
)
|
)
|
||||||
@@ -308,6 +311,9 @@ class ExtensionMeta(BaseModel):
|
|||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
archive: str | None = None
|
archive: str | None = None
|
||||||
featured: bool = False
|
featured: bool = False
|
||||||
|
paid_features: str | None = None
|
||||||
|
has_paid_release: bool = False
|
||||||
|
has_free_release: bool = False
|
||||||
|
|
||||||
|
|
||||||
class InstallableExtension(BaseModel):
|
class InstallableExtension(BaseModel):
|
||||||
@@ -451,9 +457,23 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
shutil.rmtree(self.ext_upgrade_dir, True)
|
shutil.rmtree(self.ext_upgrade_dir, True)
|
||||||
|
|
||||||
def check_latest_version(self, release: ExtensionRelease | None):
|
def check_release_updates(self, release: ExtensionRelease | None):
|
||||||
|
self._check_latest_version(release)
|
||||||
|
self._check_payment_link(release)
|
||||||
|
|
||||||
|
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
|
||||||
|
if not pay_link or not self.meta or not self.meta.payments:
|
||||||
|
return None
|
||||||
|
return next(
|
||||||
|
(p for p in self.meta.payments if p.pay_link == pay_link),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_latest_version(self, release: ExtensionRelease | None):
|
||||||
if not release:
|
if not release:
|
||||||
return
|
return
|
||||||
|
if not release.is_version_compatible:
|
||||||
|
return
|
||||||
if not self.meta or not self.meta.latest_release:
|
if not self.meta or not self.meta.latest_release:
|
||||||
meta = self.meta or ExtensionMeta()
|
meta = self.meta or ExtensionMeta()
|
||||||
meta.latest_release = release
|
meta.latest_release = release
|
||||||
@@ -464,13 +484,19 @@ class InstallableExtension(BaseModel):
|
|||||||
):
|
):
|
||||||
self.meta.latest_release = release
|
self.meta.latest_release = release
|
||||||
|
|
||||||
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
|
def _check_payment_link(self, release: ExtensionRelease | None):
|
||||||
if not pay_link or not self.meta or not self.meta.payments:
|
if not release:
|
||||||
return None
|
return
|
||||||
return next(
|
if not release.is_version_compatible:
|
||||||
(p for p in self.meta.payments if p.pay_link == pay_link),
|
return
|
||||||
None,
|
if not self.meta:
|
||||||
)
|
self.meta = ExtensionMeta()
|
||||||
|
if release.pay_link:
|
||||||
|
self.meta.has_paid_release = True
|
||||||
|
else:
|
||||||
|
self.meta.has_free_release = True
|
||||||
|
if release.paid_features:
|
||||||
|
self.meta.paid_features = release.paid_features
|
||||||
|
|
||||||
def _restore_payment_info(self):
|
def _restore_payment_info(self):
|
||||||
if (
|
if (
|
||||||
@@ -596,7 +622,7 @@ class InstallableExtension(BaseModel):
|
|||||||
(ee for ee in extension_list if ee.id == r.id), None
|
(ee for ee in extension_list if ee.id == r.id), None
|
||||||
)
|
)
|
||||||
if existing_ext and ext.meta:
|
if existing_ext and ext.meta:
|
||||||
existing_ext.check_latest_version(ext.meta.latest_release)
|
existing_ext.check_release_updates(ext.meta.latest_release)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
meta = ext.meta or ExtensionMeta()
|
meta = ext.meta or ExtensionMeta()
|
||||||
@@ -610,10 +636,10 @@ class InstallableExtension(BaseModel):
|
|||||||
(ee for ee in extension_list if ee.id == e.id), None
|
(ee for ee in extension_list if ee.id == e.id), None
|
||||||
)
|
)
|
||||||
if existing_ext:
|
if existing_ext:
|
||||||
existing_ext.check_latest_version(release)
|
existing_ext.check_release_updates(release)
|
||||||
continue
|
continue
|
||||||
ext = InstallableExtension.from_explicit_release(e)
|
ext = InstallableExtension.from_explicit_release(e)
|
||||||
ext.check_latest_version(release)
|
ext.check_release_updates(release)
|
||||||
meta = ext.meta or ExtensionMeta()
|
meta = ext.meta or ExtensionMeta()
|
||||||
meta.featured = ext.id in manifest.featured
|
meta.featured = ext.id in manifest.featured
|
||||||
ext.meta = meta
|
ext.meta = meta
|
||||||
|
|||||||
@@ -137,8 +137,40 @@
|
|||||||
@click="showExtensionDetails(extension.id, extension.details_link)"
|
@click="showExtensionDetails(extension.id, extension.details_link)"
|
||||||
v-text="extension.name"
|
v-text="extension.name"
|
||||||
></div>
|
></div>
|
||||||
<div>
|
<div style="justify-content: space-between; display: flex">
|
||||||
<lnbits-extension-rating :rating="0" />
|
<lnbits-extension-rating :rating="0" />
|
||||||
|
<q-btn-group size="xs" style="margin: 5px 0">
|
||||||
|
<q-btn
|
||||||
|
v-if="extension.hasFreeRelease"
|
||||||
|
color="green"
|
||||||
|
size="xs"
|
||||||
|
:label="$t('free')"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('extension_has_free_release')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="extension.hasPaidRelease || extension.paidFeatures"
|
||||||
|
color="primary"
|
||||||
|
size="xs"
|
||||||
|
:label="$t('paid')"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span
|
||||||
|
v-if="extension.hasPaidRelease"
|
||||||
|
v-text="$t('extension_has_paid_release')"
|
||||||
|
></span>
|
||||||
|
<br
|
||||||
|
v-if="extension.hasPaidRelease && extension.paidFeatures"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="extension.paidFeatures"
|
||||||
|
v-text="extension.paidFeatures"
|
||||||
|
></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-btn-group>
|
||||||
</div>
|
</div>
|
||||||
<div style="justify-content: space-between; display: flex">
|
<div style="justify-content: space-between; display: flex">
|
||||||
<q-toggle
|
<q-toggle
|
||||||
@@ -916,7 +948,6 @@
|
|||||||
:href="selectedExtensionDetails.repo"
|
:href="selectedExtensionDetails.repo"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="q-pr-xs"
|
|
||||||
><q-tooltip>repository</q-tooltip></q-btn
|
><q-tooltip>repository</q-tooltip></q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,9 +61,10 @@ async def api_install_extension(data: CreateExtension):
|
|||||||
data.ext_id, data.source_repo, data.archive, data.version
|
data.ext_id, data.source_repo, data.archive, data.version
|
||||||
)
|
)
|
||||||
if not release:
|
if not release:
|
||||||
raise HTTPException(
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Release not found")
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
|
|
||||||
)
|
if not release.is_version_compatible:
|
||||||
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
|
||||||
|
|
||||||
release.payment_hash = data.payment_hash
|
release.payment_hash = data.payment_hash
|
||||||
ext_meta = ExtensionMeta(installed_release=release)
|
ext_meta = ExtensionMeta(installed_release=release)
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
|||||||
if ext.meta and ext.meta.latest_release
|
if ext.meta and ext.meta.latest_release
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
|
||||||
|
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
|
||||||
|
"paidFeatures": ext.meta.paid_features if ext.meta else False,
|
||||||
"installedRelease": (
|
"installedRelease": (
|
||||||
dict(ext.meta.installed_release)
|
dict(ext.meta.installed_release)
|
||||||
if ext.meta and ext.meta.installed_release
|
if ext.meta and ext.meta.installed_release
|
||||||
|
|||||||
@@ -296,7 +296,9 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
|
|||||||
return {"paid": True, "preimage": payment.preimage}
|
return {"paid": True, "preimage": payment.preimage}
|
||||||
|
|
||||||
if payment.failed:
|
if payment.failed:
|
||||||
return {"paid": False, "status": "failed", "details": payment}
|
if wallet and wallet.id == payment.wallet_id:
|
||||||
|
return {"paid": False, "status": "failed", "details": payment}
|
||||||
|
return {"paid": False, "status": "failed"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = await payment.check_status()
|
status = await payment.check_status()
|
||||||
|
|||||||
+13
-236
@@ -6,7 +6,6 @@ from typing import Any, Literal
|
|||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from httpx import HTTPStatusError
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
@@ -26,34 +25,6 @@ from .base import (
|
|||||||
|
|
||||||
FiatMethod = Literal["checkout", "terminal"]
|
FiatMethod = Literal["checkout", "terminal"]
|
||||||
|
|
||||||
# ---- NEW: normalized subscription status type ----
|
|
||||||
StripeStatus = Literal[
|
|
||||||
"active",
|
|
||||||
"trialing",
|
|
||||||
"past_due",
|
|
||||||
"unpaid",
|
|
||||||
"canceled",
|
|
||||||
"incomplete",
|
|
||||||
"incomplete_expired",
|
|
||||||
"paused",
|
|
||||||
"not_found",
|
|
||||||
"pending",
|
|
||||||
"error",
|
|
||||||
"unknown",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Typed map to ensure mypy sees return values as StripeStatus (not plain str)
|
|
||||||
_STRIPE_STATUS_MAP: dict[str, StripeStatus] = {
|
|
||||||
"active": "active",
|
|
||||||
"trialing": "trialing",
|
|
||||||
"past_due": "past_due",
|
|
||||||
"unpaid": "unpaid",
|
|
||||||
"canceled": "canceled",
|
|
||||||
"incomplete": "incomplete",
|
|
||||||
"incomplete_expired": "incomplete_expired",
|
|
||||||
"paused": "paused",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StripeTerminalOptions(BaseModel):
|
class StripeTerminalOptions(BaseModel):
|
||||||
class Config:
|
class Config:
|
||||||
@@ -72,22 +43,6 @@ class StripeCheckoutOptions(BaseModel):
|
|||||||
line_item_name: str | None = None
|
line_item_name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
# === Direct-debit subscription options ===
|
|
||||||
class StripeRecurringOptions(BaseModel):
|
|
||||||
class Config:
|
|
||||||
extra = "ignore"
|
|
||||||
|
|
||||||
price_id: str | None = None
|
|
||||||
price_lookup_key: str | None = None
|
|
||||||
payment_method_types: list[str] = Field(default_factory=lambda: ["bacs_debit"])
|
|
||||||
|
|
||||||
success_url: str | None = None
|
|
||||||
cancel_url: str | None = None
|
|
||||||
metadata: dict[str, str] = Field(default_factory=dict)
|
|
||||||
customer_email: str | None = None
|
|
||||||
trial_days: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class StripeCreateInvoiceOptions(BaseModel):
|
class StripeCreateInvoiceOptions(BaseModel):
|
||||||
class Config:
|
class Config:
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
@@ -95,7 +50,6 @@ class StripeCreateInvoiceOptions(BaseModel):
|
|||||||
fiat_method: FiatMethod = "checkout"
|
fiat_method: FiatMethod = "checkout"
|
||||||
terminal: StripeTerminalOptions | None = None
|
terminal: StripeTerminalOptions | None = None
|
||||||
checkout: StripeCheckoutOptions | None = None
|
checkout: StripeCheckoutOptions | None = None
|
||||||
recurring: StripeRecurringOptions | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class StripeWallet(FiatProvider):
|
class StripeWallet(FiatProvider):
|
||||||
@@ -135,10 +89,12 @@ class StripeWallet(FiatProvider):
|
|||||||
r = await self.client.get(url="/v1/balance", timeout=15)
|
r = await self.client.get(url="/v1/balance", timeout=15)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
available = data.get("available") or []
|
available = data.get("available") or []
|
||||||
available_balance = 0
|
available_balance = 0
|
||||||
if available and isinstance(available, list):
|
if available and isinstance(available, list):
|
||||||
available_balance = int(available[0].get("amount", 0))
|
available_balance = int(available[0].get("amount", 0))
|
||||||
|
|
||||||
return FiatStatusResponse(balance=available_balance)
|
return FiatStatusResponse(balance=available_balance)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return FiatStatusResponse("Server error: 'invalid json response'", 0)
|
return FiatStatusResponse("Server error: 'invalid json response'", 0)
|
||||||
@@ -160,11 +116,6 @@ class StripeWallet(FiatProvider):
|
|||||||
if not opts:
|
if not opts:
|
||||||
return FiatInvoiceResponse(ok=False, error_message="Invalid Stripe options")
|
return FiatInvoiceResponse(ok=False, error_message="Invalid Stripe options")
|
||||||
|
|
||||||
if opts.recurring is not None:
|
|
||||||
return await self._create_subscription_checkout_session(
|
|
||||||
payment_hash, memo, opts
|
|
||||||
)
|
|
||||||
|
|
||||||
if opts.fiat_method == "checkout":
|
if opts.fiat_method == "checkout":
|
||||||
return await self._create_checkout_invoice(
|
return await self._create_checkout_invoice(
|
||||||
amount_cents, currency, payment_hash, memo, opts
|
amount_cents, currency, payment_hash, memo, opts
|
||||||
@@ -219,7 +170,6 @@ class StripeWallet(FiatProvider):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
# ---------- One-off Checkout ----------
|
|
||||||
async def _create_checkout_invoice(
|
async def _create_checkout_invoice(
|
||||||
self,
|
self,
|
||||||
amount_cents: int,
|
amount_cents: int,
|
||||||
@@ -273,7 +223,6 @@ class StripeWallet(FiatProvider):
|
|||||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------- Terminal ----------
|
|
||||||
async def _create_terminal_invoice(
|
async def _create_terminal_invoice(
|
||||||
self,
|
self,
|
||||||
amount_cents: int,
|
amount_cents: int,
|
||||||
@@ -316,189 +265,8 @@ class StripeWallet(FiatProvider):
|
|||||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------- Subscription Checkout ----------
|
|
||||||
async def _create_subscription_checkout_session(
|
|
||||||
self,
|
|
||||||
payment_hash: str,
|
|
||||||
memo: str | None,
|
|
||||||
opts: StripeCreateInvoiceOptions,
|
|
||||||
) -> FiatInvoiceResponse:
|
|
||||||
rc = opts.recurring or StripeRecurringOptions()
|
|
||||||
try:
|
|
||||||
price_id = rc.price_id
|
|
||||||
if not price_id and rc.price_lookup_key:
|
|
||||||
price_id = await self._get_price_id_by_lookup_key(rc.price_lookup_key)
|
|
||||||
if not price_id:
|
|
||||||
return FiatInvoiceResponse(
|
|
||||||
ok=False,
|
|
||||||
error_message="Stripe: missing price_id or price_lookup_key",
|
|
||||||
)
|
|
||||||
|
|
||||||
success_url = (
|
|
||||||
rc.success_url
|
|
||||||
or (opts.checkout.success_url if opts.checkout else None)
|
|
||||||
or settings.stripe_payment_success_url
|
|
||||||
or "https://lnbits.com"
|
|
||||||
)
|
|
||||||
cancel_url = rc.cancel_url or success_url
|
|
||||||
|
|
||||||
form_data: list[tuple[str, str]] = [
|
|
||||||
("mode", "subscription"),
|
|
||||||
("success_url", success_url),
|
|
||||||
("cancel_url", cancel_url),
|
|
||||||
("payment_method_collection", "always"),
|
|
||||||
("metadata[payment_hash]", payment_hash),
|
|
||||||
("line_items[0][price]", price_id),
|
|
||||||
("line_items[0][quantity]", "1"),
|
|
||||||
]
|
|
||||||
|
|
||||||
if rc.trial_days:
|
|
||||||
form_data.append(
|
|
||||||
("subscription_data[trial_period_days]", str(rc.trial_days))
|
|
||||||
)
|
|
||||||
|
|
||||||
if rc.customer_email:
|
|
||||||
form_data.append(("customer_email", rc.customer_email))
|
|
||||||
|
|
||||||
form_data += self._encode_metadata("metadata", rc.metadata)
|
|
||||||
|
|
||||||
r = await self.client.post(
|
|
||||||
"/v1/checkout/sessions",
|
|
||||||
headers=self._build_headers_form(),
|
|
||||||
content=urlencode(form_data),
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
session_id, url = data.get("id"), data.get("url")
|
|
||||||
if not session_id or not url:
|
|
||||||
return FiatInvoiceResponse(
|
|
||||||
ok=False,
|
|
||||||
error_message="Server error: missing id or url (subscription)",
|
|
||||||
)
|
|
||||||
return FiatInvoiceResponse(
|
|
||||||
ok=True, checking_id=session_id, payment_request=url
|
|
||||||
)
|
|
||||||
|
|
||||||
except HTTPStatusError as e:
|
|
||||||
body = e.response.text if e.response is not None else "<no body>"
|
|
||||||
logger.warning(f"Stripe subscription 400: {body}")
|
|
||||||
return FiatInvoiceResponse(ok=False, error_message=body)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return FiatInvoiceResponse(
|
|
||||||
ok=False, error_message="Server error: invalid json response"
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning(exc)
|
|
||||||
return FiatInvoiceResponse(
|
|
||||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------- Subscription status helpers (NEW) ----------
|
|
||||||
async def get_subscription_status(self, sub_or_session_id: str) -> StripeStatus:
|
|
||||||
"""
|
|
||||||
Accepts either a 'sub_...' or 'cs_...' id. If it's a 'cs_...',
|
|
||||||
returns 'pending' until the subscription exists; once it does,
|
|
||||||
returns the mapped subscription status.
|
|
||||||
"""
|
|
||||||
sid = self._normalize_stripe_id(sub_or_session_id)
|
|
||||||
try:
|
|
||||||
if sid.startswith("sub_"):
|
|
||||||
r = await self.client.get(f"/v1/subscriptions/{sid}")
|
|
||||||
if r.status_code == 404:
|
|
||||||
return "not_found"
|
|
||||||
r.raise_for_status()
|
|
||||||
return self._status_from_subscription(r.json())
|
|
||||||
|
|
||||||
if sid.startswith("cs_"):
|
|
||||||
r = await self.client.get(f"/v1/checkout/sessions/{sid}")
|
|
||||||
if r.status_code == 404:
|
|
||||||
return "not_found"
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
subscription_id = data.get("subscription")
|
|
||||||
if not subscription_id:
|
|
||||||
return "pending"
|
|
||||||
r2 = await self.client.get(f"/v1/subscriptions/{subscription_id}")
|
|
||||||
if r2.status_code == 404:
|
|
||||||
return "not_found"
|
|
||||||
r2.raise_for_status()
|
|
||||||
return self._status_from_subscription(r2.json())
|
|
||||||
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
except httpx.HTTPStatusError:
|
|
||||||
return "error"
|
|
||||||
except Exception:
|
|
||||||
return "error"
|
|
||||||
|
|
||||||
async def get_subscription_status_and_promote(
|
|
||||||
self, sub_or_session_id: str
|
|
||||||
) -> tuple[StripeStatus, str]:
|
|
||||||
"""
|
|
||||||
Returns (status, effective_id). If given a 'cs_...' and the Checkout
|
|
||||||
Session has created a subscription, returns the subscription status
|
|
||||||
AND the promoted 'sub_...' id so you can persist it. If given a 'sub_...',
|
|
||||||
returns its status and the same id.
|
|
||||||
"""
|
|
||||||
sid = self._normalize_stripe_id(sub_or_session_id)
|
|
||||||
try:
|
|
||||||
if sid.startswith("sub_"):
|
|
||||||
r = await self.client.get(f"/v1/subscriptions/{sid}")
|
|
||||||
if r.status_code == 404:
|
|
||||||
return ("not_found", sid)
|
|
||||||
r.raise_for_status()
|
|
||||||
return (self._status_from_subscription(r.json()), sid)
|
|
||||||
|
|
||||||
if sid.startswith("cs_"):
|
|
||||||
r = await self.client.get(f"/v1/checkout/sessions/{sid}")
|
|
||||||
if r.status_code == 404:
|
|
||||||
return ("not_found", sid)
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
subscription_id = data.get("subscription")
|
|
||||||
if not subscription_id:
|
|
||||||
return ("pending", sid)
|
|
||||||
|
|
||||||
# Promote to the subscription id
|
|
||||||
r2 = await self.client.get(f"/v1/subscriptions/{subscription_id}")
|
|
||||||
if r2.status_code == 404:
|
|
||||||
return ("not_found", subscription_id)
|
|
||||||
r2.raise_for_status()
|
|
||||||
return (self._status_from_subscription(r2.json()), subscription_id)
|
|
||||||
|
|
||||||
return ("unknown", sid)
|
|
||||||
|
|
||||||
except httpx.HTTPStatusError:
|
|
||||||
return ("error", sid)
|
|
||||||
except Exception:
|
|
||||||
return ("error", sid)
|
|
||||||
|
|
||||||
def _status_from_subscription(self, sub: dict) -> StripeStatus:
|
|
||||||
status = (sub or {}).get("status")
|
|
||||||
if not status:
|
|
||||||
return "unknown"
|
|
||||||
return _STRIPE_STATUS_MAP.get(str(status).lower().strip(), "unknown")
|
|
||||||
|
|
||||||
# ---------- Helpers ----------
|
|
||||||
async def _get_price_id_by_lookup_key(self, lookup_key: str) -> str | None:
|
|
||||||
params = {"active": "true", "expand[]": "data.product", "limit": "1"}
|
|
||||||
qs = urlencode(params) + f"&lookup_keys[]={lookup_key}"
|
|
||||||
r = await self.client.get(f"/v1/prices?{qs}")
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
items = (data or {}).get("data") or []
|
|
||||||
if not items:
|
|
||||||
return None
|
|
||||||
return items[0].get("id")
|
|
||||||
|
|
||||||
async def list_prices_for_product(self, product_id: str) -> list[dict]:
|
|
||||||
qs = urlencode({"product": product_id, "active": "true", "limit": "100"})
|
|
||||||
r = await self.client.get(f"/v1/prices?{qs}")
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
return (data or {}).get("data") or []
|
|
||||||
|
|
||||||
def _normalize_stripe_id(self, checking_id: str) -> str:
|
def _normalize_stripe_id(self, checking_id: str) -> str:
|
||||||
|
"""Remove our internal prefix so Stripe sees a real id."""
|
||||||
return (
|
return (
|
||||||
checking_id.replace("fiat_stripe_", "", 1)
|
checking_id.replace("fiat_stripe_", "", 1)
|
||||||
if checking_id.startswith("fiat_stripe_")
|
if checking_id.startswith("fiat_stripe_")
|
||||||
@@ -506,9 +274,11 @@ class StripeWallet(FiatProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _status_from_checkout_session(self, data: dict) -> FiatPaymentStatus:
|
def _status_from_checkout_session(self, data: dict) -> FiatPaymentStatus:
|
||||||
|
"""Map a Checkout Session to LNbits fiat status."""
|
||||||
if data.get("payment_status") == "paid":
|
if data.get("payment_status") == "paid":
|
||||||
return FiatPaymentSuccessStatus()
|
return FiatPaymentSuccessStatus()
|
||||||
|
|
||||||
|
# Consider an expired session a fail (existing 24h rule).
|
||||||
expires_at = data.get("expires_at")
|
expires_at = data.get("expires_at")
|
||||||
_24h_ago = datetime.now(timezone.utc) - timedelta(hours=24)
|
_24h_ago = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||||
if expires_at and float(expires_at) < _24h_ago.timestamp():
|
if expires_at and float(expires_at) < _24h_ago.timestamp():
|
||||||
@@ -517,18 +287,25 @@ class StripeWallet(FiatProvider):
|
|||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
def _status_from_payment_intent(self, pi: dict) -> FiatPaymentStatus:
|
def _status_from_payment_intent(self, pi: dict) -> FiatPaymentStatus:
|
||||||
|
"""Map a PaymentIntent to LNbits fiat status (card_present friendly)."""
|
||||||
status = pi.get("status")
|
status = pi.get("status")
|
||||||
|
|
||||||
if status == "succeeded":
|
if status == "succeeded":
|
||||||
return FiatPaymentSuccessStatus()
|
return FiatPaymentSuccessStatus()
|
||||||
|
|
||||||
if status in ("canceled", "payment_failed"):
|
if status in ("canceled", "payment_failed"):
|
||||||
return FiatPaymentFailedStatus()
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
if status == "requires_payment_method":
|
if status == "requires_payment_method":
|
||||||
if pi.get("last_payment_error"):
|
if pi.get("last_payment_error"):
|
||||||
return FiatPaymentFailedStatus()
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
now_ts = datetime.now(timezone.utc).timestamp()
|
now_ts = datetime.now(timezone.utc).timestamp()
|
||||||
created_ts = float(pi.get("created") or now_ts)
|
created_ts = float(pi.get("created") or now_ts)
|
||||||
if (now_ts - created_ts) > 300:
|
is_stale = (now_ts - created_ts) > 300
|
||||||
|
if is_stale:
|
||||||
return FiatPaymentFailedStatus()
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
def _build_headers_form(self) -> dict[str, str]:
|
def _build_headers_form(self) -> dict[str, str]:
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -168,6 +168,8 @@ window.localisation.en = {
|
|||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
|
extension_has_free_release: 'Has free releases',
|
||||||
|
extension_has_paid_release: 'Has paid releases',
|
||||||
extension_depends_on: 'Depends on:',
|
extension_depends_on: 'Depends on:',
|
||||||
extension_rating_soon: 'Ratings coming soon',
|
extension_rating_soon: 'Ratings coming soon',
|
||||||
extension_installed_version: 'Installed version',
|
extension_installed_version: 'Installed version',
|
||||||
@@ -663,5 +665,7 @@ window.localisation.en = {
|
|||||||
callback_success_url_hint:
|
callback_success_url_hint:
|
||||||
'The user will be redirected to this URL after the payment is successful',
|
'The user will be redirected to this URL after the payment is successful',
|
||||||
connected: 'Connected',
|
connected: 'Connected',
|
||||||
not_connected: 'Not Connected'
|
not_connected: 'Not Connected',
|
||||||
|
free: 'Free',
|
||||||
|
paid: 'Paid'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -468,7 +468,10 @@ window.AdminPageLogic = {
|
|||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return moment.utc(date * 1000).fromNow()
|
return moment
|
||||||
|
.utc(date * 1000)
|
||||||
|
.local()
|
||||||
|
.fromNow()
|
||||||
},
|
},
|
||||||
sendTestEmail() {
|
sendTestEmail() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
|||||||
@@ -267,10 +267,10 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
||||||
obj.dateFrom = moment.utc(data.created_at).fromNow()
|
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
||||||
|
|
||||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
||||||
obj.expirydateFrom = moment.utc(obj.expiry).fromNow()
|
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
||||||
obj.msat = obj.amount
|
obj.msat = obj.amount
|
||||||
obj.sat = obj.msat / 1000
|
obj.sat = obj.msat / 1000
|
||||||
obj.tag = obj.extra?.tag
|
obj.tag = obj.extra?.tag
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ function confettiStars() {
|
|||||||
setTimeout(shoot, 200)
|
setTimeout(shoot, 200)
|
||||||
}
|
}
|
||||||
!(function (t, e) {
|
!(function (t, e) {
|
||||||
!(function t(e, n, a, i) {
|
;(!(function t(e, n, a, i) {
|
||||||
var o = !!(
|
var o = !!(
|
||||||
e.Worker &&
|
e.Worker &&
|
||||||
e.Blob &&
|
e.Blob &&
|
||||||
@@ -248,12 +248,12 @@ function confettiStars() {
|
|||||||
function e(e, n) {
|
function e(e, n) {
|
||||||
t.postMessage({options: e || {}, callback: n})
|
t.postMessage({options: e || {}, callback: n})
|
||||||
}
|
}
|
||||||
;(t.init = function (e) {
|
;((t.init = function (e) {
|
||||||
var n = e.transferControlToOffscreen()
|
var n = e.transferControlToOffscreen()
|
||||||
t.postMessage({canvas: n}, [n])
|
t.postMessage({canvas: n}, [n])
|
||||||
}),
|
}),
|
||||||
(t.fire = function (n, a, i) {
|
(t.fire = function (n, a, i) {
|
||||||
if (g) return e(n, null), g
|
if (g) return (e(n, null), g)
|
||||||
var o = Math.random().toString(36).slice(2)
|
var o = Math.random().toString(36).slice(2)
|
||||||
return (g = l(function (a) {
|
return (g = l(function (a) {
|
||||||
function r(e) {
|
function r(e) {
|
||||||
@@ -264,15 +264,15 @@ function confettiStars() {
|
|||||||
i(),
|
i(),
|
||||||
a())
|
a())
|
||||||
}
|
}
|
||||||
t.addEventListener('message', r),
|
;(t.addEventListener('message', r),
|
||||||
e(n, o),
|
e(n, o),
|
||||||
(m[o] = r.bind(null, {data: {callback: o}}))
|
(m[o] = r.bind(null, {data: {callback: o}})))
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
(t.reset = function () {
|
(t.reset = function () {
|
||||||
for (var e in (t.postMessage({reset: !0}), m))
|
for (var e in (t.postMessage({reset: !0}), m))
|
||||||
m[e](), delete m[e]
|
(m[e](), delete m[e])
|
||||||
})
|
}))
|
||||||
})(h)
|
})(h)
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
@@ -328,12 +328,12 @@ function confettiStars() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
function k(t) {
|
function k(t) {
|
||||||
;(t.width = document.documentElement.clientWidth),
|
;((t.width = document.documentElement.clientWidth),
|
||||||
(t.height = document.documentElement.clientHeight)
|
(t.height = document.documentElement.clientHeight))
|
||||||
}
|
}
|
||||||
function I(t) {
|
function I(t) {
|
||||||
var e = t.getBoundingClientRect()
|
var e = t.getBoundingClientRect()
|
||||||
;(t.width = e.width), (t.height = e.height)
|
;((t.width = e.width), (t.height = e.height))
|
||||||
}
|
}
|
||||||
function T(t, e, n, o, r) {
|
function T(t, e, n, o, r) {
|
||||||
var c,
|
var c,
|
||||||
@@ -342,10 +342,10 @@ function confettiStars() {
|
|||||||
d = t.getContext('2d'),
|
d = t.getContext('2d'),
|
||||||
f = l(function (e) {
|
f = l(function (e) {
|
||||||
function l() {
|
function l() {
|
||||||
;(c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e()
|
;((c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e())
|
||||||
}
|
}
|
||||||
;(c = b.frame(function e() {
|
;((c = b.frame(function e() {
|
||||||
!a ||
|
;(!a ||
|
||||||
(o.width === i.width && o.height === i.height) ||
|
(o.width === i.width && o.height === i.height) ||
|
||||||
((o.width = t.width = i.width), (o.height = t.height = i.height)),
|
((o.width = t.width = i.width), (o.height = t.height = i.height)),
|
||||||
o.width ||
|
o.width ||
|
||||||
@@ -354,7 +354,7 @@ function confettiStars() {
|
|||||||
d.clearRect(0, 0, o.width, o.height),
|
d.clearRect(0, 0, o.width, o.height),
|
||||||
(u = u.filter(function (t) {
|
(u = u.filter(function (t) {
|
||||||
return (function (t, e) {
|
return (function (t, e) {
|
||||||
;(e.x += Math.cos(e.angle2D) * e.velocity + e.drift),
|
;((e.x += Math.cos(e.angle2D) * e.velocity + e.drift),
|
||||||
(e.y += Math.sin(e.angle2D) * e.velocity + e.gravity),
|
(e.y += Math.sin(e.angle2D) * e.velocity + e.gravity),
|
||||||
(e.wobble += 0.1),
|
(e.wobble += 0.1),
|
||||||
(e.velocity *= e.decay),
|
(e.velocity *= e.decay),
|
||||||
@@ -363,7 +363,7 @@ function confettiStars() {
|
|||||||
(e.tiltCos = Math.cos(e.tiltAngle)),
|
(e.tiltCos = Math.cos(e.tiltAngle)),
|
||||||
(e.random = Math.random() + 5),
|
(e.random = Math.random() + 5),
|
||||||
(e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)),
|
(e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)),
|
||||||
(e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble))
|
(e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble)))
|
||||||
var n = e.tick++ / e.totalTicks,
|
var n = e.tick++ / e.totalTicks,
|
||||||
a = e.x + e.random * e.tiltCos,
|
a = e.x + e.random * e.tiltCos,
|
||||||
i = e.y + e.random * e.tiltSin,
|
i = e.y + e.random * e.tiltSin,
|
||||||
@@ -393,12 +393,12 @@ function confettiStars() {
|
|||||||
2 * Math.PI
|
2 * Math.PI
|
||||||
)
|
)
|
||||||
: (function (t, e, n, a, i, o, r, l, c) {
|
: (function (t, e, n, a, i, o, r, l, c) {
|
||||||
t.save(),
|
;(t.save(),
|
||||||
t.translate(e, n),
|
t.translate(e, n),
|
||||||
t.rotate(o),
|
t.rotate(o),
|
||||||
t.scale(a, i),
|
t.scale(a, i),
|
||||||
t.arc(0, 0, 1, r, l, c),
|
t.arc(0, 0, 1, r, l, c),
|
||||||
t.restore()
|
t.restore())
|
||||||
})(
|
})(
|
||||||
t,
|
t,
|
||||||
e.x,
|
e.x,
|
||||||
@@ -420,18 +420,18 @@ function confettiStars() {
|
|||||||
})(d, t)
|
})(d, t)
|
||||||
})).length
|
})).length
|
||||||
? (c = b.frame(e))
|
? (c = b.frame(e))
|
||||||
: l()
|
: l())
|
||||||
})),
|
})),
|
||||||
(s = l)
|
(s = l))
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
addFettis: function (t) {
|
addFettis: function (t) {
|
||||||
return (u = u.concat(t)), f
|
return ((u = u.concat(t)), f)
|
||||||
},
|
},
|
||||||
canvas: t,
|
canvas: t,
|
||||||
promise: f,
|
promise: f,
|
||||||
reset: function () {
|
reset: function () {
|
||||||
c && b.cancel(c), s && s()
|
;(c && b.cancel(c), s && s())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,7 +466,7 @@ function confettiStars() {
|
|||||||
k = p(e, 'scalar'),
|
k = p(e, 'scalar'),
|
||||||
I = (function (t) {
|
I = (function (t) {
|
||||||
var e = p(t, 'origin', Object)
|
var e = p(t, 'origin', Object)
|
||||||
return (e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e
|
return ((e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e)
|
||||||
})(e),
|
})(e),
|
||||||
E = d,
|
E = d,
|
||||||
S = [],
|
S = [],
|
||||||
@@ -531,7 +531,7 @@ function confettiStars() {
|
|||||||
return l(function (t) {
|
return l(function (t) {
|
||||||
t()
|
t()
|
||||||
})
|
})
|
||||||
i && a
|
;(i && a
|
||||||
? (t = a.canvas)
|
? (t = a.canvas)
|
||||||
: i &&
|
: i &&
|
||||||
!t &&
|
!t &&
|
||||||
@@ -547,7 +547,7 @@ function confettiStars() {
|
|||||||
)
|
)
|
||||||
})(g)),
|
})(g)),
|
||||||
document.body.appendChild(t)),
|
document.body.appendChild(t)),
|
||||||
r && !d && u(t)
|
r && !d && u(t))
|
||||||
var m = {width: t.width, height: t.height}
|
var m = {width: t.width, height: t.height}
|
||||||
function b() {
|
function b() {
|
||||||
if (s) {
|
if (s) {
|
||||||
@@ -564,9 +564,9 @@ function confettiStars() {
|
|||||||
m.width = m.height = null
|
m.width = m.height = null
|
||||||
}
|
}
|
||||||
function v() {
|
function v() {
|
||||||
;(a = null),
|
;((a = null),
|
||||||
r && e.removeEventListener('resize', b),
|
r && e.removeEventListener('resize', b),
|
||||||
i && t && (document.body.removeChild(t), (t = null), (d = !1))
|
i && t && (document.body.removeChild(t), (t = null), (d = !1)))
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
s && !d && s.init(t),
|
s && !d && s.init(t),
|
||||||
@@ -578,12 +578,13 @@ function confettiStars() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
(g.reset = function () {
|
(g.reset = function () {
|
||||||
s && s.reset(), a && a.reset()
|
;(s && s.reset(), a && a.reset())
|
||||||
}),
|
}),
|
||||||
g
|
g
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
;(n.exports = E(null, {useWorker: !0, resize: !0})), (n.exports.create = E)
|
;((n.exports = E(null, {useWorker: !0, resize: !0})),
|
||||||
|
(n.exports.create = E))
|
||||||
})(
|
})(
|
||||||
(function () {
|
(function () {
|
||||||
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
|
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
|
||||||
@@ -591,5 +592,5 @@ function confettiStars() {
|
|||||||
e,
|
e,
|
||||||
!1
|
!1
|
||||||
),
|
),
|
||||||
(t.confetti = e.exports)
|
(t.confetti = e.exports))
|
||||||
})(window, {})
|
})(window, {})
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ window.app.component('lnbits-date', {
|
|||||||
return LNbits.utils.formatDate(this.ts)
|
return LNbits.utils.formatDate(this.ts)
|
||||||
},
|
},
|
||||||
dateFrom() {
|
dateFrom() {
|
||||||
return moment.utc(this.date).fromNow()
|
return moment.utc(this.date).local().fromNow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ window.PaymentsPageLogic = {
|
|||||||
if (p.extra && p.extra.tag) {
|
if (p.extra && p.extra.tag) {
|
||||||
p.tag = p.extra.tag
|
p.tag = p.extra.tag
|
||||||
}
|
}
|
||||||
p.timeFrom = moment.utc(p.created_at).fromNow()
|
p.timeFrom = moment.utc(p.created_at).local().fromNow()
|
||||||
p.outgoing = p.amount < 0
|
p.outgoing = p.amount < 0
|
||||||
p.amount =
|
p.amount =
|
||||||
new Intl.NumberFormat(window.LOCALE).format(p.amount / 1000) +
|
new Intl.NumberFormat(window.LOCALE).format(p.amount / 1000) +
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ window.WalletPageLogic = {
|
|||||||
methods: {
|
methods: {
|
||||||
dateFromNow(unix) {
|
dateFromNow(unix) {
|
||||||
const date = new Date(unix * 1000)
|
const date = new Date(unix * 1000)
|
||||||
return moment.utc(date).fromNow()
|
return moment.utc(date).local().fromNow()
|
||||||
},
|
},
|
||||||
formatFiatAmount(amount, currency) {
|
formatFiatAmount(amount, currency) {
|
||||||
this.update.currency = currency
|
this.update.currency = currency
|
||||||
@@ -476,8 +476,14 @@ window.WalletPageLogic = {
|
|||||||
createdDate,
|
createdDate,
|
||||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
)
|
)
|
||||||
cleanInvoice.expireDateFrom = moment.utc(expireDate).fromNow()
|
cleanInvoice.expireDateFrom = moment
|
||||||
cleanInvoice.createdDateFrom = moment.utc(createdDate).fromNow()
|
.utc(expireDate)
|
||||||
|
.local()
|
||||||
|
.fromNow()
|
||||||
|
cleanInvoice.createdDateFrom = moment
|
||||||
|
.utc(createdDate)
|
||||||
|
.local()
|
||||||
|
.fromNow()
|
||||||
|
|
||||||
cleanInvoice.expired = false // TODO
|
cleanInvoice.expired = false // TODO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -649,8 +649,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showButtons"
|
v-if="showButtons"
|
||||||
class="qrcode__buttons row q-gutter-x-sm"
|
class="qrcode__buttons row q-gutter-x-sm items-center justify-end no-wrap full-width"
|
||||||
style="justify-content: flex-end"
|
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="nfc && nfcSupported"
|
v-if="nfc && nfcSupported"
|
||||||
|
|||||||
Reference in New Issue
Block a user