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

View file

@ -137,10 +137,10 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true); s.RegisterNewUser(true);
var store = s.CreateNewStore(); var store = s.CreateNewStore();
s.AddLightningNode(); s.AddLightningNode();
s.GoToStore(store.storeId); s.GoToStore(store.storeId, StoreNavPages.Payment);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true); s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
s.Driver.FindElement(By.Id("Save")).Click(); 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"); var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId); s.GoToInvoiceCheckout(invoiceId);

View file

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

View file

@ -570,9 +570,9 @@ namespace BTCPayServer.Tests
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address; address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m)); tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
await notifications.NextEventAsync(); await notifications.NextEventAsync();
await bob.ModifyStore(s => s.PayJoinEnabled = true); await bob.ModifyPayment(p => p.PayJoinEnabled = true);
var invoice = bob.BitPay.CreateInvoice( 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, var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork); tester.ExplorerClient.Network.NBitcoinNetwork);

View file

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

View file

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

View file

@ -493,8 +493,8 @@ namespace BTCPayServer.Controllers
}); });
} }
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm) out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
{ {
var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode = var derivationByCryptoCode =
@ -509,6 +509,9 @@ namespace BTCPayServer.Controllers
.Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance) .Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance)
.ToDictionary(c => c.CryptoCode.ToUpperInvariant()); .ToDictionary(c => c.CryptoCode.ToUpperInvariant());
derivationSchemes = new List<StoreDerivationScheme>();
lightningNodes = new List<StoreLightningNode>();
foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods())) foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods()))
{ {
switch (paymentMethodId.PaymentType) switch (paymentMethodId.PaymentType)
@ -518,7 +521,7 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode); var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty; var value = strategy?.ToPrettyString() ?? string.Empty;
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() derivationSchemes.Add(new StoreDerivationScheme
{ {
Crypto = paymentMethodId.CryptoCode, Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported, WalletSupported = network.WalletSupported,
@ -530,12 +533,14 @@ namespace BTCPayServer.Controllers
#endif #endif
}); });
break; break;
case LNURLPayPaymentType lnurlPayPaymentType: case LNURLPayPaymentType lnurlPayPaymentType:
break; break;
case LightningPaymentType _: case LightningPaymentType _:
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode); var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null; var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null;
vm.LightningNodes.Add(new StoreViewModel.LightningNode lightningNodes.Add(new StoreLightningNode
{ {
CryptoCode = paymentMethodId.CryptoCode, CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetDisplayableConnectionString(), Address = lightning?.GetDisplayableConnectionString(),
@ -547,30 +552,92 @@ namespace BTCPayServer.Controllers
} }
[HttpGet("{storeId}")] [HttpGet("{storeId}")]
public async Task<IActionResult> UpdateStore() public IActionResult UpdateStore()
{ {
var store = HttpContext.GetStoreData(); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel(); var vm = new StoreViewModel
vm.Id = store.Id; {
vm.StoreName = store.StoreName; Id = store.Id,
vm.StoreWebsite = store.StoreWebsite; CanDelete = _Repo.CanDeleteStores(),
vm.DefaultCurrency = storeBlob.DefaultCurrency; StoreName = store.StoreName,
vm.NetworkFeeMode = storeBlob.NetworkFeeMode; StoreWebsite = store.StoreWebsite,
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice; HintWallet = storeBlob.Hints.Wallet,
vm.SpeedPolicy = store.SpeedPolicy; HintLightning = storeBlob.Hints.Lightning
vm.CanDelete = _Repo.CanDeleteStores(); };
AddPaymentMethods(store, storeBlob, vm);
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.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes; vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate; vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled; vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
vm.HintWallet = storeBlob.Hints.Wallet;
vm.HintLightning = storeBlob.Hints.Lightning;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
@ -584,12 +651,12 @@ namespace BTCPayServer.Controllers
.GetSupportedPaymentMethods(_NetworkProvider) .GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>() .OfType<DerivationSchemeSettings>()
.Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet); .Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet);
return View(vm); return View(vm);
} }
[HttpPost("{storeId}")] [HttpPost("{storeId}/payment")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null) public async Task<IActionResult> Payment(PaymentViewModel model, string command = null)
{ {
bool needUpdate = false; bool needUpdate = false;
if (CurrentStore.SpeedPolicy != model.SpeedPolicy) if (CurrentStore.SpeedPolicy != model.SpeedPolicy)
@ -597,16 +664,6 @@ namespace BTCPayServer.Controllers
needUpdate = true; needUpdate = true;
CurrentStore.SpeedPolicy = model.SpeedPolicy; 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(); var blob = CurrentStore.GetStoreBlob();
blob.DefaultCurrency = model.DefaultCurrency; blob.DefaultCurrency = model.DefaultCurrency;
@ -633,7 +690,7 @@ namespace BTCPayServer.Controllers
{ {
await _Repo.UpdateStore(CurrentStore); await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated";
if (payjoinChanged && blob.PayJoinEnabled) if (payjoinChanged && blob.PayJoinEnabled)
{ {
@ -649,13 +706,13 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Severity = StatusMessageModel.StatusSeverity.Warning, 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 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 StoreViewModel
{ {
public class DerivationScheme public List<StoreDerivationScheme> DerivationSchemes { get; set; }
{ public List<StoreLightningNode> LightningNodes { get; set; }
public string Crypto { get; set; } public bool HintWallet { get; set; }
public string Value { get; set; } public bool HintLightning { 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 bool CanDelete { get; set; } public bool CanDelete { get; set; }
[Display(Name = "Store ID")] [Display(Name = "Store ID")]
public string Id { get; set; } public string Id { get; set; }
[Display(Name = "Store Name")] [Display(Name = "Store Name")]
[Required] [Required]
[MaxLength(50)] [MaxLength(50)]
[MinLength(1)] [MinLength(1)]
public string StoreName public string StoreName { get; set; }
{
get; set;
}
[Uri] [Uri]
[Display(Name = "Store Website")] [Display(Name = "Store Website")]
[MaxLength(500)] [MaxLength(500)]
public string StoreWebsite public string StoreWebsite { get; set; }
{
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;
}
} }
} }

View file

@ -12,7 +12,7 @@
{ {
<div asp-validation-summary="All" class="text-danger"></div> <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()) @if (Model.PaymentMethods.Any())
{ {
<div class="form-group mb-4"> <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 public enum StoreNavPages
{ {
Index, Create, Rates, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage, Index, Create, Rates, Payment, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage, PullPayments, Payouts
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.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.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 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 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 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.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" /> <vc:ui-extension-point location="store-nav" model="@Model" />
</nav> </nav>