diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 2775a1dce..ff1ab497e 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -23,7 +23,7 @@ - + all diff --git a/BTCPayServer.Tests/Checkoutv2Tests.cs b/BTCPayServer.Tests/Checkoutv2Tests.cs index 6533cfb3f..282046a14 100644 --- a/BTCPayServer.Tests/Checkoutv2Tests.cs +++ b/BTCPayServer.Tests/Checkoutv2Tests.cs @@ -62,7 +62,7 @@ namespace BTCPayServer.Tests Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text); var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); - var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href"); + var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); var copyAddress = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"); Assert.Equal($"bitcoin:{address}", payUrl); Assert.StartsWith("bcrt", s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value")); @@ -70,15 +70,17 @@ namespace BTCPayServer.Tests Assert.Equal(address, copyAddress); Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue); s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC")); + s.Driver.ElementDoesNotExist(By.Id("PayByLNURL")); // Switch to LNURL s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click(); TestUtils.Eventually(() => { - payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href"); + payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); Assert.StartsWith("lightning:lnurl", payUrl); Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value")); s.Driver.ElementDoesNotExist(By.Id("Address_BTC")); + s.Driver.FindElement(By.Id("PayByLNURL")); }); // Default payment method @@ -91,12 +93,13 @@ namespace BTCPayServer.Tests Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).Text); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); - payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href"); + payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); copyAddress = s.Driver.FindElement(By.Id("Lightning_BTC_LightningLike")).GetAttribute("value"); Assert.Equal($"lightning:{address}", payUrl); Assert.Equal(address, copyAddress); Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue); s.Driver.ElementDoesNotExist(By.Id("Address_BTC")); + s.Driver.FindElement(By.Id("PayByLNURL")); // Lightning amount in Sats Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text); @@ -198,7 +201,7 @@ namespace BTCPayServer.Tests Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method"))); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); - payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href"); + payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); var copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"); var copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value"); Assert.StartsWith($"bitcoin:{address}?amount=", payUrl); @@ -209,6 +212,7 @@ namespace BTCPayServer.Tests Assert.StartsWith("lnbcrt", copyAddressLightning); Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue); Assert.Contains("&lightning=LNBCRT", qrValue); + s.Driver.FindElement(By.Id("PayByLNURL")); // BIP21 with LN as default payment method s.GoToHome(); @@ -216,9 +220,10 @@ namespace BTCPayServer.Tests s.GoToInvoiceCheckout(invoiceId); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method"))); - payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href"); + payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); Assert.StartsWith("bitcoin:", payUrl); Assert.Contains("&lightning=lnbcrt", payUrl); + s.Driver.FindElement(By.Id("PayByLNURL")); // Ensure LNURL is enabled s.GoToHome(); @@ -233,7 +238,7 @@ namespace BTCPayServer.Tests Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method"))); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); - payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href"); + payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"); copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value"); Assert.StartsWith($"bitcoin:{address}", payUrl); @@ -243,6 +248,7 @@ namespace BTCPayServer.Tests Assert.Equal(address, copyAddressOnchain); Assert.StartsWith("lnurl", copyAddressLightning); Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue); + s.Driver.FindElement(By.Id("PayByLNURL")); // Expiry message should not show amount for topup invoice expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds")); @@ -282,6 +288,26 @@ namespace BTCPayServer.Tests Assert.True(paymentInfo.Displayed); Assert.Contains("This invoice will expire in", paymentInfo.Text); Assert.Contains("09:5", paymentInfo.Text); + + // Disable LNURL again + s.GoToHome(); + s.GoToLightningSettings(); + s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false); + s.Driver.FindElement(By.Id("save")).Click(); + Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text); + + // Test: + // - NFC/LNURL-W available with just Lightning + // - BIP21 works correctly even though Lightning is default payment method + s.GoToHome(); + invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike"); + s.GoToInvoiceCheckout(invoiceId); + s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); + Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method"))); + payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); + Assert.StartsWith("bitcoin:", payUrl); + Assert.Contains("&lightning=lnbcrt", payUrl); + s.Driver.FindElement(By.Id("PayByLNURL")); } [Fact(Timeout = TestTimeout)] diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index 11dabd3b3..d447195db 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -652,17 +652,19 @@ namespace BTCPayServer.Controllers var lnurlId = PaymentMethodId.Parse("BTC_LNURLPAY"); if (paymentMethodId is null) { - var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider) - .Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 || - // Exclude LNURL for Checkout v2 + non-top up invoices - pmId != lnurlId || invoice.IsUnsetTopUp()) - .ToArray(); + var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider).ToArray(); // Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods - if (storeBlob is { CheckoutType: CheckoutType.V2, OnChainWithLnInvoiceFallback: true } && - enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnId)) + if (storeBlob is { CheckoutType: CheckoutType.V2, OnChainWithLnInvoiceFallback: true }) { - enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnId).ToArray(); + if (enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnId)) + { + enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnId).ToArray(); + } + if (enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnurlId)) + { + enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnurlId).ToArray(); + } } PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod(); @@ -688,7 +690,7 @@ namespace BTCPayServer.Controllers if (paymentMethodId is null) { paymentMethodId = enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ?? - enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.LightningLike) ?? + enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType != PaymentTypes.LNURLPay) ?? enabledPaymentIds.FirstOrDefault(); } isDefaultPaymentId = true; @@ -703,7 +705,12 @@ namespace BTCPayServer.Controllers return null; var paymentMethodTemp = invoice .GetPaymentMethods() - .FirstOrDefault(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode); + .FirstOrDefault(pm => + { + var pmId = pm.GetId(); + return paymentMethodId.CryptoCode == pmId.CryptoCode && + ((invoice.IsUnsetTopUp() && !storeBlob.OnChainWithLnInvoiceFallback) || pmId != lnurlId); + }); if (paymentMethodTemp == null) paymentMethodTemp = invoice.GetPaymentMethods().FirstOrDefault(); if (paymentMethodTemp is null) @@ -768,6 +775,7 @@ namespace BTCPayServer.Controllers BrandColor = storeBlob.BrandColor, CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType, HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice", + OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)), BtcAddress = paymentMethodDetails.GetPaymentDestination(), BtcDue = accounting.Due.ShowMoney(divisibility), @@ -803,9 +811,6 @@ namespace BTCPayServer.Controllers IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1, StoreId = store.Id, AvailableCryptos = invoice.GetPaymentMethods() - .Where(i => i.Network != null && storeBlob.CheckoutType == CheckoutType.V1 || - // Exclude LNURL for Checkout v2 + non-top up invoices - i.GetId() != lnurlId || invoice.IsUnsetTopUp()) .Select(kv => { var availableCryptoPaymentMethodId = kv.GetId(); @@ -835,20 +840,16 @@ namespace BTCPayServer.Controllers { var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == btcId.ToString()); var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnId.ToString()); - var lnurlPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnurlId.ToString()); if (onchainPM != null && lightningPM != null) { model.AvailableCryptos.Remove(lightningPM); } - if (onchainPM != null && lnurlPM != null) - { - model.AvailableCryptos.Remove(lnurlPM); - } } paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod); model.UISettings = paymentMethodHandler.GetCheckoutUISettings(); model.PaymentMethodId = paymentMethodId.ToString(); + model.PaymentType = paymentMethodId.PaymentType.ToString(); var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds); model.TimeLeft = expiration.PrettyPrint(); return model; diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index 0378965b5..b24383110 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -32,6 +32,7 @@ namespace BTCPayServer.Models.InvoicingModels public List AvailableCryptos { get; set; } = new(); public bool IsModal { get; set; } public bool IsUnsetTopUp { get; set; } + public bool OnChainWithLnInvoiceFallback { get; set; } public string CryptoCode { get; set; } public string InvoiceId { get; set; } public string BtcAddress { get; set; } diff --git a/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckout-v2.cshtml b/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckout-v2.cshtml index 7339dbb22..fc721369b 100644 --- a/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckout-v2.cshtml +++ b/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckout-v2.cshtml @@ -25,7 +25,7 @@ - @await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-post-content", model = Model}) diff --git a/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout-v2.cshtml b/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout-v2.cshtml index 6712600eb..dc64b501e 100644 --- a/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout-v2.cshtml +++ b/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout-v2.cshtml @@ -17,7 +17,7 @@ - @await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-lightning-post-content", model = Model}) diff --git a/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml b/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml index ce3069e9d..2d65278f4 100644 --- a/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml +++ b/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml @@ -3,12 +3,12 @@