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);
|
||||
var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.DoesNotContain("&lightning=", payUrl);
|
||||
Assert.DoesNotContain("lightning=", payUrl);
|
||||
|
||||
// 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");
|
||||
Assert.StartsWith("lightning:", payUrl);
|
||||
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||
});
|
||||
|
||||
// Default payment method
|
||||
@@ -78,9 +78,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
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");
|
||||
Assert.StartsWith("lightning:", payUrl);
|
||||
Assert.StartsWith("lightning:lnbcrt", payUrl);
|
||||
|
||||
// Lightning amount in Sats
|
||||
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")));
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&lightning=", payUrl);
|
||||
Assert.Contains("&lightning=lnbcrt", payUrl);
|
||||
|
||||
// BIP21 with topup invoice
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(amount: null);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
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);
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
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
|
||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
|
||||
@@ -649,12 +649,13 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var btcId = PaymentMethodId.Parse("BTC");
|
||||
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
|
||||
var lnurlId = PaymentMethodId.Parse("BTC_LNURLPAY");
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider)
|
||||
// Exclude LNURL for Checkout v2 + non-top up invoices
|
||||
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||
pmId.PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp())
|
||||
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||
// Exclude LNURL for Checkout v2 + non-top up invoices
|
||||
(pmId.PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp()))
|
||||
.ToArray();
|
||||
|
||||
// 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,
|
||||
StoreId = store.Id,
|
||||
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
|
||||
(storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||
i.GetId().PaymentType is not LNURLPayPaymentType ||
|
||||
invoice.IsUnsetTopUp()))
|
||||
i.GetId().PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp())
|
||||
.Select(kv =>
|
||||
{
|
||||
var availableCryptoPaymentMethodId = kv.GetId();
|
||||
@@ -836,10 +835,15 @@ 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);
|
||||
|
||||
@@ -504,11 +504,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
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.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Common;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
@@ -11,6 +10,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.Models;
|
||||
@@ -69,13 +69,24 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
|
||||
a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike));
|
||||
|
||||
|
||||
// Turn the colon into an equal sign to trun the whole into the lightning part of the query string
|
||||
|
||||
// lightningInfo?.PaymentUrls?.BOLT11: lightning:lnbcrt440070n1p3ua9np...
|
||||
lightningFallback = lightningInfo?.PaymentUrls?.BOLT11.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase);
|
||||
// lightningFallback: lightning=lnbcrt440070n1p3ua9np...
|
||||
if (lightningInfo is not null && !string.IsNullOrEmpty(lightningInfo.PaymentUrls?.BOLT11))
|
||||
{
|
||||
lightningFallback = lightningInfo.PaymentUrls.BOLT11;
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
|
||||
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)
|
||||
{
|
||||
// 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))
|
||||
continue;
|
||||
try
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@model BTCPayServer.Models.InvoicingModels.PaymentModel
|
||||
|
||||
<template id="bitcoin-method-checkout-template">
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-pre-content", model = Model})
|
||||
<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>
|
||||
@@ -17,15 +18,16 @@
|
||||
</div>
|
||||
<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 v-if="BOLT11" class="input-group mt-3">
|
||||
<div v-if="lightning" class="input-group mt-3">
|
||||
<div class="form-floating">
|
||||
<input id="BOLT11_@Model.PaymentMethodId" class="form-control-plaintext" readonly="readonly" :value="BOLT11" />
|
||||
<label for="BOLT11_@Model.PaymentMethodId" v-t="'lightning'"></label>
|
||||
<input id="Lightning_@Model.PaymentMethodId" class="form-control-plaintext" readonly="readonly" :value="lightning" />
|
||||
<label for="Lightning_@Model.PaymentMethodId" v-t="'lightning'"></label>
|
||||
</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>
|
||||
<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>
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-post-content", model = Model})
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -44,8 +46,8 @@
|
||||
hasPayjoin () {
|
||||
return this.model.invoiceBitcoinUrl.indexOf('@PayjoinClient.BIP21EndpointKey=') !== -1;
|
||||
},
|
||||
BOLT11 () {
|
||||
const match = this.model.invoiceBitcoinUrl.match(/&lightning=(.*)&?/i);
|
||||
lightning () {
|
||||
const match = this.model.invoiceBitcoinUrl.match(/[&?]lightning=(.*)&?/i);
|
||||
return match ? match[1].toLowerCase() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<template id="lightning-method-checkout-template">
|
||||
<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>
|
||||
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
|
||||
@@ -18,6 +19,7 @@
|
||||
</div>
|
||||
<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>
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-lightning-post-content", model = Model})
|
||||
</div>
|
||||
</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>
|
||||
@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
|
||||
{
|
||||
@@ -237,5 +237,6 @@
|
||||
<partial name="@paymentMethodHandler.ExtensionPartial-v2" model="@Model"/>
|
||||
}
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment", model = Model })
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-v2-end", model = Model })
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
delegate('click', '.setTheme', e => {
|
||||
e.preventDefault();
|
||||
@@ -60,7 +60,7 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<h3 class="mt-5 mb-3">Checkout</h3>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<input asp-for="UseNewCheckout" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target=".checkout-settings" aria-expanded="@(Model.UseNewCheckout)" aria-controls="NewCheckoutSettings" />
|
||||
@@ -71,30 +71,29 @@
|
||||
</label>
|
||||
<span asp-validation-for="UseNewCheckout" class="text-danger"></span>
|
||||
<div class="form-text">
|
||||
Since v1.7.0 a new version of the checkout is available. Note: For now, the new version is English-only.<br/>
|
||||
Since v1.7.0 a new version of the checkout is available. Note: For now, the new version is English-only.<br />
|
||||
We are still collecting <a href="https://github.com/btcpayserver/btcpayserver/discussions/4308" target="_blank" rel="noreferrer noopener">feedback</a> and offer this as an opt-in feature.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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">
|
||||
<label asp-for="DisplayExpirationTimer" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input inputmode="numeric" asp-for="DisplayExpirationTimer" class="form-control" style="max-width:10ch;"/>
|
||||
<input inputmode="numeric" asp-for="DisplayExpirationTimer" class="form-control" style="max-width:10ch;" />
|
||||
<span class="input-group-text">minutes</span>
|
||||
</div>
|
||||
<span asp-validation-for="DisplayExpirationTimer" class="text-danger"></span>
|
||||
</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="form-check">
|
||||
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check-input" />
|
||||
@@ -102,7 +101,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input asp-for="AutoDetectLanguage" type="checkbox" class="form-check-input"/>
|
||||
<input asp-for="AutoDetectLanguage" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="AutoDetectLanguage" class="form-check-label"></label>
|
||||
<div class="form-text">Detects the language of the customer's browser with 99.9% accuracy.</div>
|
||||
</div>
|
||||
@@ -132,7 +131,7 @@
|
||||
<a href="#" class="setTheme" data-theme="dark">Dark</a> |
|
||||
<a href="#" class="setTheme" data-theme="legacy">Legacy</a>
|
||||
</div>
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new { location = "invoice-checkout-theme-options", model = Model })
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new {location = "invoice-checkout-theme-options", model = Model})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +140,6 @@
|
||||
<input asp-for="HtmlTitle" class="form-control" />
|
||||
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="LazyPaymentMethods" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="LazyPaymentMethods" class="form-check-label"></label>
|
||||
@@ -150,7 +148,6 @@
|
||||
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="RedirectAutomatically" class="form-check-label"></label>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5 mb-3">Public receipt</h3>
|
||||
<div class="form-check my-3">
|
||||
<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" />
|
||||
<label asp-for="ReceiptOptions.ShowQR" class="form-check-label"></label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-4" id="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user