Merge pull request #6282 from NicolasDorier/refactorcheckout

Allow payment methods to modify all the payment model
This commit is contained in:
Nicolas Dorier
2024-10-07 21:57:00 +09:00
committed by GitHub
51 changed files with 298 additions and 511 deletions

View File

@@ -1,9 +1,11 @@
using System;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Abstractions.Services
{
public class UIExtension : IUIExtension
{
[Obsolete("Use extension method BTCPayServer.Extensions.AddUIExtension(this IServiceCollection services, string location, string partialViewName) instead")]
public UIExtension(string partial, string location)
{
Partial = partial;

View File

@@ -320,10 +320,10 @@ namespace BTCPayServer.Tests
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
//////////////////////
@@ -337,7 +337,7 @@ namespace BTCPayServer.Tests
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id)
.GetAwaiter().GetResult()).Value;
Assert.Equal("Processing", checkout.Status);
});
@@ -475,10 +475,10 @@ namespace BTCPayServer.Tests
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("BTC", checkout.CryptoCode);
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
@@ -536,10 +536,10 @@ namespace BTCPayServer.Tests
});
controller = tester.PayTester.GetController<UIInvoiceController>(null);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
.GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailableCryptos.Count);
Assert.Equal("LTC", checkout.CryptoCode);
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());

View File

@@ -2733,7 +2733,7 @@ namespace BTCPayServer.Tests
Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink);
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
var model = (PaymentModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
var model = (CheckoutModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
Assert.Equal("it-IT", model.DefaultLang);
Assert.Equal("http://toto.com/lol", model.MerchantRefLink);

View File

@@ -3264,6 +3264,7 @@ namespace BTCPayServer.Tests
public async Task CanUseLNAddress()
{
using var s = CreateSeleniumTester();
s.Server.DeleteStore = false;
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
@@ -3416,7 +3417,13 @@ namespace BTCPayServer.Tests
var succ = JsonConvert.DeserializeObject<LNURLPayRequest.LNURLPayRequestCallbackResponse>(str);
Assert.NotNull(succ.Pr);
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
await s.Server.CustomerLightningD.Pay(succ.Pr);
}
// Can we find our comment and address in the payment list?
s.GoToInvoices();
var source = s.Driver.PageSource;
Assert.Contains(lnUsername, source);
}
[Fact]

View File

@@ -609,9 +609,7 @@ retry:
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LN");
var bolt11 = method.Destination;
TestLogs.LogInformation("PAYING");
await parent.CustomerLightningD.Pay(bolt11);
TestLogs.LogInformation("PAID");
await WaitInvoicePaid(invoiceId);
}

View File

