Create separate payment settings section for stores

This commit is contained in:
Dennis Reimann 2021-10-05 17:37:25 +02:00
parent 8c2dcfa166
commit f28eb13b0d
No known key found for this signature in database
GPG key ID: 5009E1797F03F8D0
15 changed files with 381 additions and 183 deletions

View file

@ -77,7 +77,7 @@ namespace BTCPayServer.Tests
// Get enabled state from overview action
StoreViewModel storeModel;
response = await controller.UpdateStore();
response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode);
Assert.NotNull(lnNode);
@ -89,7 +89,7 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(response);
// Get enabled state from overview action
response = await controller.UpdateStore();
response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);
@ -98,7 +98,7 @@ namespace BTCPayServer.Tests
// Disable wallet
response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
response = await controller.UpdateStore();
response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);

View file

@ -137,10 +137,10 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
var store = s.CreateNewStore();
s.AddLightningNode();
s.GoToStore(store.storeId);
s.GoToStore(store.storeId, StoreNavPages.Payment);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
Assert.Contains("Payment settings successfully updated", s.FindAlertMessage().Text);
var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);

View file

@ -165,7 +165,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
await user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";

View file

@ -570,9 +570,9 @@ namespace BTCPayServer.Tests
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
await notifications.NextEventAsync();
await bob.ModifyStore(s => s.PayJoinEnabled = true);
await bob.ModifyPayment(p => p.PayJoinEnabled = true);
var invoice = bob.BitPay.CreateInvoice(
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true });
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);

View file

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
{
await RegisterAsync(isAdmin);
await CreateStoreAsync();
var store = this.GetController<StoresController>();
var store = GetController<StoresController>();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
@ -127,19 +127,28 @@ namespace BTCPayServer.Tests
public async Task SetNetworkFeeMode(NetworkFeeMode mode)
{
await ModifyStore(store =>
await ModifyPayment(payment =>
{
store.NetworkFeeMode = mode;
payment.NetworkFeeMode = mode;
});
}
public async Task ModifyStore(Action<StoreViewModel> modify)
public void ModifyStore(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
var response = await storeController.UpdateStore();
var response = storeController.UpdateStore();
StoreViewModel store = (StoreViewModel)((ViewResult)response).Model;
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
storeController.UpdateStore(store);
}
public async Task ModifyPayment(Action<PaymentViewModel> modify)
{
var storeController = GetController<StoresController>();
var response = await storeController.Payment();
PaymentViewModel payment = (PaymentViewModel)((ViewResult)response).Model;
modify(payment);
storeController.Payment(payment).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
@ -190,7 +199,7 @@ namespace BTCPayServer.Tests
public Task EnablePayJoin()
{
return ModifyStore(s => s.PayJoinEnabled = true);
return ModifyPayment(p => p.PayJoinEnabled = true);
}
public GenerateWalletResponse GenerateWalletResponseV { get; set; }

View file

@ -818,11 +818,11 @@ namespace BTCPayServer.Tests
// Set tolerance to 50%
var stores = user.GetController<StoresController>();
var response = await stores.UpdateStore();
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(response).Model);
var response = await stores.Payment();
var vm = Assert.IsType<PaymentViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
Assert.IsType<RedirectToActionResult>(stores.UpdateStore(vm).Result);
Assert.IsType<RedirectToActionResult>(stores.Payment(vm).Result);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
@ -996,8 +996,7 @@ namespace BTCPayServer.Tests
Assert.Equal(4, tor.Services.Length);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
@ -1012,7 +1011,7 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyStore(model => model.SpeedPolicy = SpeedPolicy.HighSpeed);
await user.ModifyPayment(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
@ -1065,7 +1064,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess(true);
var storeController = user.GetController<StoresController>();
var storeResponse = await storeController.UpdateStore();
var storeResponse = storeController.UpdateStore();
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(await storeController.SetupLightningNode(user.StoreId, "BTC"));
@ -1089,7 +1088,7 @@ namespace BTCPayServer.Tests
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult());
storeResponse = await storeController.UpdateStore();
storeResponse = storeController.UpdateStore();
var storeVm =
Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(storeResponse).Model);
@ -1205,7 +1204,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
await acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed);
await acc.ModifyPayment(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice
{
Price = 5.0m,
@ -2032,7 +2031,7 @@ namespace BTCPayServer.Tests
});
Assert.Equal(404, (int)response.StatusCode);
await user.ModifyStore(s => s.AnyoneCanCreateInvoice = true);
await user.ModifyPayment(p => p.AnyoneCanCreateInvoice = true);
Logs.Tester.LogInformation("Bad store with anyone can create invoice = 403");
response = await tester.PayTester.HttpClient.SendAsync(
@ -2448,12 +2447,12 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
// enable unified QR code in settings
var vm = Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(await user.GetController<StoresController>().UpdateStore()).Model
var vm = Assert.IsType<PaymentViewModel>(Assert
.IsType<ViewResult>(await user.GetController<StoresController>().Payment()).Model
);
vm.OnChainWithLnInvoiceFallback = true;
Assert.IsType<RedirectToActionResult>(
user.GetController<StoresController>().UpdateStore(vm).Result
user.GetController<StoresController>().Payment(vm).Result
);
// validate that QR code now has both onchain and offchain payment urls
@ -2470,7 +2469,7 @@ namespace BTCPayServer.Tests
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
// Fallback lightning invoice should be uppercase inside the QR code.
var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new string[] { "&lightning=" }, StringSplitOptions.None)[1];
var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new [] { "&lightning=" }, StringSplitOptions.None)[1];
Assert.True(lightningFallback.ToUpperInvariant() == lightningFallback);
}
}

