diff --git a/lnbits/core/services/fiat_providers.py b/lnbits/core/services/fiat_providers.py
index b0fa5e257..eb52ff529 100644
--- a/lnbits/core/services/fiat_providers.py
+++ b/lnbits/core/services/fiat_providers.py
@@ -200,6 +200,47 @@ def check_square_signature(
raise ValueError("Square signature verification failed.")
+def check_revolut_signature(
+ payload: bytes,
+ sig_header: str | None,
+ timestamp_header: str | None,
+ secret: str | None,
+ tolerance_seconds=300,
+):
+ if not sig_header:
+ logger.warning("Revolut signature header is missing.")
+ raise ValueError("Revolut signature header is missing.")
+
+ if not timestamp_header:
+ logger.warning("Revolut timestamp header is missing.")
+ raise ValueError("Revolut timestamp header is missing.")
+
+ if not secret:
+ logger.warning("Revolut webhook signing secret is not set.")
+ raise ValueError("Revolut webhook cannot be verified.")
+
+ timestamp = int(timestamp_header)
+ if abs(time.time() - timestamp) > tolerance_seconds:
+ logger.warning("Timestamp outside tolerance.")
+ raise ValueError("Timestamp outside tolerance." f"Timestamp: {timestamp}")
+
+ candidates = [
+ payload,
+ f"{timestamp}.{payload.decode()}".encode(),
+ timestamp_header.encode() + b"." + payload,
+ ]
+ signatures = []
+ for candidate in candidates:
+ digest = hmac.new(
+ key=secret.encode(), msg=candidate, digestmod=hashlib.sha256
+ ).digest()
+ signatures.extend([digest.hex(), b64encode(digest).decode()])
+
+ if not any(hmac.compare_digest(expected, sig_header) for expected in signatures):
+ logger.warning("Revolut signature verification failed.")
+ raise ValueError("Revolut signature verification failed.")
+
+
async def test_connection(provider: str) -> SimpleStatus:
"""
Test the connection to Stripe by checking if the API key is valid.
diff --git a/lnbits/core/views/callback_api.py b/lnbits/core/views/callback_api.py
index 6ee9b350e..be6716399 100644
--- a/lnbits/core/views/callback_api.py
+++ b/lnbits/core/views/callback_api.py
@@ -13,6 +13,7 @@ from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import (
check_fiat_status,
+ check_revolut_signature,
check_square_signature,
check_stripe_signature,
verify_paypal_webhook,
@@ -21,6 +22,7 @@ from lnbits.core.services.payments import create_fiat_invoice
from lnbits.db import Filter, Filters
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
+from lnbits.fiat.revolut import RevolutWallet
from lnbits.fiat.square import SquareWallet
from lnbits.settings import settings
@@ -74,6 +76,24 @@ async def api_generic_webhook_handler(
message=f"Callback received successfully from '{provider_name}'.",
)
+ if provider_name.lower() == "revolut":
+ payload = await request.body()
+ sig_header = request.headers.get("Revolut-Signature")
+ timestamp_header = request.headers.get("Revolut-Request-Timestamp")
+ check_revolut_signature(
+ payload,
+ sig_header,
+ timestamp_header,
+ settings.revolut_webhook_signing_secret,
+ )
+ event = await request.json()
+ await handle_revolut_event(event)
+
+ return SimpleStatus(
+ success=True,
+ message=f"Callback received successfully from '{provider_name}'.",
+ )
+
return SimpleStatus(
success=False,
message=f"Unknown fiat provider '{provider_name}'.",
@@ -322,6 +342,104 @@ async def handle_square_event(event: dict):
logger.warning(f"Unhandled Square event type: '{event_type}'.")
+async def handle_revolut_event(event: dict):
+ event_type = event.get("event", "")
+ order_id = event.get("order_id")
+ logger.info(f"Handling Revolut event: '{event_type}'. Order ID: '{order_id}'.")
+
+ if event_type in ["ORDER_AUTHORISED", "ORDER_COMPLETED"]:
+ if not order_id:
+ logger.warning("Revolut event missing order_id.")
+ return
+
+ payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
+ if not payment:
+ logger.warning(f"No payment found for Revolut order: '{order_id}'.")
+ return
+
+ await check_fiat_status(payment)
+ return
+
+ if event_type == "SUBSCRIPTION_INITIATED":
+ await _handle_revolut_subscription_initiated(event)
+ return
+
+ if event_type in ["SUBSCRIPTION_CANCELLED", "SUBSCRIPTION_FINISHED", "SUBSCRIPTION_OVERDUE"]:
+ logger.info(f"Revolut subscription lifecycle event received: '{event_type}'.")
+ return
+
+ logger.warning(f"Unhandled Revolut event type: '{event_type}'.")
+
+
+async def _handle_revolut_subscription_initiated(event: dict):
+ subscription_id = event.get("subscription_id")
+ if not subscription_id:
+ logger.warning("Revolut subscription event missing subscription_id.")
+ return
+
+ fiat_provider = await get_fiat_provider("revolut")
+ if not isinstance(fiat_provider, RevolutWallet):
+ logger.warning("Revolut fiat provider is not configured.")
+ return
+
+ subscription = await fiat_provider.get_subscription(subscription_id)
+ reference = fiat_provider.deserialize_subscription_reference(
+ subscription.get("external_reference")
+ )
+ if not reference:
+ logger.warning("Revolut subscription event missing LNbits metadata.")
+ return
+
+ cycle_id = subscription.get("current_cycle_id")
+ if not cycle_id:
+ logger.warning("Revolut subscription missing current_cycle_id.")
+ return
+
+ cycle = await fiat_provider.get_subscription_cycle(subscription_id, cycle_id)
+ order_id = cycle.get("order_id")
+ if not order_id:
+ logger.warning("Revolut subscription cycle missing order_id.")
+ return
+
+ existing_payment = await get_standalone_payment(f"fiat_revolut_order_{order_id}")
+ if existing_payment:
+ if existing_payment.external_id != subscription_id:
+ existing_payment.external_id = subscription_id
+ await update_payment(existing_payment)
+ await check_fiat_status(existing_payment)
+ return
+
+ order = await fiat_provider.get_order(order_id)
+ amount_minor = order.get("amount")
+ currency = (order.get("currency") or "").upper()
+ if amount_minor is None or not currency:
+ raise ValueError("Revolut subscription order missing amount or currency.")
+
+ extra = {
+ **(reference.extra or {}),
+ "subscription_request_id": reference.subscription_request_id,
+ "fiat_method": "subscription",
+ "tag": reference.tag,
+ "subscription": {
+ "checking_id": f"order_{order_id}",
+ "payment_request": order.get("checkout_url") or "",
+ },
+ }
+ lnbits_payment = await create_fiat_invoice(
+ wallet_id=reference.wallet_id,
+ invoice_data=CreateInvoice(
+ unit=currency,
+ amount=amount_minor / 100,
+ memo=reference.memo or "",
+ extra=extra,
+ fiat_provider="revolut",
+ external_id=subscription_id,
+ ),
+ )
+
+ await check_fiat_status(lnbits_payment)
+
+
async def _handle_square_payment_event(event: dict):
payment = _square_extract_payment(event)
payment_options = _deserialize_square_metadata(_square_payment_note(payment))
diff --git a/lnbits/fiat/__init__.py b/lnbits/fiat/__init__.py
index 362cd8939..5d46256fd 100644
--- a/lnbits/fiat/__init__.py
+++ b/lnbits/fiat/__init__.py
@@ -9,6 +9,7 @@ from lnbits.fiat.base import FiatProvider
from lnbits.settings import settings
from .paypal import PayPalWallet
+from .revolut import RevolutWallet
from .square import SquareWallet
from .stripe import StripeWallet
@@ -19,6 +20,7 @@ class FiatProviderType(Enum):
stripe = "StripeWallet"
paypal = "PayPalWallet"
square = "SquareWallet"
+ revolut = "RevolutWallet"
async def get_fiat_provider(name: str) -> FiatProvider | None:
@@ -54,6 +56,7 @@ fiat_providers: dict[str, FiatProvider] = {}
__all__ = [
"PayPalWallet",
+ "RevolutWallet",
"SquareWallet",
"StripeWallet",
]
diff --git a/lnbits/fiat/revolut.py b/lnbits/fiat/revolut.py
new file mode 100644
index 000000000..fef4e1fbc
--- /dev/null
+++ b/lnbits/fiat/revolut.py
@@ -0,0 +1,350 @@
+import asyncio
+import json
+from collections.abc import AsyncGenerator
+from typing import Any
+
+import httpx
+from loguru import logger
+from pydantic import BaseModel, Field, ValidationError
+
+from lnbits.helpers import normalize_endpoint, urlsafe_short_hash
+from lnbits.settings import settings
+
+from .base import (
+ FiatInvoiceResponse,
+ FiatPaymentFailedStatus,
+ FiatPaymentPendingStatus,
+ FiatPaymentResponse,
+ FiatPaymentStatus,
+ FiatPaymentSuccessStatus,
+ FiatProvider,
+ FiatStatusResponse,
+ FiatSubscriptionPaymentOptions,
+ FiatSubscriptionResponse,
+)
+
+
+class RevolutCheckoutOptions(BaseModel):
+ class Config:
+ extra = "ignore"
+
+ success_url: str | None = None
+ metadata: dict[str, Any] = Field(default_factory=dict)
+ description: str | None = None
+
+
+class RevolutCreateInvoiceOptions(BaseModel):
+ class Config:
+ extra = "ignore"
+
+ checkout: RevolutCheckoutOptions | None = None
+
+
+class RevolutSubscriptionReference(BaseModel):
+ wallet_id: str
+ tag: str | None = None
+ subscription_request_id: str | None = None
+ extra: dict[str, Any] | None = None
+ memo: str | None = None
+
+
+class RevolutWallet(FiatProvider):
+ """https://developer.revolut.com/docs/merchant"""
+
+ def __init__(self):
+ logger.debug("Initializing RevolutWallet")
+ self._settings_fields = self._settings_connection_fields()
+ if not settings.revolut_api_endpoint:
+ raise ValueError("Cannot initialize RevolutWallet: missing endpoint.")
+ if not settings.revolut_api_secret_key:
+ raise ValueError("Cannot initialize RevolutWallet: missing API secret key.")
+
+ self.endpoint = normalize_endpoint(settings.revolut_api_endpoint)
+ self.headers = {
+ "Authorization": f"Bearer {settings.revolut_api_secret_key}",
+ "Revolut-Api-Version": settings.revolut_api_version,
+ "Content-Type": "application/json",
+ "User-Agent": settings.user_agent,
+ }
+ self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
+ logger.info("RevolutWallet initialized.")
+
+ async def cleanup(self):
+ try:
+ await self.client.aclose()
+ except RuntimeError as e:
+ logger.warning(f"Error closing Revolut wallet connection: {e}")
+
+ async def status(
+ self, only_check_settings: bool | None = False
+ ) -> FiatStatusResponse:
+ if only_check_settings:
+ if self._settings_fields != self._settings_connection_fields():
+ return FiatStatusResponse("Connection settings have changed.", 0)
+ return FiatStatusResponse(balance=0)
+
+ try:
+ r = await self.client.get("/api/orders", params={"limit": 1}, timeout=15)
+ r.raise_for_status()
+ _ = r.json()
+ return FiatStatusResponse(balance=0)
+ except json.JSONDecodeError:
+ return FiatStatusResponse("Server error: 'invalid json response'", 0)
+ except Exception as exc:
+ logger.warning(exc)
+ return FiatStatusResponse(f"Unable to connect to {self.endpoint}.", 0)
+
+ async def create_invoice(
+ self,
+ amount: float,
+ payment_hash: str,
+ currency: str,
+ memo: str | None = None,
+ extra: dict[str, Any] | None = None,
+ **kwargs,
+ ) -> FiatInvoiceResponse:
+ opts = self._parse_create_opts(extra or {})
+ if opts is None:
+ return FiatInvoiceResponse(ok=False, error_message="Invalid Revolut options")
+
+ amount_minor = int(amount * 100)
+ checkout = opts.checkout or RevolutCheckoutOptions()
+ success_url = (
+ checkout.success_url
+ or settings.revolut_payment_success_url
+ or "https://lnbits.com"
+ )
+
+ payload = {
+ "amount": amount_minor,
+ "currency": currency.upper(),
+ "description": checkout.description or memo or "LNbits Invoice",
+ "redirect_url": success_url,
+ "metadata": {
+ **checkout.metadata,
+ "payment_hash": payment_hash,
+ "alan_action": "invoice",
+ },
+ }
+
+ try:
+ r = await self.client.post("/api/orders", json=payload)
+ r.raise_for_status()
+ data = r.json()
+ order_id = data.get("id")
+ checkout_url = data.get("checkout_url")
+ if not order_id or not checkout_url:
+ return FiatInvoiceResponse(
+ ok=False, error_message="Server error: missing order id or url"
+ )
+ return FiatInvoiceResponse(
+ ok=True,
+ checking_id=f"order_{order_id}",
+ payment_request=checkout_url,
+ )
+ 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}."
+ )
+
+ async def create_subscription(
+ self,
+ subscription_id: str,
+ quantity: int,
+ payment_options: FiatSubscriptionPaymentOptions,
+ **kwargs,
+ ) -> FiatSubscriptionResponse:
+ if quantity != 1:
+ return FiatSubscriptionResponse(
+ ok=False,
+ error_message="Revolut subscriptions do not support quantity.",
+ )
+
+ wallet_id = payment_options.wallet_id
+ if not wallet_id:
+ return FiatSubscriptionResponse(
+ ok=False, error_message="Wallet ID is required."
+ )
+
+ extra = payment_options.extra or {}
+ customer_id = extra.get("customer_id")
+ if not customer_id:
+ return FiatSubscriptionResponse(
+ ok=False,
+ error_message="Revolut subscriptions require extra.customer_id.",
+ )
+
+ if not payment_options.subscription_request_id:
+ payment_options.subscription_request_id = urlsafe_short_hash()
+
+ reference = RevolutSubscriptionReference(
+ wallet_id=wallet_id,
+ tag=payment_options.tag,
+ subscription_request_id=payment_options.subscription_request_id,
+ extra=extra,
+ memo=payment_options.memo,
+ )
+ payload: dict[str, Any] = {
+ "plan_variation_id": subscription_id,
+ "customer_id": customer_id,
+ "external_reference": self._serialize_subscription_reference(reference),
+ "setup_order_redirect_url": (
+ payment_options.success_url
+ or settings.revolut_payment_success_url
+ or "https://lnbits.com"
+ ),
+ }
+ if extra.get("trial_duration"):
+ payload["trial_duration"] = extra["trial_duration"]
+
+ headers = {**self.headers, "Idempotency-Key": payment_options.subscription_request_id}
+
+ try:
+ r = await self.client.post("/api/subscriptions", json=payload, headers=headers)
+ r.raise_for_status()
+ data = r.json()
+ revolut_subscription_id = data.get("id")
+ setup_order_id = data.get("setup_order_id")
+ if not revolut_subscription_id or not setup_order_id:
+ return FiatSubscriptionResponse(
+ ok=False,
+ error_message="Server error: missing subscription id or setup order id",
+ )
+
+ setup_order = await self.get_order(setup_order_id)
+ checkout_url = setup_order.get("checkout_url")
+ if not checkout_url:
+ return FiatSubscriptionResponse(
+ ok=False, error_message="Server error: missing setup checkout url"
+ )
+
+ return FiatSubscriptionResponse(
+ ok=True,
+ checkout_session_url=checkout_url,
+ subscription_request_id=revolut_subscription_id,
+ )
+ except json.JSONDecodeError:
+ return FiatSubscriptionResponse(
+ ok=False, error_message="Server error: invalid json response"
+ )
+ except Exception as exc:
+ logger.warning(exc)
+ return FiatSubscriptionResponse(
+ ok=False, error_message=f"Unable to connect to {self.endpoint}."
+ )
+
+ async def cancel_subscription(
+ self,
+ subscription_id: str,
+ correlation_id: str,
+ **kwargs,
+ ) -> FiatSubscriptionResponse:
+ try:
+ r = await self.client.post(f"/api/subscriptions/{subscription_id}/cancel")
+ r.raise_for_status()
+ return FiatSubscriptionResponse(ok=True)
+ except Exception as exc:
+ logger.warning(exc)
+ return FiatSubscriptionResponse(
+ ok=False, error_message="Unable to cancel subscription."
+ )
+
+ async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
+ raise NotImplementedError("Revolut does not support paying invoices directly.")
+
+ async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
+ try:
+ order_id = self._normalize_revolut_id(checking_id)
+ return self._status_from_order(await self.get_order(order_id))
+ except Exception as exc:
+ logger.debug(f"Error getting Revolut invoice status: {exc}")
+ return FiatPaymentPendingStatus()
+
+ async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
+ raise NotImplementedError("Revolut does not support outgoing payments.")
+
+ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
+ logger.warning(
+ "Revolut does not support paid invoices stream. Use webhooks instead."
+ )
+ mock_queue: asyncio.Queue[str] = asyncio.Queue(0)
+ while settings.lnbits_running:
+ value = await mock_queue.get()
+ yield value
+
+ def _normalize_revolut_id(self, checking_id: str) -> str:
+ value = (
+ checking_id.replace("fiat_revolut_", "", 1)
+ if checking_id.startswith("fiat_revolut_")
+ else checking_id
+ )
+ return value.replace("order_", "", 1) if value.startswith("order_") else value
+
+ async def get_order(self, order_id: str) -> dict[str, Any]:
+ r = await self.client.get(f"/api/orders/{order_id}")
+ r.raise_for_status()
+ return r.json()
+
+ async def get_subscription(self, subscription_id: str) -> dict[str, Any]:
+ r = await self.client.get(f"/api/subscriptions/{subscription_id}")
+ r.raise_for_status()
+ return r.json()
+
+ async def get_subscription_cycle(
+ self, subscription_id: str, cycle_id: str
+ ) -> dict[str, Any]:
+ r = await self.client.get(f"/api/subscriptions/{subscription_id}/cycles/{cycle_id}")
+ r.raise_for_status()
+ return r.json()
+
+ def _status_from_order(self, order: dict[str, Any]) -> FiatPaymentStatus:
+ status = (order.get("state") or "").upper()
+ if status == "COMPLETED":
+ return FiatPaymentSuccessStatus()
+ if status in ["CANCELLED", "FAILED"]:
+ return FiatPaymentFailedStatus()
+ return FiatPaymentPendingStatus()
+
+ def _parse_create_opts(
+ self, raw_opts: dict[str, Any]
+ ) -> RevolutCreateInvoiceOptions | None:
+ try:
+ return RevolutCreateInvoiceOptions.parse_obj(raw_opts)
+ except ValidationError as e:
+ logger.warning(f"Invalid Revolut options: {e}")
+ return None
+
+ def _serialize_subscription_reference(
+ self, reference: RevolutSubscriptionReference
+ ) -> str:
+ payload = reference.dict(exclude_none=True)
+ serialized = json.dumps(payload, separators=(",", ":"))
+ if len(serialized) > 1024:
+ raise ValueError("Revolut subscription external_reference is too long.")
+ return serialized
+
+ def deserialize_subscription_reference(
+ self, external_reference: str | None
+ ) -> RevolutSubscriptionReference | None:
+ if not external_reference:
+ return None
+ try:
+ return RevolutSubscriptionReference.parse_obj(json.loads(external_reference))
+ except (json.JSONDecodeError, ValidationError) as exc:
+ logger.warning(exc)
+ return None
+
+ def _settings_connection_fields(self) -> str:
+ return "-".join(
+ [
+ str(settings.revolut_api_endpoint),
+ str(settings.revolut_api_secret_key),
+ str(settings.revolut_api_version),
+ str(settings.revolut_webhook_signing_secret),
+ ]
+ )
diff --git a/lnbits/fiat/square.py b/lnbits/fiat/square.py
index a68c7f4c7..4d9995582 100644
--- a/lnbits/fiat/square.py
+++ b/lnbits/fiat/square.py
@@ -548,55 +548,60 @@ class SquareWallet(FiatProvider):
async def _get_square_subscription_id(
self, subscription_id: str, wallet_id: str
) -> str:
- from lnbits.core.crud.payments import get_payments
- from lnbits.core.models import PaymentFilters
- from lnbits.db import Filter, Filters
+ try:
+ from lnbits.core.crud.payments import get_payments
+ from lnbits.core.models import PaymentFilters
+ from lnbits.db import Filter, Filters
- payments = await get_payments(
- wallet_id=wallet_id,
- filters=Filters(
- filters=[
- Filter.parse_query("external_id", [subscription_id], PaymentFilters)
- ],
- model=PaymentFilters,
- sortby="created_at",
- direction="desc",
- limit=1,
- ),
- )
- payment = next(
- (
- payment
- for payment in payments
- if payment.external_id and payment.fiat_provider == "square"
- ),
- None,
- )
- if payment and payment.external_id:
- return payment.external_id
+ payments = await get_payments(
+ wallet_id=wallet_id,
+ filters=Filters(
+ filters=[
+ Filter.parse_query(
+ "external_id", [subscription_id], PaymentFilters
+ )
+ ],
+ model=PaymentFilters,
+ sortby="created_at",
+ direction="desc",
+ limit=1,
+ ),
+ )
+ payment = next(
+ (
+ payment
+ for payment in payments
+ if payment.external_id and payment.fiat_provider == "square"
+ ),
+ None,
+ )
+ if payment and payment.external_id:
+ return payment.external_id
- payments = await get_payments(
- wallet_id=wallet_id,
- incoming=True,
- filters=Filters(
- model=PaymentFilters,
- sortby="created_at",
- direction="desc",
- ),
- )
- payment = next(
- (
- payment
- for payment in payments
- if payment.external_id
- and payment.fiat_provider == "square"
- and (payment.extra or {}).get("subscription_request_id")
- == subscription_id
- ),
- None,
- )
- if payment and payment.external_id:
- return payment.external_id
+ payments = await get_payments(
+ wallet_id=wallet_id,
+ incoming=True,
+ filters=Filters(
+ model=PaymentFilters,
+ sortby="created_at",
+ direction="desc",
+ ),
+ )
+ payment = next(
+ (
+ payment
+ for payment in payments
+ if payment.external_id
+ and payment.fiat_provider == "square"
+ and (payment.extra or {}).get("subscription_request_id")
+ == subscription_id
+ ),
+ None,
+ )
+ if payment and payment.external_id:
+ return payment.external_id
+ except Exception as exc:
+ logger.warning(exc)
return subscription_id
diff --git a/lnbits/settings.py b/lnbits/settings.py
index 316794ea1..f96d981a1 100644
--- a/lnbits/settings.py
+++ b/lnbits/settings.py
@@ -718,6 +718,20 @@ class SquareFiatProvider(LNbitsSettings):
square_limits: FiatProviderLimits = Field(default_factory=FiatProviderLimits)
+class RevolutFiatProvider(LNbitsSettings):
+ revolut_enabled: bool = Field(default=False)
+ revolut_api_endpoint: str = Field(default="https://merchant.revolut.com")
+ revolut_api_secret_key: str | None = Field(default=None)
+ revolut_api_version: str = Field(default="2026-04-20")
+ revolut_payment_success_url: str = Field(default="https://lnbits.com")
+ revolut_payment_webhook_url: str = Field(
+ default="https://your-lnbits-domain-here.com/api/v1/callback/revolut"
+ )
+ revolut_webhook_signing_secret: str | None = Field(default=None)
+
+ revolut_limits: FiatProviderLimits = Field(default_factory=FiatProviderLimits)
+
+
class LightningSettings(LNbitsSettings):
lightning_invoice_expiry: int = Field(default=3600, gt=0)
@@ -755,7 +769,12 @@ class FundingSourcesSettings(
funding_source_max_retries: int = Field(default=4, ge=0)
-class FiatProvidersSettings(StripeFiatProvider, PayPalFiatProvider, SquareFiatProvider):
+class FiatProvidersSettings(
+ StripeFiatProvider,
+ PayPalFiatProvider,
+ SquareFiatProvider,
+ RevolutFiatProvider,
+):
def is_fiat_provider_enabled(self, provider: str | None) -> bool:
"""
Checks if a specific fiat provider is enabled.
@@ -768,6 +787,8 @@ class FiatProvidersSettings(StripeFiatProvider, PayPalFiatProvider, SquareFiatPr
return self.paypal_enabled
if provider == "square":
return self.square_enabled
+ if provider == "revolut":
+ return self.revolut_enabled
return False
def get_fiat_providers_for_user(self, user_id: str) -> list[str]:
@@ -793,6 +814,12 @@ class FiatProvidersSettings(StripeFiatProvider, PayPalFiatProvider, SquareFiatPr
):
allowed_providers.append("square")
+ if self.revolut_enabled and (
+ not self.revolut_limits.allowed_users
+ or user_id in self.revolut_limits.allowed_users
+ ):
+ allowed_providers.append("revolut")
+
return allowed_providers
def get_fiat_provider_limits(self, provider_name: str) -> FiatProviderLimits | None:
diff --git a/lnbits/static/js/components/admin/lnbits-admin-fiat-providers.js b/lnbits/static/js/components/admin/lnbits-admin-fiat-providers.js
index dcd331925..da19e27d6 100644
--- a/lnbits/static/js/components/admin/lnbits-admin-fiat-providers.js
+++ b/lnbits/static/js/components/admin/lnbits-admin-fiat-providers.js
@@ -6,6 +6,7 @@ window.app.component('lnbits-admin-fiat-providers', {
formAddStripeUser: '',
formAddPaypalUser: '',
formAddSquareUser: '',
+ formAddRevolutUser: '',
hideInputToggle: true
}
},
@@ -21,6 +22,12 @@ window.app.component('lnbits-admin-fiat-providers', {
this.formData?.paypal_payment_webhook_url ||
this.calculateWebhookUrl('paypal')
)
+ },
+ revolutWebhookUrl() {
+ return (
+ this.formData?.revolut_payment_webhook_url ||
+ this.calculateWebhookUrl('revolut')
+ )
}
},
watch: {
@@ -60,6 +67,7 @@ window.app.component('lnbits-admin-fiat-providers', {
this.maybeSetWebhookUrl('stripe_payment_webhook_url', 'stripe')
this.maybeSetWebhookUrl('paypal_payment_webhook_url', 'paypal')
this.maybeSetWebhookUrl('square_payment_webhook_url', 'square')
+ this.maybeSetWebhookUrl('revolut_payment_webhook_url', 'revolut')
},
maybeSetWebhookUrl(fieldName, provider) {
if (!this.formData) {
@@ -130,6 +138,23 @@ window.app.component('lnbits-admin-fiat-providers', {
this.formData.square_limits.allowed_users =
this.formData.square_limits.allowed_users.filter(u => u !== user)
},
+ addRevolutAllowedUser() {
+ const addUser = this.formAddRevolutUser || ''
+ if (
+ addUser.length &&
+ !this.formData.revolut_limits.allowed_users.includes(addUser)
+ ) {
+ this.formData.revolut_limits.allowed_users = [
+ ...this.formData.revolut_limits.allowed_users,
+ addUser
+ ]
+ this.formAddRevolutUser = ''
+ }
+ },
+ removeRevolutAllowedUser(user) {
+ this.formData.revolut_limits.allowed_users =
+ this.formData.revolut_limits.allowed_users.filter(u => u !== user)
+ },
checkFiatProvider(providerName) {
LNbits.api
.request('PUT', `/api/v1/fiat/check/${providerName}`)
diff --git a/lnbits/templates/components/admin/fiat_providers.vue b/lnbits/templates/components/admin/fiat_providers.vue
index 0a4330fea..6b092c052 100644
--- a/lnbits/templates/components/admin/fiat_providers.vue
+++ b/lnbits/templates/components/admin/fiat_providers.vue
@@ -866,6 +866,239 @@
+ ORDER_AUTHORISED,
+ ORDER_COMPLETED, and
+ SUBSCRIPTION_INITIATED.
+