@@ -1587,14 +1587,14 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
var checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
Assert.Equal(lnMethod, checkout.PaymentMethodId);
// If we change store's default, it should change the checkout's default
vm.DefaultPaymentMethod = btcMethod;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
.Result);
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<CheckoutModel>();
Assert.Equal(btcMethod, checkout.PaymentMethodId);
}
@@ -1625,7 +1625,7 @@ namespace BTCPayServer.Tests
// validate that invoice data model doesn't have lightning string initially
var res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
var paymentMethodFirst = Assert.IsType<PaymentModel>(
var paymentMethodFirst = Assert.IsType<CheckoutModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
@@ -1641,7 +1641,7 @@ namespace BTCPayServer.Tests
// validate that QR code now has both onchain and offchain payment urls
res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
var paymentMethodUnified = Assert.IsType<PaymentModel>(
var paymentMethodUnified = Assert.IsType<CheckoutModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.StartsWith("bitcoin:bcrt", paymentMethodUnified.InvoiceBitcoinUrl);
@@ -1655,8 +1655,8 @@ namespace BTCPayServer.Tests
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
// But we're proceeding with BECH32 being uppercase
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
Assert.Equal($"bitcoin:{paymentMethodUnified.Address}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
Assert.Equal($"bitcoin:{paymentMethodUnified.Address.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
// Fallback lightning invoice should be uppercase inside the QR code, lowercase in payment URI
var lightningFallback = paymentMethodUnified.InvoiceBitcoinUrl.Split(new[] { "&lightning=" }, StringSplitOptions.None)[1];

View File

@@ -1,6 +1,6 @@
@using BTCPayServer.Payments
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
@inject Dictionary<PaymentMethodId, IPaymentModelExtension> Extensions
@inject Dictionary<PaymentMethodId, ICheckoutModelExtension> Extensions
@{
var state = Model.State.ToString();

View File

@@ -68,7 +68,7 @@ public class StoreRecentInvoices : ViewComponent
Details = new InvoiceDetailsModel
{
Archived = invoice.Archived,
Payments = invoice.GetPayments(false)
Payments = invoice.GetPayments(false)
}
}).ToList();

View File

@@ -128,6 +128,7 @@ namespace BTCPayServer.Controllers
StoreLink = Url.Action(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId = store.Id }),
PaymentRequestLink = Url.Action(nameof(UIPaymentRequestController.ViewPaymentRequest), "UIPaymentRequest", new { payReqId = invoice.Metadata.PaymentRequestId }),
Id = invoice.Id,
Entity = invoice,
State = invoiceState,
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
@@ -554,6 +555,7 @@ namespace BTCPayServer.Controllers
{
Archived = invoice.Archived,
Payments = invoice.GetPayments(false),
Entity = invoice,
CryptoPayments = invoice.GetPaymentPrompts().Select(
data =>
{
@@ -692,7 +694,7 @@ namespace BTCPayServer.Controllers
if (invoiceId is null)
return NotFound();
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
var model = await GetCheckoutModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
if (model == null)
{
// see if the invoice actually exists and is in a state for which we do not display the checkout
@@ -711,22 +713,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet("invoice-noscript")]
public async Task<IActionResult> CheckoutNoScript(string? invoiceId, string? id = null, string? paymentMethodId = null, [FromQuery] string? lang = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
//
if (invoiceId is null)
return NotFound();
var model = await GetInvoiceModel(invoiceId, paymentMethodId is null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
if (model == null)
return NotFound();
return View(model);
}
private async Task<PaymentModel?> GetInvoiceModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang)
private async Task<CheckoutModel?> GetCheckoutModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null)
@@ -739,7 +726,7 @@ namespace BTCPayServer.Controllers
bool isDefaultPaymentId = false;
var storeBlob = store.GetStoreBlob();
var displayedPaymentMethods = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).ToList();
var displayedPaymentMethods = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).ToHashSet();
var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
@@ -835,7 +822,7 @@ namespace BTCPayServer.Controllers
if (prompt is null)
return null;
if (activated)
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
return await GetCheckoutModel(invoiceId, paymentMethodId, lang);
var accounting = prompt.Calculate();
@@ -876,11 +863,11 @@ namespace BTCPayServer.Controllers
}
string ShowMoney(decimal value) => MoneyExtensions.ShowMoney(value, prompt.RateDivisibility ?? prompt.Divisibility);
var model = new PaymentModel
var model = new CheckoutModel
{
Activated = prompt.Activated,
PaymentMethodName = _prettyName.PrettyName(paymentMethodId),
CryptoCode = prompt.Currency,
PaymentMethodCurrency = prompt.Currency,
RootPath = Request.PathBase.Value.WithTrailingSlash(),
OrderId = orderId,
InvoiceId = invoiceId,
@@ -892,9 +879,9 @@ namespace BTCPayServer.Controllers
CelebratePayment = storeBlob.CelebratePayment,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)),
BtcAddress = prompt.Destination,
BtcDue = ShowMoney(accounting.Due),
BtcPaid = ShowMoney(accounting.Paid),
Address = prompt.Destination,
Due = ShowMoney(accounting.Due),
Paid = ShowMoney(accounting.Paid),
InvoiceCurrency = invoice.Currency,
// The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible.
OrderAmount = ShowMoney(accounting.TotalDue - (prompt.PaymentMethodFee - prompt.TweakFee)),
@@ -922,45 +909,31 @@ namespace BTCPayServer.Controllers
Status = invoice.Status.ToString(),
// The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible.
NetworkFee = prompt.PaymentMethodFee - prompt.TweakFee,
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.PaymentMethodId).Concat(new[] { prompt.PaymentMethodId }).Distinct().Count() > 1,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentPrompts()
AvailablePaymentMethods = invoice.GetPaymentPrompts()
.Select(kv =>
{
var handler = _handlers[kv.PaymentMethodId];
return new PaymentModel.AvailableCrypto
return new CheckoutModel.AvailablePaymentMethod
{
Displayed = displayedPaymentMethods.Contains(kv.PaymentMethodId),
PaymentMethodId = kv.PaymentMethodId.ToString(),
CryptoCode = kv.Currency,
PaymentMethodId = kv.PaymentMethodId,
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId),
IsLightning = handler is ILightningPaymentHandler,
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(kv.PaymentMethodId)),
Link = Url.Action(nameof(Checkout),
new
{
invoiceId,
paymentMethodId = kv.PaymentMethodId.ToString()
})
Order = kv.PaymentMethodId switch
{
_ when PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 0,
_ when PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 1,
_ when handler is ILightningPaymentHandler => 2,
_ => 3
}
};
}).Where(c => c.CryptoImage != "/")
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
})
.OrderBy(a => a.Order)
.ToList()
};
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension))
extension.ModifyPaymentModel(new PaymentModelContext(model, store, storeBlob, invoice, Url, prompt, handler));
model.UISettings = _viewProvider.TryGetViewViewModel(prompt, "CheckoutUI")?.View as CheckoutUIPaymentMethodSettings;
model.PaymentMethodId = paymentMethodId.ToString();
model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol);
foreach (var paymentPrompt in invoice.GetPaymentPrompts())
{
var vvm = _viewProvider.TryGetViewViewModel(paymentPrompt, "CheckoutUI");
if (vvm?.View is CheckoutUIPaymentMethodSettings { ExtensionPartial: { } partial })
{
model.ExtensionPartials.Add(partial);
}
}
model.PaymentMethodId = paymentMethodId.ToString();
model.OrderAmountFiat = OrderAmountFromInvoice(model.PaymentMethodCurrency, invoice, DisplayFormatter.CurrencyFormat.Symbol);
if (storeBlob.PlaySoundOnPayment)
{
@@ -973,6 +946,12 @@ namespace BTCPayServer.Controllers
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = expiration.PrettyPrint();
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension) &&
_handlers.TryGetValue(paymentMethodId, out var h))
{
extension.ModifyCheckoutModel(new CheckoutModelContext(model, store, storeBlob, invoice, Url, prompt, h));
}
return model;
}
@@ -1008,7 +987,7 @@ namespace BTCPayServer.Controllers
{
if (string.IsNullOrEmpty(paymentMethodId))
paymentMethodId = implicitPaymentMethodId;
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
var model = await GetCheckoutModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
if (model == null)
return NotFound();
return Json(model);

View File

@@ -62,8 +62,7 @@ namespace BTCPayServer.Controllers
private readonly LinkGenerator _linkGenerator;
private readonly IAuthorizationService _authorizationService;
private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly PaymentMethodViewProvider _viewProvider;
private readonly Dictionary<PaymentMethodId, ICheckoutModelExtension> _paymentModelExtensions;
private readonly PrettyNameProvider _prettyName;
private readonly AppService _appService;
private readonly IFileService _fileService;
@@ -98,8 +97,7 @@ namespace BTCPayServer.Controllers
DefaultRulesCollection defaultRules,
IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
PaymentMethodViewProvider viewProvider,
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
PrettyNameProvider prettyName)
{
_displayFormatter = displayFormatter;
@@ -124,7 +122,6 @@ namespace BTCPayServer.Controllers
_authorizationService = authorizationService;
_transactionLinkProviders = transactionLinkProviders;
_paymentModelExtensions = paymentModelExtensions;
_viewProvider = viewProvider;
_prettyName = prettyName;
_fileService = fileService;
_uriResolver = uriResolver;

View File

@@ -21,13 +21,13 @@ namespace BTCPayServer.Controllers
public class UIPublicLightningNodeInfoController : Controller
{
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly Dictionary<PaymentMethodId, ICheckoutModelExtension> _paymentModelExtensions;
private readonly UriResolver _uriResolver;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly StoreRepository _StoreRepository;
public UIPublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
UriResolver uriResolver,
PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository)

View File

@@ -70,7 +70,7 @@ namespace BTCPayServer.Controllers
private readonly LabelService _labelService;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly DefaultRulesCollection _defaultRules;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly Dictionary<PaymentMethodId, ICheckoutModelExtension> _paymentModelExtensions;
private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly WalletHistogramService _walletHistogramService;
@@ -98,7 +98,7 @@ namespace BTCPayServer.Controllers
LabelService labelService,
DefaultRulesCollection defaultRules,
PaymentMethodHandlerDictionary handlers,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
TransactionLinkProviders transactionLinkProviders)
{
_currencyTable = currencyTable;

View File

@@ -14,6 +14,8 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
@@ -289,6 +291,14 @@ namespace BTCPayServer
}
}
public static IServiceCollection AddUIExtension(this IServiceCollection services, string location, string partialViewName)
{
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IUIExtension>(new UIExtension(partialViewName, location));
#pragma warning restore CS0618 // Type or member is obsolete
return services;
}
public static IServiceCollection AddReportProvider<T>(this IServiceCollection services)
where T : ReportProvider
{

View File

@@ -158,8 +158,7 @@ namespace BTCPayServer.Hosting
//
AddOnchainWalletParsers(services);
services.AddSingleton<IUIExtension>(new UIExtension("Bitcoin/ViewBitcoinLikePaymentData", "store-invoices-payments"));
services.AddSingleton<IUIExtension>(new UIExtension("Lightning/ViewLightningLikePaymentData", "store-invoices-payments"));
services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.AddStartupTask<LoadCurrencyNameTableStartupTask>();
@@ -365,6 +364,11 @@ namespace BTCPayServer.Hosting
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
});
services.AddUIExtension("checkout-end", "Bitcoin/BitcoinLikeMethodCheckout");
services.AddUIExtension("checkout-end", "Lightning/LightningLikeMethodCheckout");
services.AddUIExtension("store-invoices-payments", "Bitcoin/ViewBitcoinLikePaymentData");
services.AddUIExtension("store-invoices-payments", "Lightning/ViewLightningLikePaymentData");
services.AddSingleton<Services.NBXplorerConnectionFactory>();
services.AddSingleton<IHostedService, Services.NBXplorerConnectionFactory>(o => o.GetRequiredService<Services.NBXplorerConnectionFactory>());
services.AddSingleton<HostedServices.CheckConfigurationHostedService>();
@@ -386,8 +390,8 @@ namespace BTCPayServer.Hosting
o.GetRequiredService<IEnumerable<IPaymentMethodBitpayAPIExtension>>().ToDictionary(o => o.PaymentMethodId, o => o));
services.AddSingleton<Dictionary<PaymentMethodId, IPaymentLinkExtension>>(o =>
o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.PaymentMethodId, o => o));
services.AddSingleton<Dictionary<PaymentMethodId, IPaymentModelExtension>>(o =>
o.GetRequiredService<IEnumerable<IPaymentModelExtension>>().ToDictionary(o => o.PaymentMethodId, o => o));
services.AddSingleton<Dictionary<PaymentMethodId, ICheckoutModelExtension>>(o =>
o.GetRequiredService<IEnumerable<ICheckoutModelExtension>>().ToDictionary(o => o.PaymentMethodId, o => o));
services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient)
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
@@ -398,13 +402,11 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<IUIExtension>(new UIExtension("LNURL/LightningAddressNav",
"store-integrations-nav"));
services.AddUIExtension("store-integrations-nav", "LNURL/LightningAddressNav");
services.AddSingleton<IHostedService, LightningListener>();
services.AddSingleton<IHostedService, LightningPendingPayoutListener>();
services.AddSingleton<PaymentMethodHandlerDictionary>();
services.AddSingleton<PaymentMethodViewProvider>();
services.AddSingleton<PayoutMethodHandlerDictionary>();
@@ -627,12 +629,10 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(BitcoinLikePaymentHandler)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinLikePaymentHandler), new object[] { network, pmi }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentModelExtension>(provider =>
(BitcoinPaymentModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentModelExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(BitcoinCheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentMethodBitpayAPIExtension>(provider =>
(IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodBitpayAPIExtension), new object[] { pmi }));
services.AddSingleton<IPaymentMethodViewExtension>(provider =>
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodViewExtension), new object[] { pmi }));
if (!network.ReadonlyWallet && network.WalletSupported)
{
@@ -650,10 +650,8 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(LightningLikePaymentHandler)ActivatorUtilities.CreateInstance(provider, typeof(LightningLikePaymentHandler), new object[] { network, pmi }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentModelExtension>(provider =>
(IPaymentModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentModelExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentMethodViewExtension>(provider =>
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentMethodViewExtension), new object[] { pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(LNCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentMethodBitpayAPIExtension>(provider =>
(IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentMethodBitpayAPIExtension), new object[] { pmi }));
var payoutMethodId = PayoutTypes.LN.GetPayoutMethodId(network.CryptoCode);
@@ -667,10 +665,8 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(LNURLPayPaymentHandler)ActivatorUtilities.CreateInstance(provider, typeof(LNURLPayPaymentHandler), new object[] { network, pmi }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(LNURLPayPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentModelExtension>(provider =>
(IPaymentModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(LNURLPayPaymentModelExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentMethodViewExtension>(provider =>
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(LNURLPaymentMethodViewExtension), new object[] { pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(LNURLCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentMethodBitpayAPIExtension>(provider =>
(IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(LNURLPayPaymentMethodBitpayAPIExtension), new object[] { pmi }));
}

View File

@@ -1,21 +1,24 @@
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.JsonConverters;
using BTCPayServer.Payments;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models.InvoicingModels
{
public class PaymentModel
public class CheckoutModel
{
public CheckoutUIPaymentMethodSettings UISettings;
public class AvailableCrypto
public string CheckoutBodyComponentName { get; set; }
public class AvailablePaymentMethod
{
public string PaymentMethodId { get; set; }
public string CryptoImage { get; set; }
public string Link { get; set; }
public string PaymentMethodName { get; set; }
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
[JsonConverter(typeof(PaymentMethodIdJsonConverter))]
public PaymentMethodId PaymentMethodId { get; set; }
public bool Displayed { get; set; }
public string PaymentMethodName { get; set; }
public int Order { get; set; }
[JsonExtensionData]
public Dictionary<string, JToken> AdditionalData { get; set; } = new();
}
public StoreBrandingViewModel StoreBranding { get; set; }
public string PaymentSoundUrl { get; set; }
@@ -26,15 +29,15 @@ namespace BTCPayServer.Models.InvoicingModels
public string DefaultLang { get; set; }
public bool ShowPayInWalletButton { get; set; }
public bool ShowStoreHeader { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new();
public List<AvailablePaymentMethod> AvailablePaymentMethods { get; set; } = new();
public bool IsModal { get; set; }
public bool IsUnsetTopUp { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
public bool CelebratePayment { get; set; }
public string CryptoCode { get; set; }
public string PaymentMethodCurrency { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
public string BtcDue { get; set; }
public string Address { get; set; }
public string Due { get; set; }
public string CustomerEmail { get; set; }
public bool ShowRecommendedFee { get; set; }
public decimal FeeRate { get; set; }
@@ -53,12 +56,11 @@ namespace BTCPayServer.Models.InvoicingModels
public string InvoiceBitcoinUrlQR { get; set; }
public int TxCount { get; set; }
public int TxCountForFee { get; set; }
public string BtcPaid { get; set; }
public string Paid { get; set; }
public string StoreSupportUrl { get; set; }
public string OrderId { get; set; }
public decimal NetworkFee { get; set; }
public bool IsMultiCurrency { get; set; }
public int MaxTimeMinutes { get; set; }
public string PaymentMethodId { get; set; }
public string PaymentMethodName { get; set; }
@@ -72,7 +74,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string ReceiptLink { get; set; }
public int? RequiredConfirmations { get; set; }
public long? ReceivedConfirmations { get; set; }
public HashSet<string> ExtensionPartials { get; } = new HashSet<string>();
[JsonExtensionData]
public Dictionary<string, JToken> AdditionalData { get; set; } = new();
}
}

View File

@@ -136,5 +136,6 @@ namespace BTCPayServer.Models.InvoicingModels
public bool HasRefund { get; set; }
public bool StillDue { get; set; }
public bool HasRates { get; set; }
public InvoiceEntity Entity { get; internal set; }
}
}

View File

@@ -12,8 +12,9 @@ using NBitcoin.DataEncoders;
namespace BTCPayServer.Payments.Bitcoin
{
public class BitcoinPaymentModelExtension : IPaymentModelExtension
public class BitcoinCheckoutModelExtension : ICheckoutModelExtension
{
public const string CheckoutBodyComponentName = "BitcoinCheckoutBody";
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly BTCPayNetwork _Network;
private readonly DisplayFormatter _displayFormatter;
@@ -22,7 +23,7 @@ namespace BTCPayServer.Payments.Bitcoin
private readonly IPaymentLinkExtension? lnurlPaymentLinkExtension;
private readonly string? _bech32Prefix;
public BitcoinPaymentModelExtension(
public BitcoinCheckoutModelExtension(
PaymentMethodId paymentMethodId,
BTCPayNetwork network,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
@@ -44,18 +45,19 @@ namespace BTCPayServer.Payments.Bitcoin
public string Image => _Network.CryptoImagePath;
public string Badge => "";
public PaymentMethodId PaymentMethodId { get; }
public void ModifyPaymentModel(PaymentModelContext context)
public void ModifyCheckoutModel(CheckoutModelContext context)
{
var prompt = context.Prompt;
if (!_handlers.TryGetValue(PaymentMethodId, out var o) || o is not BitcoinLikePaymentHandler handler)
if (context is not { Handler: BitcoinLikePaymentHandler handler})
return;
var prompt = context.Prompt;
var details = handler.ParsePaymentPromptDetails(prompt.Details);
context.Model.CheckoutBodyComponentName = CheckoutBodyComponentName;
context.Model.ShowRecommendedFee = context.StoreBlob.ShowRecommendedFee;
context.Model.FeeRate = details.RecommendedFeeRate.SatoshiPerByte;
var bip21Case = _Network.SupportLightning && context.StoreBlob.OnChainWithLnInvoiceFallback;
var amountInSats = bip21Case && context.StoreBlob.LightningAmountInSatoshi && context.Model.CryptoCode == "BTC";
var amountInSats = bip21Case && context.StoreBlob.LightningAmountInSatoshi && context.Model.PaymentMethodCurrency == "BTC";
string? lightningFallback = null;
if (context.Model.Activated && bip21Case)
{
@@ -117,10 +119,10 @@ namespace BTCPayServer.Payments.Bitcoin
// model.InvoiceBitcoinUrlQR: bitcoin:bcrt1qxp2qa5dhn7?amount=0.00044007&lightning=LNBCRT4400...
}
if (_Network.CryptoCode.Equals("BTC", StringComparison.InvariantCultureIgnoreCase) && _bech32Prefix is not null && context.Model.BtcAddress.StartsWith(_bech32Prefix, StringComparison.OrdinalIgnoreCase))
if (_Network.CryptoCode.Equals("BTC", StringComparison.InvariantCultureIgnoreCase) && _bech32Prefix is not null && context.Model.Address.StartsWith(_bech32Prefix, StringComparison.OrdinalIgnoreCase))
{
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrlQR.Replace(
$"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.BtcAddress}", $"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.BtcAddress.ToUpperInvariant()}",
$"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.Address}", $"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.Address.ToUpperInvariant()}",
StringComparison.OrdinalIgnoreCase);
// model.InvoiceBitcoinUrlQR: bitcoin:BCRT1QXP2QA5DHN...?amount=0.00044007&lightning=LNBCRT4400...
}
@@ -136,15 +138,15 @@ namespace BTCPayServer.Payments.Bitcoin
}
}
public static void PreparePaymentModelForAmountInSats(PaymentModel model, decimal rate, DisplayFormatter displayFormatter)
public static void PreparePaymentModelForAmountInSats(CheckoutModel model, decimal rate, DisplayFormatter displayFormatter)
{
var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name)
{
NumberFormat = { NumberGroupSeparator = " " }
};
model.CryptoCode = "sats";
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.PaymentMethodCurrency = "sats";
model.Due = Money.Parse(model.Due).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.Paid = Money.Parse(model.Paid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
model.Rate = model.InvoiceCurrency is "BTC" or "SATS"

View File

@@ -1,22 +0,0 @@
namespace BTCPayServer.Payments.Bitcoin
{
public class BitcoinPaymentMethodViewExtension : IPaymentMethodViewExtension
{
public BitcoinPaymentMethodViewExtension(PaymentMethodId paymentMethodId)
{
PaymentMethodId = paymentMethodId;
}
public PaymentMethodId PaymentMethodId { get; }
public void RegisterViews(PaymentMethodViewContext context)
{
context.RegisterCheckoutUI(new CheckoutUIPaymentMethodSettings
{
ExtensionPartial = "Bitcoin/BitcoinLikeMethodCheckout",
CheckoutBodyVueComponentName = "BitcoinLikeMethodCheckout",
CheckoutHeaderVueComponentName = "BitcoinLikeMethodCheckoutHeader",
NoScriptPartialName = "Bitcoin/BitcoinLikeMethodCheckoutNoScript"
});
}
}
}

View File

@@ -4,20 +4,20 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Payments
{
public record PaymentModelContext(
PaymentModel Model,
public record CheckoutModelContext(
CheckoutModel Model,
Data.StoreData Store,
Data.StoreBlob StoreBlob,
InvoiceEntity InvoiceEntity,
IUrlHelper UrlHelper,
PaymentPrompt Prompt,
IPaymentMethodHandler Handler);
public interface IPaymentModelExtension
public interface ICheckoutModelExtension
{
public PaymentMethodId PaymentMethodId { get; }
string DisplayName { get; }
string Image { get; }
string Badge { get; }
void ModifyPaymentModel(PaymentModelContext context);
void ModifyCheckoutModel(CheckoutModelContext context);
}
}

View File

@@ -1,70 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments
{
public interface IPaymentMethodViewExtension
{
PaymentMethodId PaymentMethodId { get; }
void RegisterViews(PaymentMethodViewContext context);
}
public record ViewViewModel(object View, object ViewModel);
public class PaymentMethodViewProvider
{
private readonly Dictionary<PaymentMethodId, IPaymentMethodViewExtension> _extensions;
private readonly PaymentMethodHandlerDictionary _handlers;
public PaymentMethodViewProvider(
IEnumerable<IPaymentMethodViewExtension> extensions,
PaymentMethodHandlerDictionary handlers)
{
_extensions = extensions.ToDictionary(o => o.PaymentMethodId, o => o);
_handlers = handlers;
}
public ViewViewModel? TryGetViewViewModel(PaymentPrompt paymentPrompt, string key)
{
if (!_extensions.TryGetValue(paymentPrompt.PaymentMethodId, out var extension))
return null;
if (!_handlers.TryGetValue(paymentPrompt.PaymentMethodId, out var handler) || paymentPrompt.Details is null)
return null;
var ctx = new PaymentMethodViewContext()
{
Details = handler.ParsePaymentPromptDetails(paymentPrompt.Details)
};
extension.RegisterViews(ctx);
object? view = null;
if (!ctx._Views.TryGetValue(key, out view))
return null;
return new ViewViewModel(view, handler.ParsePaymentPromptDetails(paymentPrompt.Details));
}
}
public class PaymentMethodViewContext
{
internal Dictionary<string, object> _Views = new Dictionary<string, object>();
public object? Details { get; internal set; }
public void RegisterPaymentMethodDetails(string partialName)
{
_Views.Add("AdditionalPaymentMethodDetails", partialName);
}
public void RegisterCheckoutUI(CheckoutUIPaymentMethodSettings settings)
{
_Views.Add("CheckoutUI", settings);
}
public void Register(string key, object value)
{
_Views.Add(key, value);
}
}
public class CheckoutUIPaymentMethodSettings
{
public string? ExtensionPartial { get; set; }
public string? CheckoutBodyVueComponentName { get; set; }
public string? CheckoutHeaderVueComponentName { get; set; }
public string? NoScriptPartialName { get; set; }
}
}

View File

@@ -8,9 +8,9 @@ using NBitcoin;
namespace BTCPayServer.Payments.LNURLPay
{
public class LNURLPayPaymentModelExtension : IPaymentModelExtension
public class LNURLCheckoutModelExtension : ICheckoutModelExtension
{
public LNURLPayPaymentModelExtension(
public LNURLCheckoutModelExtension(
PaymentMethodId paymentMethodId,
BTCPayNetwork network,
DisplayFormatter displayFormatter,
@@ -37,19 +37,22 @@ namespace BTCPayServer.Payments.LNURLPay
public string Badge => "⚡";
private const string UriScheme = "lightning:";
public void ModifyPaymentModel(PaymentModelContext context)
public void ModifyCheckoutModel(CheckoutModelContext context)
{
if (context is not { Handler: LNURLPayPaymentHandler handler })
return;
var lnurl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
if (lnurl is not null)
{
context.Model.BtcAddress = lnurl.Replace(UriScheme, "");
context.Model.Address = lnurl.Replace(UriScheme, "");
context.Model.InvoiceBitcoinUrl = lnurl;
context.Model.InvoiceBitcoinUrlQR = lnurl.ToUpperInvariant().Replace(UriScheme.ToUpperInvariant(), UriScheme);
}
context.Model.CheckoutBodyComponentName = LNCheckoutModelExtension.CheckoutBodyComponentName;
context.Model.PeerInfo = handler.ParsePaymentPromptDetails(context.Prompt.Details).NodeInfo;
if (context.StoreBlob.LightningAmountInSatoshi && context.Model.CryptoCode == "BTC")
if (context.StoreBlob.LightningAmountInSatoshi && context.Model.PaymentMethodCurrency == "BTC")
{
BitcoinPaymentModelExtension.PreparePaymentModelForAmountInSats(context.Model, context.Prompt.Rate, _displayFormatter);
BitcoinCheckoutModelExtension.PreparePaymentModelForAmountInSats(context.Model, context.Prompt.Rate, _displayFormatter);
}
}
}

View File

@@ -1,26 +0,0 @@
namespace BTCPayServer.Payments.LNURLPay
{
public class LNURLPaymentMethodViewExtension : IPaymentMethodViewExtension
{
public LNURLPaymentMethodViewExtension(PaymentMethodId paymentMethodId)
{
PaymentMethodId = paymentMethodId;
}
public PaymentMethodId PaymentMethodId { get; }
public void RegisterViews(PaymentMethodViewContext context)
{
var details = context.Details;
if (details is not LNURLPayPaymentMethodDetails d)
return;
context.RegisterPaymentMethodDetails("LNURL/AdditionalPaymentMethodDetails");
context.RegisterCheckoutUI(new CheckoutUIPaymentMethodSettings()
{
ExtensionPartial = "Lightning/LightningLikeMethodCheckout",
CheckoutBodyVueComponentName = "LightningLikeMethodCheckout",
CheckoutHeaderVueComponentName = "LightningLikeMethodCheckoutHeader",
NoScriptPartialName = "Lightning/LightningLikeMethodCheckoutNoScript"
});
}
}
}

View File

@@ -9,11 +9,12 @@ using System.Linq;
namespace BTCPayServer.Payments.Lightning
{
public class LightningPaymentModelExtension : IPaymentModelExtension
public class LNCheckoutModelExtension : ICheckoutModelExtension
{
public const string CheckoutBodyComponentName = "LightningCheckoutBody";
private readonly DisplayFormatter _displayFormatter;
IPaymentLinkExtension _PaymentLinkExtension;
public LightningPaymentModelExtension(
public LNCheckoutModelExtension(
PaymentMethodId paymentMethodId,
BTCPayNetwork network,
DisplayFormatter displayFormatter,
@@ -37,20 +38,21 @@ namespace BTCPayServer.Payments.Lightning
public string Image => Network.LightningImagePath;
public string Badge => "⚡";
public void ModifyPaymentModel(PaymentModelContext context)
public void ModifyCheckoutModel(CheckoutModelContext context)
{
if (!Handlers.TryGetValue(PaymentMethodId, out var o) || o is not LightningLikePaymentHandler handler)
if (context is not { Handler: LightningLikePaymentHandler handler })
return;
var paymentPrompt = context.InvoiceEntity.GetPaymentPrompt(PaymentMethodId);
if (paymentPrompt is null)
return;
context.Model.CheckoutBodyComponentName = CheckoutBodyComponentName;
context.Model.InvoiceBitcoinUrl = _PaymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
if (context.Model.InvoiceBitcoinUrl is not null)
context.Model.InvoiceBitcoinUrlQR = $"lightning:{context.Model.InvoiceBitcoinUrl.ToUpperInvariant()?.Substring("LIGHTNING:".Length)}";
context.Model.PeerInfo = handler.ParsePaymentPromptDetails(paymentPrompt.Details).NodeInfo;
if (context.StoreBlob.LightningAmountInSatoshi && context.Model.CryptoCode == "BTC")
if (context.StoreBlob.LightningAmountInSatoshi && context.Model.PaymentMethodCurrency == "BTC")
{
BitcoinPaymentModelExtension.PreparePaymentModelForAmountInSats(context.Model, paymentPrompt.Rate, _displayFormatter);
BitcoinCheckoutModelExtension.PreparePaymentModelForAmountInSats(context.Model, paymentPrompt.Rate, _displayFormatter);
}
}
}

View File

@@ -21,9 +21,9 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning
{
public interface ILightningPaymentHandler : IHasNetwork
public interface ILightningPaymentHandler : IHasNetwork, IPaymentMethodHandler
{
LightningPaymentData ParsePaymentDetails(JToken details);
new LightningPaymentData ParsePaymentDetails(JToken details);
}
public class LightningLikePaymentHandler : IPaymentMethodHandler, ILightningPaymentHandler
{

View File

@@ -1,22 +0,0 @@
namespace BTCPayServer.Payments.Lightning
{
public class LightningPaymentMethodViewExtension : IPaymentMethodViewExtension
{
public LightningPaymentMethodViewExtension(PaymentMethodId paymentMethodId)
{
PaymentMethodId = paymentMethodId;
}
public PaymentMethodId PaymentMethodId { get; }
public void RegisterViews(PaymentMethodViewContext context)
{
context.RegisterCheckoutUI(new CheckoutUIPaymentMethodSettings()
{
ExtensionPartial = "Lightning/LightningLikeMethodCheckout",
CheckoutBodyVueComponentName = "LightningLikeMethodCheckout",
CheckoutHeaderVueComponentName = "LightningLikeMethodCheckoutHeader",
NoScriptPartialName = "Lightning/LightningLikeMethodCheckoutNoScript"
});
}
}
}

View File

@@ -42,8 +42,6 @@ public partial class AltcoinsPlugin
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("XMR");
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink));
services.AddSingleton<IPaymentMethodViewExtension>(provider =>
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodViewExtension), new object[] { pmi }));
services.AddSingleton(provider =>
@@ -69,12 +67,12 @@ public partial class AltcoinsPlugin
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), new object[] { network }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentModelExtension>(provider =>
(IPaymentModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroPaymentModelExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<IUIExtension>(new UIExtension("Monero/StoreNavMoneroExtension", "store-nav"));
services.AddSingleton<IUIExtension>(new UIExtension("Monero/StoreWalletsNavMoneroExtension", "store-wallets-nav"));
services.AddSingleton<IUIExtension>(new UIExtension("Monero/ViewMoneroLikePaymentData", "store-invoices-payments"));
services.AddUIExtension("store-nav", "Monero/StoreNavMoneroExtension");
services.AddUIExtension("store-wallets-nav", "Monero/StoreWalletsNavMoneroExtension");
services.AddUIExtension("store-invoices-payments", "Monero/ViewMoneroLikePaymentData");
services.AddSingleton<ISyncSummaryProvider, MoneroSyncSummaryProvider>();
}
private static MoneroLikeConfiguration ConfigureMoneroLikeConfiguration(IServiceProvider serviceProvider)

View File

@@ -43,8 +43,6 @@ public partial class AltcoinsPlugin
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("ZEC");
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink));
services.AddSingleton<IPaymentMethodViewExtension>(provider =>
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodViewExtension), new object[] { pmi }));
services.AddSingleton(provider =>
@@ -58,13 +56,13 @@ public partial class AltcoinsPlugin
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(ZcashLikePaymentMethodHandler), new object[] { network }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(ZcashPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<IPaymentModelExtension>(provider =>
(IPaymentModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(ZcashPaymentModelExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(ZcashCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<ZcashLikePaymentMethodHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetRequiredService<ZcashLikePaymentMethodHandler>());
services.AddSingleton<IUIExtension>(new UIExtension("Zcash/StoreNavZcashExtension", "store-nav"));
services.AddSingleton<IUIExtension>(new UIExtension("Zcash/ViewZcashLikePaymentData", "store-invoices-payments"));
services.AddUIExtension("store-nav", "Zcash/StoreNavZcashExtension");
services.AddUIExtension("store-invoices-payments", "Zcash/ViewZcashLikePaymentData");
services.AddSingleton<ISyncSummaryProvider, ZcashSyncSummaryProvider>();
}

View File

@@ -36,7 +36,7 @@ namespace BTCPayServer.Plugins.Crowdfund
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("Crowdfund/NavExtension", "header-nav"));
services.AddUIExtension("header-nav", "Crowdfund/NavExtension");
services.AddSingleton<CrowdfundAppType>();
services.AddSingleton<AppBaseType, CrowdfundAppType>();

View File

@@ -15,12 +15,9 @@ namespace BTCPayServer.Plugins.NFC
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/LNURLNFCPostContent",
"checkout-bitcoin-post-content"));
applicationBuilder.AddUIExtension("checkout-end", "NFC/CheckoutEnd");
applicationBuilder.AddUIExtension("checkout-lightning-post-content", "NFC/LNURLNFCPostContent");
applicationBuilder.AddUIExtension("checkout-bitcoin-post-content", "NFC/LNURLNFCPostContent");
base.Execute(applicationBuilder);
}
}

View File

@@ -13,7 +13,7 @@ namespace BTCPayServer.Plugins.PayButton
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("PayButton/NavExtension", "header-nav"));
services.AddUIExtension("header-nav", "PayButton/NavExtension");
base.Execute(services);
}
}

View File

@@ -28,7 +28,7 @@ namespace BTCPayServer.Plugins.PointOfSale
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("PointOfSale/NavExtension", "header-nav"));
services.AddUIExtension("header-nav", "PointOfSale/NavExtension");
services.AddSingleton<AppBaseType, PointOfSaleAppType>();
base.Execute(services);
}

View File

@@ -16,7 +16,7 @@ namespace BTCPayServer.Plugins.Shopify
{
applicationBuilder.AddSingleton<ShopifyService>();
applicationBuilder.AddSingleton<IHostedService, ShopifyService>(provider => provider.GetRequiredService<ShopifyService>());
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Shopify/NavExtension", "header-nav"));
applicationBuilder.AddUIExtension("header-nav", "Shopify/NavExtension");
base.Execute(applicationBuilder);
}
}

View File

@@ -2,18 +2,19 @@ using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Altcoins.Monero.Services;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Altcoins.Monero.Payments
{
public class MoneroPaymentModelExtension : IPaymentModelExtension
public class MoneroCheckoutModelExtension : ICheckoutModelExtension
{
private readonly BTCPayNetworkBase _network;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IPaymentLinkExtension paymentLinkExtension;
public MoneroPaymentModelExtension(
public MoneroCheckoutModelExtension(
PaymentMethodId paymentMethodId,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
BTCPayNetworkBase network,
@@ -31,22 +32,23 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
public string Image => _network.CryptoImagePath;
public string Badge => "";
public void ModifyPaymentModel(PaymentModelContext context)
public void ModifyCheckoutModel(CheckoutModelContext context)
{
if (context is not { Handler: MoneroLikePaymentMethodHandler handler })
return;
context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName;
if (context.Model.Activated)
{
if (_handlers.TryGetValue(PaymentMethodId, out var handler))
var details = context.InvoiceEntity.GetPayments(true)
.Select(p => p.GetDetails<MoneroLikePaymentData>(handler))
.Where(p => p is not null)
.FirstOrDefault();
if (details is not null)
{
var details = context.InvoiceEntity.GetPayments(true)
.Select(p => p.GetDetails<MoneroLikePaymentData>(handler))
.Where(p => p is not null)
.FirstOrDefault();
if (details is not null)
{
context.Model.ReceivedConfirmations = details.ConfirmationCount;
context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy);
}
context.Model.ReceivedConfirmations = details.ConfirmationCount;
context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy);
}
context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl;
}

View File

@@ -108,17 +108,6 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
return details.ToObject<MoneroLikeOnChainPaymentMethodDetails>(Serializer);
}
public CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
{
return new CheckoutUIPaymentMethodSettings
{
ExtensionPartial = "Bitcoin/BitcoinLikeMethodCheckout",
CheckoutBodyVueComponentName = "BitcoinLikeMethodCheckout",
CheckoutHeaderVueComponentName = "BitcoinLikeMethodCheckoutHeader",
NoScriptPartialName = "Bitcoin/BitcoinLikeMethodCheckoutNoScript"
};
}
public MoneroLikePaymentData ParsePaymentDetails(JToken details)
{
return details.ToObject<MoneroLikePaymentData>(Serializer) ?? throw new FormatException($"Invalid {nameof(MoneroLikePaymentMethodHandler)}");

View File

@@ -1,18 +1,20 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Zcash.Services;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{
public class ZcashPaymentModelExtension : IPaymentModelExtension
public class ZcashCheckoutModelExtension : ICheckoutModelExtension
{
private readonly BTCPayNetworkBase _network;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IPaymentLinkExtension paymentLinkExtension;
public ZcashPaymentModelExtension(
public ZcashCheckoutModelExtension(
PaymentMethodId paymentMethodId,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
BTCPayNetworkBase network,
@@ -30,21 +32,21 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
public string Image => _network.CryptoImagePath;
public string Badge => "";
public void ModifyPaymentModel(PaymentModelContext context)
public void ModifyCheckoutModel(CheckoutModelContext context)
{
if (context.Model.Activated)
if (context is not { Handler: ZcashLikePaymentMethodHandler handler })
return;
context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName;
if (context.Model.Activated)
{
if (_handlers.TryGetValue(PaymentMethodId, out var handler))
var details = context.InvoiceEntity.GetPayments(true)
.Select(p => p.GetDetails<ZcashLikePaymentData>(handler))
.Where(p => p is not null)
.FirstOrDefault();
if (details is not null)
{
var details = context.InvoiceEntity.GetPayments(true)
.Select(p => p.GetDetails<ZcashLikePaymentData>(handler))
.Where(p => p is not null)
.FirstOrDefault();
if (details is not null)
{
context.Model.ReceivedConfirmations = details.ConfirmationCount;
context.Model.RequiredConfirmations = (int)ZcashListener.ConfirmationsRequired(context.InvoiceEntity.SpeedPolicy);
}
context.Model.ReceivedConfirmations = details.ConfirmationCount;
context.Model.RequiredConfirmations = (int)ZcashListener.ConfirmationsRequired(context.InvoiceEntity.SpeedPolicy);
}
context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl;

View File

@@ -102,17 +102,6 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
public long AccountIndex { get; internal set; }
}
public CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
{
return new CheckoutUIPaymentMethodSettings
{
ExtensionPartial = "Bitcoin/BitcoinLikeMethodCheckout",
CheckoutBodyVueComponentName = "BitcoinLikeMethodCheckout",
CheckoutHeaderVueComponentName = "BitcoinLikeMethodCheckoutHeader",
NoScriptPartialName = "Bitcoin/BitcoinLikeMethodCheckoutNoScript"
};
}
public ZcashLikePaymentData ParsePaymentDetails(JToken details)
{
return details.ToObject<ZcashLikePaymentData>(Serializer) ?? throw new FormatException($"Invalid {nameof(ZcashLikePaymentData)}");

View File

@@ -6,9 +6,9 @@ namespace BTCPayServer.Services
{
public class PrettyNameProvider
{
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _extensions;
private readonly Dictionary<PaymentMethodId, ICheckoutModelExtension> _extensions;
public PrettyNameProvider(Dictionary<PaymentMethodId, IPaymentModelExtension> extensions)
public PrettyNameProvider(Dictionary<PaymentMethodId, ICheckoutModelExtension> extensions)
{
_extensions = extensions;
}

View File

@@ -1,9 +1,10 @@
@using BTCPayServer.BIP78.Sender
@using BTCPayServer.Components.TruncateCenter
@using BTCPayServer.Abstractions.TagHelpers
@model BTCPayServer.Models.InvoicingModels.PaymentModel
@using BTCPayServer.Payments.Bitcoin
@model BTCPayServer.Models.InvoicingModels.CheckoutModel
<template id="bitcoin-method-checkout-template">
<template id="@BitcoinCheckoutModelExtension.CheckoutBodyComponentName">
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-bitcoin-pre-content", model = Model})
<div class="payment-box">
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.invoiceBitcoinUrl" data-clipboard-confirm-element="#Address_@Model.PaymentMethodId [data-clipboard]">
@@ -12,9 +13,9 @@
</div>
<img class="qr-icon" :src="model.cryptoImage" :alt="model.paymentMethodName"/>
</div>
<div v-if="model.btcAddress" class="input-group mt-3">
<div v-if="model.address" class="input-group mt-3">
<div class="form-floating" id="Address_@Model.PaymentMethodId">
<vc:truncate-center text="model.btcAddress" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<vc:truncate-center text="model.address" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label v-t="{ path: 'address', args: { paymentMethod: model.paymentMethodName }}"></label>
</div>
</div>
@@ -31,9 +32,9 @@
</template>
<script>
Vue.component('BitcoinLikeMethodCheckout', {
Vue.component(@Safe.Json(BitcoinCheckoutModelExtension.CheckoutBodyComponentName), {
props: ['model', 'nfcSupported', 'nfcScanning', 'nfcErrorMessage'],
template: "#bitcoin-method-checkout-template",
template: @Safe.Json("#" + BitcoinCheckoutModelExtension.CheckoutBodyComponentName),
components: {
qrcode: VueQrcode
},

View File

@@ -1,8 +0,0 @@
@model PaymentModel
<div>
<p>To complete payment, please send <b>@(Model.IsUnsetTopUp ? "any amount of" : Model.BtcDue) @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
<p>Time remaining: @Model.TimeLeft</p>
<p>
<a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;" rel="noreferrer noopener">@Model.InvoiceBitcoinUrl</a>
</p>
</div>

View File

@@ -7,10 +7,10 @@
@inject DisplayFormatter DisplayFormatter
@inject TransactionLinkProviders TransactionLinkProviders
@inject PaymentMethodHandlerDictionary handlers
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@model InvoiceDetailsModel
@{
PayjoinInformation payjoinInformation = null;
var payments = Model
var payments = Model.Payments
.Select(payment =>
{
if (!handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not BitcoinLikePaymentHandler handler)

View File

@@ -1,18 +0,0 @@
@model BTCPayServer.Payments.LNURLPayPaymentMethodDetails
@if (!string.IsNullOrEmpty(Model.ProvidedComment))
{
<tr>
<td colspan="100% bg-tile">
LNURL Comment: @Model.ProvidedComment
</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.ConsumedLightningAddress))
{
<tr>
<td colspan="100% bg-tile">
Lightning address used: @Model.ConsumedLightningAddress
</td>
</tr>
}

View File

@@ -1,6 +1,7 @@
@model BTCPayServer.Models.InvoicingModels.PaymentModel
@using BTCPayServer.Payments.Lightning
@model BTCPayServer.Models.InvoicingModels.CheckoutModel
<template id="lightning-method-checkout-template">
<template id="@LNCheckoutModelExtension.CheckoutBodyComponentName">
<div class="payment-box">
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-lightning-pre-content", model = Model})
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.invoiceBitcoinUrl" data-clipboard-confirm-element="#Lightning_@Model.PaymentMethodId [data-clipboard]">
@@ -9,23 +10,23 @@
</div>
<img class="qr-icon" :src="model.cryptoImage" :alt="model.paymentMethodName"/>
</div>
<div v-if="model.btcAddress" class="input-group mt-3">
<div v-if="model.address" class="input-group mt-3">
<div class="form-floating" id="Lightning_@Model.PaymentMethodId">
<vc:truncate-center text="model.btcAddress" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<vc:truncate-center text="model.address" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label v-t="'lightning'"></label>
</div>
</div>
<a v-if="model.invoiceBitcoinUrl && model.showPayInWalletButton" class="btn btn-primary rounded-pill w-100 mt-4" id="PayInWallet" target="_blank"
:href="model.invoiceBitcoinUrl" v-t="'pay_in_wallet'"></a>
<div v-if="!model.invoiceBitcoinUrl && !model.btcAddress" class="alert alert-danger">This payment method is not available when using an insecure connection. Please use HTTPS or Tor.</div>
<div v-if="!model.invoiceBitcoinUrl && !model.address" class="alert alert-danger">This payment method is not available when using an insecure connection. Please use HTTPS or Tor.</div>
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-lightning-post-content", model = Model})
</div>
</template>
<script>
Vue.component('LightningLikeMethodCheckout', {
Vue.component(@Safe.Json(LNCheckoutModelExtension.CheckoutBodyComponentName), {
props: ['model', 'nfcSupported', 'nfcScanning', 'nfcErrorMessage'],
template: "#lightning-method-checkout-template",
template: @Safe.Json("#" + LNCheckoutModelExtension.CheckoutBodyComponentName),
components: {
qrcode: VueQrcode
},

View File

@@ -1,12 +0,0 @@
@model PaymentModel
<div>
<p>To complete payment, please send <b>@Model.BtcDue @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
<p>Time remaining: @Model.TimeLeft</p>
<p>
<a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;" rel="noreferrer noopener">@Model.InvoiceBitcoinUrl</a>
</p>
@if (!string.IsNullOrEmpty(Model.PeerInfo))
{
<p>Peer Info: <b>@Model.PeerInfo</b></p>
}
</div>

View File

@@ -7,16 +7,31 @@
@inject DisplayFormatter DisplayFormatter
@inject PaymentMethodHandlerDictionary handlers
@inject PrettyNameProvider prettyName
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@model InvoiceDetailsModel
@{
string providedComment = null;
string consumedLightningAddress = null;
var payments = Model
.Payments
.Select(payment =>
{
if (handlers.TryGet(payment.PaymentMethodId) is not ILightningPaymentHandler handler)
return null;
var offChainPaymentData = handler.ParsePaymentDetails(payment.Details);
if (handler.ParsePaymentPromptDetails(Model.Entity.GetPaymentPrompt(payment.PaymentMethodId)?.Details) is LNURLPayPaymentMethodDetails lnurlPrompt)
{
if (lnurlPrompt.ConsumedLightningAddress is string consumed)
{
consumedLightningAddress = consumed;
}
if (lnurlPrompt.ProvidedComment is string comment)
{
providedComment = comment;
}
}
return new OffChainPaymentViewModel
{
Type = prettyName.PrettyName(payment.PaymentMethodId),
@@ -25,8 +40,8 @@
Amount = DisplayFormatter.Currency(payment.Value, handler.Network.CryptoCode, divisibility: payment.Divisibility)
};
})
.Where(model => model != null)
.ToList();
.Where(model => model != null)
.ToList();
}
@if (payments.Any())
@@ -37,7 +52,7 @@
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="w-75px">Type</th>
<th class="w-175px">Type</th>
<th class="w-175px">Destination</th>
<th class="text-nowrap">Payment Proof</th>
<th class="w-150px text-end">Paid</th>
@@ -61,6 +76,19 @@
}
</tbody>
</table>
@if (!string.IsNullOrEmpty(providedComment))
{
<div>
<b>LNURL Comment</b>: @providedComment
</div>
}
@if (!string.IsNullOrEmpty(consumedLightningAddress))
{
<div>
<b>Lightning address used</b>: @consumedLightningAddress
</div>
}
</div>
</section>
}

View File

@@ -19,7 +19,7 @@ Vue.component("lnurl-withdraw-checkout", {
const {
onChainWithLnInvoiceFallback: isUnified,
paymentMethodId: activePaymentMethodId,
availableCryptos: availablePaymentMethods,
availablePaymentMethods: availablePaymentMethods,
invoiceBitcoinUrl: paymentUrl
} = this.model
const lnurlwAvailable =

View File

@@ -1,4 +1,4 @@
@model PaymentModel
@model CheckoutModel
<style>
#checkout-cheating form + form { margin-top: var(--btcpay-space-l); }
@@ -52,14 +52,14 @@
paying: false,
mining: false,
expiring: false,
amountRemaining: this.btcDue
amountRemaining: this.due
}
},
props: {
invoiceId: String,
paymentMethodId: String,
cryptoCode: String,
btcDue: Number,
due: Number,
isProcessing: Boolean,
isSettled: Boolean
},

View File

@@ -1,16 +1,16 @@
@model PaymentModel
@model CheckoutModel
<div id="testing">
<hr class="my-3" />
<p class="alert alert-danger" style="display: none; word-break: break-all;"></p>
<p class="alert alert-success" style="display: none; word-break: break-all;"></p>
<form id="test-payment" action="/i/@Model.InvoiceId/test-payment" method="post" class="cheat-mode form-inline my-2">
<input name="CryptoCode" type="hidden" value="@Model.CryptoCode">
<input name="CryptoCode" type="hidden" value="@Model.PaymentMethodCurrency">
<div class="form-group mb-1">
<label for="test-payment-amount" class="control-label">{{$t("Fake a @Model.CryptoCode payment for testing")}}</label>
<label for="test-payment-amount" class="control-label">{{$t("Fake a @Model.PaymentMethodCurrency payment for testing")}}</label>
<div class="input-group">
<input id="test-payment-amount" name="Amount" type="number" step="0.00000001" min="0" class="form-control" placeholder="Amount" value="@Model.BtcDue" />
<div id="test-payment-crypto-code" class="input-group-addon">@Model.CryptoCode</div>
<input id="test-payment-amount" name="Amount" type="number" step="0.00000001" min="0" class="form-control" placeholder="Amount" value="@Model.Due" />
<div id="test-payment-crypto-code" class="input-group-addon">@Model.PaymentMethodCurrency</div>
</div>
</div>
<button id="FakePayment" class="btn btn-primary" type="submit">{{$t("Fake Payment")}}</button>

View File

@@ -5,19 +5,19 @@
@inject IEnumerable<IUIExtension> UiExtensions
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@model PaymentModel
@model CheckoutModel
@{
Layout = null;
ViewData["Title"] = Model.HtmlTitle;
ViewData["StoreBranding"] = Model.StoreBranding;
Csp.UnsafeEval();
var hasPaymentPlugins = UiExtensions.Any(extension => extension.Location == "checkout-payment-method");
var displayedPaymentMethods = Model.AvailableCryptos.Where(c => c.Displayed).ToList();
var displayedPaymentMethods = Model.AvailablePaymentMethods.Where(c => c.Displayed).ToList();
}
@functions {
private string ToJsValue(object value)
{
return Safe.Json(value).ToString()?.Replace("\"", "'");
return Safe.Json(value?.ToString()).ToString()?.Replace("\"", "'");
}
}
<!DOCTYPE html>
@@ -60,7 +60,7 @@
}
else
{
<h2 id="AmountDue" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`" :data-clipboard="asNumber(srvModel.btcDue)" data-clipboard-hover :data-amount-due="srvModel.btcDue">@Model.BtcDue @Model.CryptoCode</h2>
<h2 id="AmountDue" v-text="`${srvModel.due} ${srvModel.paymentMethodCurrency}`" :data-clipboard="asNumber(srvModel.due)" data-clipboard-hover :data-amount-due="srvModel.due">@Model.Due @Model.PaymentMethodCurrency</h2>
}
</div>
<div id="PaymentInfo" class="info mt-3 mb-2" v-collapsible="showInfo">
@@ -73,7 +73,7 @@
<vc:icon symbol="info" />
<span v-t="'partial_payment_info'"></span>
</div>
<div v-if="showPaymentDueInfo" v-html="replaceNewlines($t('still_due', { amount: `${srvModel.btcDue} ${srvModel.cryptoCode}` }))"></div>
<div v-if="showPaymentDueInfo" v-html="replaceNewlines($t('still_due', { amount: `${srvModel.due} ${srvModel.paymentMethodCurrency}` }))"></div>
</div>
</div>
<button id="DetailsToggle" class="d-flex align-items-center gap-1 btn btn-link payment-details-button mb-2" type="button" :aria-expanded="displayPaymentDetails ? 'true' : 'false'" v-on:click="displayPaymentDetails = !displayPaymentDetails">
@@ -85,8 +85,8 @@
:srv-model="srvModel"
:is-active="isActive"
:order-amount="orderAmount"
:btc-paid="btcPaid"
:btc-due="btcDue"
:paid="paid"
:due="due"
:show-recommended-fee="showRecommendedFee"
class="pb-4" />
</div>
@@ -108,7 +108,7 @@
</div>
</div>
}
<component v-if="paymentMethodComponent" :is="paymentMethodComponent"
<component v-if="paymentMethodComponent" :is="paymentMethodComponent"
:model="srvModel"
:nfc-scanning="nfc.scanning"
:nfc-supported="nfc.supported"
@@ -127,7 +127,7 @@
</span>
<h4 v-t="'payment_received'" class="mb-4"></h4>
<p class="text-center" v-t="'payment_received_body'"></p>
<p class="text-center" v-if="srvModel.receivedConfirmations !== null && srvModel.requiredConfirmations" v-t="{ path: 'payment_received_confirmations', args: { cryptoCode: realCryptoCode, receivedConfirmations: srvModel.receivedConfirmations, requiredConfirmations: srvModel.requiredConfirmations } }"></p>
<p class="text-center" v-if="srvModel.receivedConfirmations !== null && srvModel.requiredConfirmations" v-t="{ path: 'payment_received_confirmations', args: { cryptoCode: realPaymentMethodCurrency, receivedConfirmations: srvModel.receivedConfirmations, requiredConfirmations: srvModel.requiredConfirmations } }"></p>
<div id="PaymentDetails" class="payment-details">
<dl class="mb-0">
<div>
@@ -143,8 +143,8 @@
:srv-model="srvModel"
:is-active="isActive"
:order-amount="orderAmount"
:btc-paid="btcPaid"
:btc-due="btcDue"
:paid="paid"
:due="due"
:show-recommended-fee="showRecommendedFee"
v-collapsible="displayPaymentDetails" />
</div>
@@ -180,8 +180,8 @@
:srv-model="srvModel"
:is-active="isActive"
:order-amount="orderAmount"
:btc-paid="btcPaid"
:btc-due="btcDue"
:paid="paid"
:due="due"
:show-recommended-fee="showRecommendedFee"
class="mb-5" />
</div>
@@ -213,8 +213,8 @@
:srv-model="srvModel"
:is-active="isActive"
:order-amount="orderAmount"
:btc-paid="btcPaid"
:btc-due="btcDue"
:paid="paid"
:due="due"
:show-recommended-fee="showRecommendedFee"
v-collapsible="displayPaymentDetails" />
</div>
@@ -234,7 +234,7 @@
</main>
@if (Env.CheatMode)
{
<checkout-cheating invoice-id="@Model.InvoiceId" :btc-due="btcDue" :is-settled="isSettled" :is-processing="isProcessing" :payment-method-id="pmId" :crypto-code="srvModel.cryptoCode"></checkout-cheating>
<checkout-cheating invoice-id="@Model.InvoiceId" :due="due" :is-settled="isSettled" :is-processing="isProcessing" :payment-method-id="pmId" :crypto-code="srvModel.paymentMethodCurrency"></checkout-cheating>
}
<footer class="store-footer">
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
@@ -257,33 +257,33 @@
<dl>
<div v-if="orderAmount > 0" id="PaymentDetails-TotalPrice" key="TotalPrice">
<dt v-t="'total_price'"></dt>
<dd :data-clipboard="asNumber(srvModel.orderAmount)" data-clipboard-hover="start">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</dd>
<dd :data-clipboard="asNumber(srvModel.orderAmount)" data-clipboard-hover="start">{{srvModel.orderAmount}} {{ srvModel.paymentMethodCurrency }}</dd>
</div>
<div v-if="orderAmount > 0 && srvModel.orderAmountFiat" id="PaymentDetails-TotalFiat" key="TotalFiat">
<dt v-t="'total_fiat'"></dt>
<dd :data-clipboard="asNumber(srvModel.orderAmountFiat)" data-clipboard-hover="start">{{srvModel.orderAmountFiat}}</dd>
</div>
<div v-if="srvModel.rate && srvModel.cryptoCode" id="PaymentDetails-ExchangeRate" key="ExchangeRate">
<div v-if="srvModel.rate && srvModel.paymentMethodCurrency" id="PaymentDetails-ExchangeRate" key="ExchangeRate">
<dt v-t="'exchange_rate'"></dt>
<dd :data-clipboard="asNumber(srvModel.rate)" data-clipboard-hover="start">
<template v-if="srvModel.cryptoCode === 'sats'">1 sat = {{ srvModel.rate }}</template>
<template v-else>1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}</template>
<template v-if="srvModel.paymentMethodCurrency === 'sats'">1 sat = {{ srvModel.rate }}</template>
<template v-else>1 {{ srvModel.paymentMethodCurrency }} = {{ srvModel.rate }}</template>
</dd>
</div>
<div v-if="srvModel.networkFee" id="PaymentDetails-NetworkCost" key="NetworkCost">
<dt v-t="'network_cost'"></dt>
<dd :data-clipboard="asNumber(srvModel.networkFee)" data-clipboard-hover="start">
<div v-if="srvModel.txCountForFee > 0" v-t="{ path: 'tx_count', args: { count: srvModel.txCount } }"></div>
<div v-text="`${srvModel.networkFee} ${srvModel.cryptoCode}`"></div>
<div v-text="`${srvModel.networkFee} ${srvModel.paymentMethodCurrency}`"></div>
</dd>
</div>
<div v-if="btcPaid > 0" id="PaymentDetails-AmountPaid" key="AmountPaid">
<div v-if="paid > 0" id="PaymentDetails-AmountPaid" key="AmountPaid">
<dt v-t="'amount_paid'"></dt>
<dd :data-clipboard="asNumber(srvModel.btcPaid)" data-clipboard-hover="start" v-text="`${srvModel.btcPaid} ${srvModel.cryptoCode}`"></dd>
<dd :data-clipboard="asNumber(srvModel.paid)" data-clipboard-hover="start" v-text="`${srvModel.paid} ${srvModel.paymentMethodCurrency}`"></dd>
</div>
<div v-if="btcDue > 0" id="PaymentDetails-AmountDue" key="AmountDue">
<div v-if="due > 0" id="PaymentDetails-AmountDue" key="AmountDue">
<dt v-t="'amount_due'"></dt>
<dd :data-clipboard="asNumber(srvModel.btcDue)" data-clipboard-hover="start" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`"></dd>
<dd :data-clipboard="asNumber(srvModel.due)" data-clipboard-hover="start" v-text="`${srvModel.due} ${srvModel.paymentMethodCurrency}`"></dd>
</div>
<div v-if="showRecommendedFee" id="PaymentDetails-RecommendedFee" key="RecommendedFee">
<dt v-t="'recommended_fee'"></dt>
@@ -316,10 +316,6 @@
@if (Env.CheatMode)
{
<partial name="Checkout-Cheating" model="@Model" />
}
@foreach (var extensionPartial in Model.ExtensionPartials)
{
<partial name="@extensionPartial" model="@Model" />
}
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment", model = Model })
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-end", model = Model })

View File

@@ -1,7 +1,6 @@
@model PaymentModel
@model CheckoutModel
@{
Layout = null;
var displayedPaymentMethods = Model.AvailableCryptos.Where(a => a.Displayed).ToList();
}
<!DOCTYPE html>
<html>
@@ -17,36 +16,7 @@
<h1>Pay with @Model.StoreName</h1>
@if (Model.Status == "new")
{
if (!string.IsNullOrEmpty(Model.UISettings?.NoScriptPartialName))
{
<partial model="@Model" name="@Model.UISettings.NoScriptPartialName"/>
}
else
{
<h1 class="text-danger">This payment method requires javascript.</h1>
}
@if (displayedPaymentMethods.Count > 1)
{
<div>
<hr />
<h2>Pay with:</h2>
<ul style="list-style-type: none;padding-left: 5px;">
@foreach (var crypto in displayedPaymentMethods)
{
<li style="height: 32px; line-height: 32px;">
<a asp-action="CheckoutNoScript" asp-route-invoiceId="@Model.InvoiceId" asp-route-paymentMethodId="@crypto.PaymentMethodId">
<img alt="@crypto.PaymentMethodName" src="@crypto.CryptoImage" style="vertical-align:middle; height:24px; text-decoration:none; margin-top: -3px;" asp-append-version="true" />
</a>
<a asp-action="CheckoutNoScript" asp-route-invoiceId="@Model.InvoiceId" asp-route-paymentMethodId="@crypto.PaymentMethodId" style="padding-top: 2px;">
@crypto.PaymentMethodName
@(crypto.IsLightning ? Html.Raw("&#9889;") : null)
(@crypto.CryptoCode)
</a>
</li>
}
</ul>
</div>
}
<h1 class="text-danger">This payment method requires javascript.</h1>
}
else if (Model.Status == "paid" || Model.Status == "complete" || Model.Status == "confirmed")
{

View File

@@ -1,5 +1,4 @@
@using BTCPayServer.Payments
@inject PaymentMethodViewProvider paymentMethodViewProvider
@model (InvoiceDetailsModel Invoice, bool ShowAddress)
@{
var invoice = Model.Invoice;
@@ -87,14 +86,10 @@
}
</td>
</tr>
var vvm = paymentMethodViewProvider.TryGetViewViewModel(payment.PaymentMethodRaw, "AdditionalPaymentMethodDetails");;
if (vvm != null)
{
<partial name="@((string)vvm.View)" model="@vvm.ViewModel" />
}
<vc:ui-extension-point location="invoice-payments-list" />
}
</tbody>
</table>
</div>
<vc:ui-extension-point location="store-invoices-payments" model="@invoice.Payments" />
<vc:ui-extension-point location="store-invoices-payments" model="@invoice" />

View File

@@ -73,8 +73,8 @@ const PaymentDetails = {
isActive: Boolean,
showRecommendedFee: Boolean,
orderAmount: Number,
btcPaid: Number,
btcDue: Number
paid: Number,
due: Number
},
methods: {
asNumber
@@ -129,7 +129,7 @@ function initApp() {
return STATUS_PAYABLE.includes(this.srvModel.status);
},
isPaidPartial () {
return this.btcPaid > 0 && this.btcDue > 0;
return this.paid > 0 && this.due > 0;
},
showInfo () {
return this.showTimer || this.showPaymentDueInfo;
@@ -146,13 +146,13 @@ function initApp() {
orderAmount () {
return this.asNumber(this.srvModel.orderAmount);
},
btcDue () {
return this.asNumber(this.srvModel.btcDue);
due () {
return this.asNumber(this.srvModel.due);
},
btcPaid () {
return this.asNumber(this.srvModel.btcPaid);
paid () {
return this.asNumber(this.srvModel.paid);
},
pmId () {
pmId() {
return this.paymentMethodId || this.srvModel.paymentMethodId;
},
minutesLeft () {
@@ -172,18 +172,18 @@ function initApp() {
: null;
},
paymentMethodIds () {
return this.srvModel.availableCryptos.map(function (c) { return c.paymentMethodId });
return this.srvModel.availablePaymentMethods.map(function (c) { return c.paymentMethodId });
},
paymentMethodComponent () {
paymentMethodComponent() {
return this.isPluginPaymentMethod
? `${this.pmId}Checkout`
: this.srvModel.activated && this.srvModel.uiSettings.checkoutBodyVueComponentName;
: this.srvModel.activated && this.srvModel.checkoutBodyComponentName;
},
isPluginPaymentMethod () {
return !this.paymentMethodIds.includes(this.pmId);
},
realCryptoCode () {
return this.srvModel.cryptoCode.toLowerCase() === 'sats' ? 'BTC' : this.srvModel.cryptoCode;
realPaymentMethodCurrency () {
return this.srvModel.paymentMethodCurrency.toLowerCase() === 'sats' ? 'BTC' : this.srvModel.paymentMethodCurrency;
}
},
watch: {