From d73d0f178f5e340301fe476bb6e16f7c0b76dd96 Mon Sep 17 00:00:00 2001 From: d11n Date: Wed, 22 Feb 2023 07:53:14 +0100 Subject: [PATCH] Checkout: Allow NFC/LNURL-W whenever LNURL is available (#4671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Checkout: Allow NFC/LNURL-W whenever LNURL is available With what we have in master right now, we display NFC only for top-up invoices. With these changes, we display NFC in all cases, where LNURL is available. Note that this hides LNURL from the list of selectable payment methods, it's only available to use the NFC — and explicitely selectable only for the edge case of top-up invoice + non-unified QR (as before). Rationale: Now that we got NFC tightly integrated, it doesn't make sense to support the NFC experience only for top-up invoices. With this we bring back LNURL for regular invoices as well, but don't make it selectable and use it only for the NFC functionality. * Fix LNURL condition * Improve and test NFC/LNURL display condition Restores what was fixed in #4660. * Fix and test Lightning-only case * Add cache busting for locales --- BTCPayServer.Tests/BTCPayServer.Tests.csproj | 2 +- BTCPayServer.Tests/Checkoutv2Tests.cs | 38 ++++++++++++++++--- .../Controllers/UIInvoiceController.UI.cs | 37 +++++++++--------- .../Models/InvoicingModels/PaymentModel.cs | 1 + .../BitcoinLikeMethodCheckout-v2.cshtml | 2 +- .../LightningLikeMethodCheckout-v2.cshtml | 2 +- .../Views/Shared/NFC/CheckoutEnd.cshtml | 32 ++++++++++++---- .../Views/UIInvoice/CheckoutV2.cshtml | 11 ++++-- 8 files changed, 86 insertions(+), 39 deletions(-) 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 @@