View file

@ -493,8 +493,8 @@ namespace BTCPayServer.Controllers
});
}
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
{
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode =
@ -509,6 +509,9 @@ namespace BTCPayServer.Controllers
.Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance)
.ToDictionary(c => c.CryptoCode.ToUpperInvariant());
derivationSchemes = new List<StoreDerivationScheme>();
lightningNodes = new List<StoreLightningNode>();
foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods()))
{
switch (paymentMethodId.PaymentType)
@ -518,7 +521,7 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
derivationSchemes.Add(new StoreDerivationScheme
{
Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported,
@ -530,12 +533,14 @@ namespace BTCPayServer.Controllers
#endif
});
break;
case LNURLPayPaymentType lnurlPayPaymentType:
break;
case LightningPaymentType _:
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null;
vm.LightningNodes.Add(new StoreViewModel.LightningNode
lightningNodes.Add(new StoreLightningNode
{
CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetDisplayableConnectionString(),
@ -547,30 +552,92 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}")]
public async Task<IActionResult> UpdateStore()
public IActionResult UpdateStore()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.DefaultCurrency = storeBlob.DefaultCurrency;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
var vm = new StoreViewModel
{
Id = store.Id,
CanDelete = _Repo.CanDeleteStores(),
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
HintWallet = storeBlob.Hints.Wallet,
HintLightning = storeBlob.Hints.Lightning
};
AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
vm.DerivationSchemes = derivationSchemes;
vm.LightningNodes = lightningNodes;
return View(vm);
}
[HttpPost("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
{
bool needUpdate = false;
if (CurrentStore.StoreName != model.StoreName)
{
needUpdate = true;
CurrentStore.StoreName = model.StoreName;
}
if (CurrentStore.StoreWebsite != model.StoreWebsite)
{
needUpdate = true;
CurrentStore.StoreWebsite = model.StoreWebsite;
}
var blob = CurrentStore.GetStoreBlob();
if (CurrentStore.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
}
return RedirectToAction(nameof(UpdateStore), new
{
storeId = CurrentStore.Id
});
}
[HttpGet("{storeId}/payment")]
public async Task<IActionResult> Payment()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new PaymentViewModel
{
NetworkFeeMode = storeBlob.NetworkFeeMode,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
SpeedPolicy = store.SpeedPolicy,
PaymentTolerance = storeBlob.PaymentTolerance,
DefaultCurrency = storeBlob.DefaultCurrency
};
AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
vm.DerivationSchemes = derivationSchemes;
vm.LightningNodes = lightningNodes;
vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
vm.HintWallet = storeBlob.Hints.Wallet;
vm.HintLightning = storeBlob.Hints.Lightning;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
@ -584,12 +651,12 @@ namespace BTCPayServer.Controllers
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet);
return View(vm);
}
[HttpPost("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
[HttpPost("{storeId}/payment")]
public async Task<IActionResult> Payment(PaymentViewModel model, string command = null)
{
bool needUpdate = false;
if (CurrentStore.SpeedPolicy != model.SpeedPolicy)
@ -597,16 +664,6 @@ namespace BTCPayServer.Controllers
needUpdate = true;
CurrentStore.SpeedPolicy = model.SpeedPolicy;
}
if (CurrentStore.StoreName != model.StoreName)
{
needUpdate = true;
CurrentStore.StoreName = model.StoreName;
}
if (CurrentStore.StoreWebsite != model.StoreWebsite)
{
needUpdate = true;
CurrentStore.StoreWebsite = model.StoreWebsite;
}
var blob = CurrentStore.GetStoreBlob();
blob.DefaultCurrency = model.DefaultCurrency;
@ -633,7 +690,7 @@ namespace BTCPayServer.Controllers
{
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated";
if (payjoinChanged && blob.PayJoinEnabled)
{
@ -649,13 +706,13 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = $"The store was updated successfully. However, payjoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
Html = $"The payment settings were updated successfully. However, payjoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
});
}
}
}
return RedirectToAction(nameof(UpdateStore), new
return RedirectToAction(nameof(Payment), new
{
storeId = CurrentStore.Id
});

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Client.Models;
using BTCPayServer.Validation;
using static BTCPayServer.Data.StoreBlob;
namespace BTCPayServer.Models.StoreViewModels
{
public class PaymentViewModel
{
public List<StoreDerivationScheme> DerivationSchemes { get; set; }
public List<StoreLightningNode> LightningNodes { get; set; }
public bool IsOnchainSetup { get; set; }
public bool IsLightningSetup { get; set; }
public bool CanUsePayJoin { get; set; }
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }
[Display(Name = "Invoice expires if the full amount has not been paid after …")]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration { get; set; }
[Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")]
[Range(10, 60 * 24 * 24)]
public int MonitoringExpiration { get; set; }
[Display(Name = "Consider the invoice confirmed when the payment transaction …")]
public SpeedPolicy SpeedPolicy { get; set; }
[Display(Name = "Add additional fee (network fee) to invoice …")]
public NetworkFeeMode NetworkFeeMode { get; set; }
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
[Display(Name = "Enable Payjoin/P2EP")]
public bool PayJoinEnabled { get; set; }
[Display(Name = "Show recommended fee")]
public bool ShowRecommendedFee { get; set; }
[Display(Name = "Recommended fee confirmation target blocks")]
[Range(1, double.PositiveInfinity)]
public int RecommendedFeeBlockTarget { get; set; }
[Display(Name = "Display Lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
[Display(Name = "Add hop hints for private channels to the Lightning invoice")]
public bool LightningPrivateRouteHints { get; set; }
[Display(Name = "Include Lightning invoice fallback to on-chain BIP21 payment URL")]
public bool OnChainWithLnInvoiceFallback { get; set; }
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
[Range(0, 100)]
public double PaymentTolerance { get; set; }
[Display(Name = "Default currency")]
[MaxLength(10)]
public string DefaultCurrency { get; set; }
}
}

View file

@ -0,0 +1,12 @@
namespace BTCPayServer.Models.StoreViewModels
{
public class StoreDerivationScheme
{
public string Crypto { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
public bool WalletSupported { get; set; }
public bool Enabled { get; set; }
public bool Collapsed { get; set; }
}
}

View file

@ -0,0 +1,9 @@
namespace BTCPayServer.Models.StoreViewModels
{
public class StoreLightningNode
{
public string CryptoCode { get; set; }
public string Address { get; set; }
public bool Enabled { get; set; }
}
}

View file

@ -8,130 +8,24 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoreViewModel
{
public class DerivationScheme
{
public string Crypto { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
public bool WalletSupported { get; set; }
public bool Enabled { get; set; }
public bool Collapsed { get; set; }
}
public class AdditionalPaymentMethod
{
public string Provider { get; set; }
public bool Enabled { get; set; }
public string Action { get; set; }
}
public StoreViewModel()
{
}
public List<StoreDerivationScheme> DerivationSchemes { get; set; }
public List<StoreLightningNode> LightningNodes { get; set; }
public bool HintWallet { get; set; }
public bool HintLightning { get; set; }
public bool CanDelete { get; set; }
[Display(Name = "Store ID")]
public string Id { get; set; }
[Display(Name = "Store Name")]
[Required]
[MaxLength(50)]
[MinLength(1)]
public string StoreName
{
get; set;
}
public string StoreName { get; set; }
[Uri]
[Display(Name = "Store Website")]
[MaxLength(500)]
public string StoreWebsite
{
get;
set;
}
[Display(Name = "Default currency")]
[MaxLength(10)]
public string DefaultCurrency { get; set; }
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Invoice expires if the full amount has not been paid after …")]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration
{
get;
set;
}
[Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")]
[Range(10, 60 * 24 * 24)]
public int MonitoringExpiration
{
get;
set;
}
[Display(Name = "Consider the invoice confirmed when the payment transaction …")]
public SpeedPolicy SpeedPolicy
{
get; set;
}
[Display(Name = "Add additional fee (network fee) to invoice …")]
public NetworkFeeMode NetworkFeeMode
{
get; set;
}
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
[Display(Name = "Enable Payjoin/P2EP")]
public bool PayJoinEnabled { get; set; }
public bool CanUsePayJoin { get; set; }
public bool IsOnchainSetup { get; set; }
public bool IsLightningSetup { get; set; }
public bool HintWallet { get; set; }
public bool HintLightning { get; set; }
[Display(Name = "Show recommended fee")]
public bool ShowRecommendedFee { get; set; }
[Display(Name = "Recommended fee confirmation target blocks")]
[Range(1, double.PositiveInfinity)]
public int RecommendedFeeBlockTarget { get; set; }
[Display(Name = "Display Lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
[Display(Name = "Add hop hints for private channels to the Lightning invoice")]
public bool LightningPrivateRouteHints { get; set; }
[Display(Name = "Include Lightning invoice fallback to on-chain BIP21 payment URL")]
public bool OnChainWithLnInvoiceFallback { get; set; }
public class LightningNode
{
public string CryptoCode { get; set; }
public string Address { get; set; }
public bool Enabled { get; set; }
}
public List<LightningNode> LightningNodes
{
get; set;
} = new List<LightningNode>();
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
[Range(0, 100)]
public double PaymentTolerance
{
get;
set;
}
public string StoreWebsite { get; set; }
}
}

View file

@ -12,7 +12,7 @@
{
<div asp-validation-summary="All" class="text-danger"></div>
}
<h4 class="mb-3">Payment</h4>
<h4 class="mb-3">Invoice Settings</h4>
@if (Model.PaymentMethods.Any())
{
<div class="form-group mb-4">

View file

@ -0,0 +1,156 @@
@model PaymentViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Payment, "Payment", Context.GetStoreData().StoreName);
}
<div class="row">
<div class="col-lg-10 col-xl-9">
<h4 class="mb-3">Payment</h4>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
}
@if (Model.IsOnchainSetup || Model.IsLightningSetup)
{
<form method="post">
<div class="form-group d-flex align-items-center">
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="AnyoneCanCreateInvoice" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#allow-anyone-to-create-invoice" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
<div class="form-group mt-4">
<label asp-for="NetworkFeeMode" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="NetworkFeeMode" class="form-select">
<option value="MultiplePaymentsOnly">... only if the customer makes more than one payment for the invoice</option>
<option value="Always">... on every payment</option>
<option value="Never">Never add network fee</option>
</select>
</div>
<div class="form-group">
<label asp-for="InvoiceExpiration" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#invoice-expires-if-the-full-amount-has-not-been-paid-after-minutes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="InvoiceExpiration" class="form-control" style="max-width:10ch;"/>
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PaymentTolerance" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-paid-even-if-the-paid-amount-is-less-than-expected" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="PaymentTolerance" class="form-control" style="max-width:10ch;"/>
<span class="input-group-text">percent</span>
</div>
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCurrency" class="form-label"></label>
<input asp-for="DefaultCurrency" class="form-control" />
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
</div>
@if (Model.IsOnchainSetup)
{
<h5 class="mt-5 mb-3">On-Chain</h5>
@if (Model.CanUsePayJoin)
{
<div class="form-group">
<div class="d-flex align-items-center">
<input asp-for="PayJoinEnabled" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="PayJoinEnabled" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/Payjoin/" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
<span asp-validation-for="PayJoinEnabled" class="text-danger"></span>
</div>
}
<div class="form-group">
<label asp-for="MonitoringExpiration" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#payment-invalid-if-transactions-fails-to-confirm-minutes-after-invoice-expiration" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="MonitoringExpiration" class="form-control" style="max-width:10ch;"/>
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SpeedPolicy" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="SpeedPolicy" class="form-select w-auto" onchange="document.getElementById('unconfirmed-warning').hidden = this.value !== '0';">
<option value="0">Is unconfirmed</option>
<option value="1">Has at least 1 confirmation</option>
<option value="3">Has at least 2 confirmations</option>
<option value="2">Has at least 6 confirmations</option>
</select>
<div class="alert alert-warning my-2" hidden="@(Model.SpeedPolicy != 0)" id="unconfirmed-warning" role="alert">
Choosing to accept an unconfirmed invoice can lead to double-spending and is strongly discouraged.
</div>
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
</div>
<div class="form-check my-1">
<input asp-for="ShowRecommendedFee" type="checkbox" class="form-check-input"/>
<label asp-for="ShowRecommendedFee" class="form-check-label"></label>
<p class="form-text text-muted mb-0">Fee will be shown for BTC and LTC onchain payments only.</p>
</div>
<div class="form-group mt-2 mb-4">
<label asp-for="RecommendedFeeBlockTarget" class="form-label"></label>
<input asp-for="RecommendedFeeBlockTarget" class="form-control" style="width:8ch" min="1" />
<span asp-validation-for="RecommendedFeeBlockTarget" class="text-danger"></span>
</div>
}
@if (Model.IsLightningSetup)
{
<h5 class="mt-5 mb-3">Lightning</h5>
<div class="form-check my-1">
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input"/>
<label asp-for="LightningAmountInSatoshi" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="LightningPrivateRouteHints" type="checkbox" class="form-check-input"/>
<label asp-for="LightningPrivateRouteHints" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input"/>
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
</div>
<div class="form-group mt-3">
<label asp-for="LightningDescriptionTemplate" class="form-label"></label>
<input asp-for="LightningDescriptionTemplate" class="form-control"/>
<span asp-validation-for="LightningDescriptionTemplate" class="text-danger"></span>
<p class="form-text text-muted">
Available placeholders:
<code>{StoreName} {ItemDescription} {OrderId}</code>
</p>
</div>
}
<button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save Payment Settings</button>
</form>
}
else
{
<p class="text-secondary mt-3">
Please configure either an on-chain wallet or Lightning node first.
</p>
}
</div>
</div>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
}

View file

@ -2,8 +2,6 @@ namespace BTCPayServer.Views.Stores
{
public enum StoreNavPages
{
Index, Create, Rates, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage,
PullPayments,
Payouts
Index, Create, Rates, Payment, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage, PullPayments, Payouts
}
}

View file

@ -7,7 +7,7 @@
<a id="@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Pay Button</a>
<a id="@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Integrations</a>
<a id="@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Webhooks</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@this.Context.GetRouteValue("storeId")" id="PullPayments">Pull payments</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" asp-action="Payouts" asp-controller="StorePullPayments" asp-route-storeId="@this.Context.GetRouteValue("storeId")" id="Payouts">Payouts</a>
<a id="@(nameof(StoreNavPages.PullPayments))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Pull payments</a>
<a id="@(nameof(StoreNavPages.Payouts))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" asp-action="Payouts" asp-controller="StorePullPayments" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Payouts</a>
<vc:ui-extension-point location="store-nav" model="@Model" />
</nav>