mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 14:04:26 +01:00
Make NFC built in (#4541)
* Make NFC built int * support checkout v2 * uninstall old plugin * fix lnurl in unified checkout * fix tests * fix tests * fix old checkout unified qr * clean up and make nfc submission more sturdy * support topup invoices for lnurlw * fix test * Payment URI fixes * Fix LNURL exclusion cases * UI updates * Adapt test --------- Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
@@ -61,14 +61,14 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
|
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
|
||||||
var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||||
Assert.StartsWith("bitcoin:", payUrl);
|
Assert.StartsWith("bitcoin:", payUrl);
|
||||||
Assert.DoesNotContain("&lightning=", payUrl);
|
Assert.DoesNotContain("lightning=", payUrl);
|
||||||
|
|
||||||
// Switch to LNURL
|
// Switch to LNURL
|
||||||
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
|
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
|
||||||
TestUtils.Eventually(() =>
|
TestUtils.Eventually(() =>
|
||||||
{
|
{
|
||||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||||
Assert.StartsWith("lightning:", payUrl);
|
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default payment method
|
// Default payment method
|
||||||
@@ -78,9 +78,9 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||||
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
|
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
|
||||||
Assert.DoesNotContain("LNURL", s.Driver.PageSource);
|
Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).Text);
|
||||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||||
Assert.StartsWith("lightning:", payUrl);
|
Assert.StartsWith("lightning:lnbcrt", payUrl);
|
||||||
|
|
||||||
// Lightning amount in Sats
|
// Lightning amount in Sats
|
||||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||||
@@ -188,17 +188,16 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||||
Assert.StartsWith("bitcoin:", payUrl);
|
Assert.StartsWith("bitcoin:", payUrl);
|
||||||
Assert.Contains("&lightning=", payUrl);
|
Assert.Contains("&lightning=lnbcrt", payUrl);
|
||||||
|
|
||||||
// BIP21 with topup invoice
|
// BIP21 with topup invoice
|
||||||
s.GoToHome();
|
s.GoToHome();
|
||||||
invoiceId = s.CreateInvoice(amount: null);
|
invoiceId = s.CreateInvoice(amount: null);
|
||||||
s.GoToInvoiceCheckout(invoiceId);
|
s.GoToInvoiceCheckout(invoiceId);
|
||||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||||
Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text);
|
|
||||||
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
|
|
||||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||||
Assert.DoesNotContain("&lightning=", payUrl);
|
Assert.StartsWith("bitcoin:", payUrl);
|
||||||
|
Assert.DoesNotContain("&lightning=lnurl", payUrl);
|
||||||
|
|
||||||
// Expiry message should not show amount for topup invoice
|
// Expiry message should not show amount for topup invoice
|
||||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||||
|
|||||||
@@ -649,12 +649,13 @@ namespace BTCPayServer.Controllers
|
|||||||
var storeBlob = store.GetStoreBlob();
|
var storeBlob = store.GetStoreBlob();
|
||||||
var btcId = PaymentMethodId.Parse("BTC");
|
var btcId = PaymentMethodId.Parse("BTC");
|
||||||
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
|
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
|
||||||
|
var lnurlId = PaymentMethodId.Parse("BTC_LNURLPAY");
|
||||||
if (paymentMethodId is null)
|
if (paymentMethodId is null)
|
||||||
{
|
{
|
||||||
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider)
|
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider)
|
||||||
// Exclude LNURL for Checkout v2 + non-top up invoices
|
|
||||||
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 ||
|
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||||
pmId.PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp())
|
// Exclude LNURL for Checkout v2 + non-top up invoices
|
||||||
|
(pmId.PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp()))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
|
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
|
||||||
@@ -802,11 +803,9 @@ namespace BTCPayServer.Controllers
|
|||||||
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||||
StoreId = store.Id,
|
StoreId = store.Id,
|
||||||
AvailableCryptos = invoice.GetPaymentMethods()
|
AvailableCryptos = invoice.GetPaymentMethods()
|
||||||
.Where(i => i.Network != null &&
|
.Where(i => i.Network != null && storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||||
// Exclude LNURL for Checkout v2 + non-top up invoices
|
// Exclude LNURL for Checkout v2 + non-top up invoices
|
||||||
(storeBlob.CheckoutType == CheckoutType.V1 ||
|
i.GetId().PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp())
|
||||||
i.GetId().PaymentType is not LNURLPayPaymentType ||
|
|
||||||
invoice.IsUnsetTopUp()))
|
|
||||||
.Select(kv =>
|
.Select(kv =>
|
||||||
{
|
{
|
||||||
var availableCryptoPaymentMethodId = kv.GetId();
|
var availableCryptoPaymentMethodId = kv.GetId();
|
||||||
@@ -836,10 +835,15 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == btcId.ToString());
|
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == btcId.ToString());
|
||||||
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnId.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)
|
if (onchainPM != null && lightningPM != null)
|
||||||
{
|
{
|
||||||
model.AvailableCryptos.Remove(lightningPM);
|
model.AvailableCryptos.Remove(lightningPM);
|
||||||
}
|
}
|
||||||
|
if (onchainPM != null && lnurlPM != null)
|
||||||
|
{
|
||||||
|
model.AvailableCryptos.Remove(lnurlPM);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
|
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
|
||||||
|
|||||||
@@ -504,11 +504,8 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
blob.CheckoutType = model.UseNewCheckout ? Client.Models.CheckoutType.V2 : Client.Models.CheckoutType.V1;
|
blob.CheckoutType = model.UseNewCheckout ? Client.Models.CheckoutType.V2 : Client.Models.CheckoutType.V1;
|
||||||
if (blob.CheckoutType == Client.Models.CheckoutType.V2)
|
|
||||||
{
|
|
||||||
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
||||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Common;
|
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
@@ -11,6 +10,7 @@ using BTCPayServer.Models;
|
|||||||
using BTCPayServer.Models.InvoicingModels;
|
using BTCPayServer.Models.InvoicingModels;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
@@ -69,13 +69,24 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
{
|
{
|
||||||
var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
|
var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
|
||||||
a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike));
|
a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike));
|
||||||
|
if (lightningInfo is not null && !string.IsNullOrEmpty(lightningInfo.PaymentUrls?.BOLT11))
|
||||||
|
{
|
||||||
// Turn the colon into an equal sign to trun the whole into the lightning part of the query string
|
lightningFallback = lightningInfo.PaymentUrls.BOLT11;
|
||||||
|
}
|
||||||
// lightningInfo?.PaymentUrls?.BOLT11: lightning:lnbcrt440070n1p3ua9np...
|
else
|
||||||
lightningFallback = lightningInfo?.PaymentUrls?.BOLT11.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase);
|
{
|
||||||
// lightningFallback: lightning=lnbcrt440070n1p3ua9np...
|
var lnurl = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
|
||||||
|
a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LNURLPay));
|
||||||
|
if (lnurl is not null)
|
||||||
|
{
|
||||||
|
lightningFallback = LNURL.LNURL.EncodeUri(new Uri(lnurl.Url), "payRequest", true).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(lightningFallback))
|
||||||
|
{
|
||||||
|
lightningFallback = lightningFallback
|
||||||
|
.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.Activated)
|
if (model.Activated)
|
||||||
|
|||||||
183
BTCPayServer/Plugins/NFC/NFCController.cs
Normal file
183
BTCPayServer/Plugins/NFC/NFCController.cs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Data.Payouts.LightningLike;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using LNURL;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins.NFC
|
||||||
|
{
|
||||||
|
[Route("plugins/NFC")]
|
||||||
|
public class NFCController : Controller
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly InvoiceRepository _invoiceRepository;
|
||||||
|
private readonly UILNURLController _uilnurlController;
|
||||||
|
private readonly InvoiceActivator _invoiceActivator;
|
||||||
|
private readonly StoreRepository _storeRepository;
|
||||||
|
|
||||||
|
public NFCController(IHttpClientFactory httpClientFactory,
|
||||||
|
InvoiceRepository invoiceRepository,
|
||||||
|
UILNURLController uilnurlController,
|
||||||
|
InvoiceActivator invoiceActivator,
|
||||||
|
StoreRepository storeRepository)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_invoiceRepository = invoiceRepository;
|
||||||
|
_uilnurlController = uilnurlController;
|
||||||
|
_invoiceActivator = invoiceActivator;
|
||||||
|
_storeRepository = storeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubmitRequest
|
||||||
|
{
|
||||||
|
public string Lnurl { get; set; }
|
||||||
|
public string InvoiceId { get; set; }
|
||||||
|
public long? Amount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> SubmitLNURLWithdrawForInvoice([FromBody] SubmitRequest request)
|
||||||
|
{
|
||||||
|
var invoice = await _invoiceRepository.GetInvoice(request.InvoiceId);
|
||||||
|
if (invoice?.Status is not InvoiceStatusLegacy.New)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var methods = invoice.GetPaymentMethods();
|
||||||
|
PaymentMethod lnPaymentMethod = null;
|
||||||
|
if (!methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LNURLPay), out var lnurlPaymentMethod) &&
|
||||||
|
!methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike), out lnPaymentMethod))
|
||||||
|
{
|
||||||
|
return BadRequest("destination for lnurlw was not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri;
|
||||||
|
string tag;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uri = LNURL.LNURL.Parse(request.Lnurl, out tag);
|
||||||
|
if (uri is null)
|
||||||
|
{
|
||||||
|
return BadRequest("lnurl was malformed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return BadRequest(e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(tag) && !tag.Equals("withdrawRequest"))
|
||||||
|
{
|
||||||
|
return BadRequest("lnurl was not lnurl-withdraw");
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpClient = _httpClientFactory.CreateClient(uri.IsOnion()
|
||||||
|
? LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient
|
||||||
|
: LightningLikePayoutHandler.LightningLikePayoutHandlerClearnetNamedClient);
|
||||||
|
var info = (await
|
||||||
|
LNURL.LNURL.FetchInformation(uri, "withdrawRequest", httpClient)) as LNURLWithdrawRequest;
|
||||||
|
if (info?.Callback is null)
|
||||||
|
{
|
||||||
|
return BadRequest("Could not fetch info from lnurl-withdraw ");
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient = _httpClientFactory.CreateClient(info.Callback.IsOnion()
|
||||||
|
? LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient
|
||||||
|
: LightningLikePayoutHandler.LightningLikePayoutHandlerClearnetNamedClient);
|
||||||
|
|
||||||
|
string bolt11 = null;
|
||||||
|
|
||||||
|
if (lnPaymentMethod is not null)
|
||||||
|
{
|
||||||
|
if (lnPaymentMethod.GetPaymentMethodDetails() is LightningLikePaymentMethodDetails
|
||||||
|
{
|
||||||
|
Activated: false
|
||||||
|
} lnPMD)
|
||||||
|
{
|
||||||
|
var store = await _storeRepository.FindStore(invoice.StoreId);
|
||||||
|
await _invoiceActivator.ActivateInvoicePaymentMethod(lnPaymentMethod.GetId(), invoice, store);
|
||||||
|
}
|
||||||
|
|
||||||
|
lnPMD = lnPaymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
|
||||||
|
Money due;
|
||||||
|
if (invoice.Type == InvoiceType.TopUp && request.Amount is not null)
|
||||||
|
{
|
||||||
|
due = new Money(request.Amount.Value, MoneyUnit.Satoshi);
|
||||||
|
}else if (invoice.Type == InvoiceType.TopUp)
|
||||||
|
{
|
||||||
|
return BadRequest("This is a topup invoice and you need to provide the amount in sats to pay.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
due = lnPaymentMethod.Calculate().Due;
|
||||||
|
}
|
||||||
|
if (info.MinWithdrawable > due || due > info.MaxWithdrawable)
|
||||||
|
{
|
||||||
|
return BadRequest("invoice amount is not payable with the lnurl allowed amounts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lnPMD?.Activated is true)
|
||||||
|
{
|
||||||
|
bolt11 = lnPMD.BOLT11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lnurlPaymentMethod is not null)
|
||||||
|
{
|
||||||
|
Money due;
|
||||||
|
if (invoice.Type == InvoiceType.TopUp && request.Amount is not null)
|
||||||
|
{
|
||||||
|
due = new Money(request.Amount.Value, MoneyUnit.Satoshi);
|
||||||
|
}else if (invoice.Type == InvoiceType.TopUp)
|
||||||
|
{
|
||||||
|
return BadRequest("This is a topup invoice and you need to provide the amount in sats to pay.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
due = lnurlPaymentMethod.Calculate().Due;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _uilnurlController.GetLNURLForInvoice(request.InvoiceId, "BTC",
|
||||||
|
due.Satoshi);
|
||||||
|
|
||||||
|
if (response is ObjectResult objectResult)
|
||||||
|
{
|
||||||
|
switch (objectResult.Value)
|
||||||
|
{
|
||||||
|
case LNURLPayRequest.LNURLPayRequestCallbackResponse lnurlPayRequestCallbackResponse:
|
||||||
|
bolt11 = lnurlPayRequestCallbackResponse.Pr;
|
||||||
|
break;
|
||||||
|
case LNUrlStatusResponse lnUrlStatusResponse:
|
||||||
|
|
||||||
|
return BadRequest(
|
||||||
|
$"Could not fetch bolt11 invoice to pay to: {lnUrlStatusResponse.Reason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bolt11 is null)
|
||||||
|
{
|
||||||
|
return BadRequest("Could not fetch bolt11 invoice to pay to.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await info.SendRequest(bolt11, httpClient);
|
||||||
|
if (result.Status.Equals("ok", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return Ok(result.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest(result.Reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
BTCPayServer/Plugins/NFC/NFCPlugin.cs
Normal file
31
BTCPayServer/Plugins/NFC/NFCPlugin.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Abstractions.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins.NFC
|
||||||
|
{
|
||||||
|
public class NFCPlugin : BaseBTCPayServerPlugin
|
||||||
|
{
|
||||||
|
|
||||||
|
public override string Identifier => "BTCPayServer.Plugins.NFC";
|
||||||
|
public override string Name => "NFC";
|
||||||
|
public override string Description => "Allows you to support contactless card payments over NFC and LNURL Withdraw!";
|
||||||
|
|
||||||
|
|
||||||
|
public override void Execute(IServiceCollection applicationBuilder)
|
||||||
|
{
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NFC/CheckoutEnd",
|
||||||
|
"checkout-end"));
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NFC/LNURLNFCPostContent",
|
||||||
|
"checkout-lightning-post-content"));
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NFC/CheckoutEnd",
|
||||||
|
"checkout-v2-end"));
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NFC/LNURLNFCPostContent",
|
||||||
|
"checkout-v2-lightning-post-content"));
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("NFC/LNURLNFCPostContent",
|
||||||
|
"checkout-v2-bitcoin-post-content"));
|
||||||
|
base.Execute(applicationBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,6 +120,12 @@ namespace BTCPayServer.Plugins
|
|||||||
|
|
||||||
foreach (var toLoad in pluginsToLoad)
|
foreach (var toLoad in pluginsToLoad)
|
||||||
{
|
{
|
||||||
|
// This used to be a standalone plugin but due to popular demand has been made as part of core. If we detect an install, we remove the redundant plugin.
|
||||||
|
if (toLoad.PluginIdentifier == "BTCPayServer.Plugins.NFC")
|
||||||
|
{
|
||||||
|
QueueCommands(pluginsFolder, ("delete", toLoad.PluginIdentifier));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!loadedPluginIdentifiers.Add(toLoad.PluginIdentifier))
|
if (!loadedPluginIdentifiers.Add(toLoad.PluginIdentifier))
|
||||||
continue;
|
continue;
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@model BTCPayServer.Models.InvoicingModels.PaymentModel
|
@model BTCPayServer.Models.InvoicingModels.PaymentModel
|
||||||
|
|
||||||
<template id="bitcoin-method-checkout-template">
|
<template id="bitcoin-method-checkout-template">
|
||||||
|
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-pre-content", model = Model})
|
||||||
<div class="payment-box">
|
<div class="payment-box">
|
||||||
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
||||||
<div>
|
<div>
|
||||||
@@ -17,15 +18,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-link" data-clipboard-target="#Address_@Model.PaymentMethodId" :data-clipboard-confirm="$t('copy_confirm')" v-t="'copy'"></button>
|
<button type="button" class="btn btn-link" data-clipboard-target="#Address_@Model.PaymentMethodId" :data-clipboard-confirm="$t('copy_confirm')" v-t="'copy'"></button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="BOLT11" class="input-group mt-3">
|
<div v-if="lightning" class="input-group mt-3">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input id="BOLT11_@Model.PaymentMethodId" class="form-control-plaintext" readonly="readonly" :value="BOLT11" />
|
<input id="Lightning_@Model.PaymentMethodId" class="form-control-plaintext" readonly="readonly" :value="lightning" />
|
||||||
<label for="BOLT11_@Model.PaymentMethodId" v-t="'lightning'"></label>
|
<label for="Lightning_@Model.PaymentMethodId" v-t="'lightning'"></label>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-link" data-clipboard-target="#BOLT11_@Model.PaymentMethodId" :data-clipboard-confirm="$t('copy_confirm')" v-t="'copy'"></button>
|
<button type="button" class="btn btn-link" data-clipboard-target="#Lightning_@Model.PaymentMethodId" :data-clipboard-confirm="$t('copy_confirm')" v-t="'copy'"></button>
|
||||||
</div>
|
</div>
|
||||||
<a v-if="model.invoiceBitcoinUrl" class="btn btn-primary rounded-pill w-100 mt-4" target="_top"
|
<a v-if="model.invoiceBitcoinUrl" class="btn btn-primary rounded-pill w-100 mt-4" target="_top"
|
||||||
:href="model.invoiceBitcoinUrl" :title="$t(hasPayjoin ? 'BIP21 payment link with PayJoin support' : 'BIP21 payment link')" v-t="'pay_in_wallet'"></a>
|
:href="model.invoiceBitcoinUrl" :title="$t(hasPayjoin ? 'BIP21 payment link with PayJoin support' : 'BIP21 payment link')" v-t="'pay_in_wallet'"></a>
|
||||||
|
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-post-content", model = Model})
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,8 +46,8 @@
|
|||||||
hasPayjoin () {
|
hasPayjoin () {
|
||||||
return this.model.invoiceBitcoinUrl.indexOf('@PayjoinClient.BIP21EndpointKey=') !== -1;
|
return this.model.invoiceBitcoinUrl.indexOf('@PayjoinClient.BIP21EndpointKey=') !== -1;
|
||||||
},
|
},
|
||||||
BOLT11 () {
|
lightning () {
|
||||||
const match = this.model.invoiceBitcoinUrl.match(/&lightning=(.*)&?/i);
|
const match = this.model.invoiceBitcoinUrl.match(/[&?]lightning=(.*)&?/i);
|
||||||
return match ? match[1].toLowerCase() : null;
|
return match ? match[1].toLowerCase() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<template id="lightning-method-checkout-template">
|
<template id="lightning-method-checkout-template">
|
||||||
<div class="payment-box">
|
<div class="payment-box">
|
||||||
|
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-v2-lightning-pre-content", model = Model})
|
||||||
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
||||||
<div>
|
<div>
|
||||||
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
|
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<a v-if="model.invoiceBitcoinUrl" class="btn btn-primary rounded-pill w-100 mt-4" target="_top"
|
<a v-if="model.invoiceBitcoinUrl" class="btn btn-primary rounded-pill w-100 mt-4" target="_top"
|
||||||
:href="model.invoiceBitcoinUrl" v-t="'pay_in_wallet'"></a>
|
:href="model.invoiceBitcoinUrl" v-t="'pay_in_wallet'"></a>
|
||||||
|
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-lightning-post-content", model = Model})
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
133
BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml
Normal file
133
BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@using BTCPayServer.Abstractions.TagHelpers
|
||||||
|
<template id="lnurl-withdraw-template">
|
||||||
|
<button v-if="v2" class="btn btn-secondary rounded-pill w-100 mt-4" type="button"
|
||||||
|
v-on:click="startScan"
|
||||||
|
v-bind:disabled="scanning || submitting"
|
||||||
|
v-bind:class="{ 'loading': scanning || submitting, 'text-secondary': !supported }">{{btnText}}</button>
|
||||||
|
|
||||||
|
<bp-loading-button v-else>
|
||||||
|
<button v-on:click="startScan" style="margin-top: 15px;" v-bind:disabled="scanning || submitting"
|
||||||
|
v-bind:class="{ 'loading': scanning || submitting, 'action-button': supported, 'btn btn-text w-100': !supported }">
|
||||||
|
<span class="button-text">{{btnText}}</span>
|
||||||
|
<div class="loader-wrapper">
|
||||||
|
<partial name="Checkout-Spinner" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</bp-loading-button>
|
||||||
|
</template>
|
||||||
|
<script type="text/javascript">
|
||||||
|
Vue.component("lnurl-withdraw-checkout", {
|
||||||
|
template: "#lnurl-withdraw-template",
|
||||||
|
computed: {
|
||||||
|
v2: function() {
|
||||||
|
return !!this.$parent.model;
|
||||||
|
},
|
||||||
|
topup: function (){
|
||||||
|
return (this.$parent.srvModel || this.$parent.model).isUnsetTopUp;
|
||||||
|
},
|
||||||
|
destination: function (){
|
||||||
|
return (this.$parent.srvModel || this.$parent.model).invoiceId;
|
||||||
|
},
|
||||||
|
btnText: function (){
|
||||||
|
return this.supported? "Pay by NFC & LNURL-Withdraw" : "Pay by LNURL-Withdraw";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
url : @Safe.Json(Context.Request.GetAbsoluteUri(Url.Action("SubmitLNURLWithdrawForInvoice", "NFC"))),
|
||||||
|
supported: ('NDEFReader' in window && window.self === window.top),
|
||||||
|
scanning: false,
|
||||||
|
submitting: false,
|
||||||
|
readerAbortController: null,
|
||||||
|
amount: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
startScan: async function () {
|
||||||
|
try {
|
||||||
|
if (this.scanning || this.submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.topup) {
|
||||||
|
const amountStr = prompt("How many sats do you want to pay?")
|
||||||
|
if (amountStr){
|
||||||
|
try {
|
||||||
|
this.amount = parseInt(amountStr)
|
||||||
|
} catch {
|
||||||
|
alert("Please provide a valid number amount in sats");
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
self.submitting = false;
|
||||||
|
self.scanning = true;
|
||||||
|
if (!this.supported) {
|
||||||
|
const result = prompt("Enter LNURL withdraw");
|
||||||
|
if (result) {
|
||||||
|
self.sendData.bind(self)(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.scanning = false;
|
||||||
|
}
|
||||||
|
ndef = new NDEFReader()
|
||||||
|
self.readerAbortController = new AbortController()
|
||||||
|
await ndef.scan({signal: self.readerAbortController.signal})
|
||||||
|
|
||||||
|
ndef.addEventListener('readingerror', () => {
|
||||||
|
self.scanning = false;
|
||||||
|
self.readerAbortController.abort()
|
||||||
|
})
|
||||||
|
|
||||||
|
ndef.addEventListener('reading', ({message, serialNumber}) => {
|
||||||
|
//Decode NDEF data from tag
|
||||||
|
const record = message.records[0]
|
||||||
|
const textDecoder = new TextDecoder('utf-8')
|
||||||
|
const lnurl = textDecoder.decode(record.data)
|
||||||
|
|
||||||
|
//User feedback, show loader icon
|
||||||
|
self.scanning = false;
|
||||||
|
self.sendData.bind(self)(lnurl);
|
||||||
|
|
||||||
|
})
|
||||||
|
} catch(e) {
|
||||||
|
self.scanning = false;
|
||||||
|
self.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendData: function (lnurl) {
|
||||||
|
|
||||||
|
this.submitting = true;
|
||||||
|
//Post LNURLW data to server
|
||||||
|
var xhr = new XMLHttpRequest()
|
||||||
|
xhr.open('POST', this.url, true)
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||||
|
xhr.send(JSON.stringify({lnurl, invoiceId: this.destination, amount: this.amount}))
|
||||||
|
const self = this;
|
||||||
|
//User feedback, reset on failure
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.readyState === xhr.DONE) {
|
||||||
|
console.log(xhr.response);
|
||||||
|
console.log(xhr.responseText);
|
||||||
|
self.scanning = false;
|
||||||
|
self.submitting = false;
|
||||||
|
|
||||||
|
if(self.readerAbortController) {
|
||||||
|
self.readerAbortController.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(xhr.response){
|
||||||
|
alert(xhr.response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
3
BTCPayServer/Views/Shared/NFC/LNURLNFCPostContent.cshtml
Normal file
3
BTCPayServer/Views/Shared/NFC/LNURLNFCPostContent.cshtml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<lnurl-withdraw-checkout>
|
||||||
|
|
||||||
|
</lnurl-withdraw-checkout>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<h5 class="text-center mt-1 mb-3 fw-semibold" v-if="srvModel.itemDesc" v-text="srvModel.itemDesc">@Model.ItemDesc</h5>
|
<h5 class="text-center mt-1 mb-3 fw-semibold" v-if="srvModel.itemDesc" v-text="srvModel.itemDesc">@Model.ItemDesc</h5>
|
||||||
@if (Model.IsUnsetTopUp)
|
@if (Model.IsUnsetTopUp)
|
||||||
{
|
{
|
||||||
<h2 id="AmountDue" class="text-center mb-3" v-t="'any_amount'"></h2>
|
<h2 id="AmountDue" class="text-center" v-t="'any_amount'"></h2>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -237,5 +237,6 @@
|
|||||||
<partial name="@paymentMethodHandler.ExtensionPartial-v2" model="@Model"/>
|
<partial name="@paymentMethodHandler.ExtensionPartial-v2" model="@Model"/>
|
||||||
}
|
}
|
||||||
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment", model = Model })
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment", model = Model })
|
||||||
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-v2-end", model = Model })
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -78,13 +78,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkout-settings collapse @(Model.UseNewCheckout ? "show" : "")" id="NewCheckoutSettings">
|
<div class="checkout-settings collapse @(Model.UseNewCheckout ? "show" : "")" id="NewCheckoutSettings">
|
||||||
<div class="form-check">
|
|
||||||
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input"/>
|
|
||||||
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
|
|
||||||
<a href="https://bitcoinqr.dev/" target="_blank" rel="noreferrer noopener">
|
|
||||||
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="DisplayExpirationTimer" class="form-label"></label>
|
<label asp-for="DisplayExpirationTimer" class="form-label"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -94,7 +87,13 @@
|
|||||||
<span asp-validation-for="DisplayExpirationTimer" class="text-danger"></span>
|
<span asp-validation-for="DisplayExpirationTimer" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input" />
|
||||||
|
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
|
||||||
|
<a href="https://bitcoinqr.dev/" target="_blank" rel="noreferrer noopener">
|
||||||
|
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="checkout-settings collapse @(Model.UseNewCheckout ? "" : "show")" id="OldCheckoutSettings">
|
<div class="checkout-settings collapse @(Model.UseNewCheckout ? "" : "show")" id="OldCheckoutSettings">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check-input" />
|
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check-input" />
|
||||||
@@ -141,7 +140,6 @@
|
|||||||
<input asp-for="HtmlTitle" class="form-control" />
|
<input asp-for="HtmlTitle" class="form-control" />
|
||||||
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
|
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check my-3">
|
<div class="form-check my-3">
|
||||||
<input asp-for="LazyPaymentMethods" type="checkbox" class="form-check-input" />
|
<input asp-for="LazyPaymentMethods" type="checkbox" class="form-check-input" />
|
||||||
<label asp-for="LazyPaymentMethods" class="form-check-label"></label>
|
<label asp-for="LazyPaymentMethods" class="form-check-label"></label>
|
||||||
@@ -150,7 +148,6 @@
|
|||||||
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" />
|
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" />
|
||||||
<label asp-for="RedirectAutomatically" class="form-check-label"></label>
|
<label asp-for="RedirectAutomatically" class="form-check-label"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="mt-5 mb-3">Public receipt</h3>
|
<h3 class="mt-5 mb-3">Public receipt</h3>
|
||||||
<div class="form-check my-3">
|
<div class="form-check my-3">
|
||||||
<input asp-for="ReceiptOptions.Enabled" type="checkbox" class="form-check-input" />
|
<input asp-for="ReceiptOptions.Enabled" type="checkbox" class="form-check-input" />
|
||||||
@@ -164,7 +161,6 @@
|
|||||||
<input asp-for="ReceiptOptions.ShowQR" type="checkbox" class="form-check-input" />
|
<input asp-for="ReceiptOptions.ShowQR" type="checkbox" class="form-check-input" />
|
||||||
<label asp-for="ReceiptOptions.ShowQR" class="form-check-label"></label>
|
<label asp-for="ReceiptOptions.ShowQR" class="form-check-label"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mt-4" id="Save">Save</button>
|
<button type="submit" class="btn btn-primary mt-4" id="Save">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user