mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
Merge pull request #3337 from dennisreimann/merge-general-payment
Store: Combine General and Payment settings
This commit is contained in:
commit
fe9de98dd1
30 changed files with 216 additions and 245 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace BTCPayServer.Rating
|
namespace BTCPayServer.Rating
|
||||||
{
|
{
|
||||||
public enum RateSource
|
public enum RateSource
|
||||||
|
@ -25,5 +27,13 @@ namespace BTCPayServer.Rating
|
||||||
Url = url;
|
Url = url;
|
||||||
Source = source;
|
Source = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string DisplayName =>
|
||||||
|
Source switch
|
||||||
|
{
|
||||||
|
RateSource.Direct => Name,
|
||||||
|
RateSource.Coingecko => $"{Name} (via CoinGecko)",
|
||||||
|
_ => throw new NotSupportedException(Source.ToString())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -386,7 +386,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
// BTC crash by 50%
|
// BTC crash by 50%
|
||||||
s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m / 2.0m, 5100.0m / 2.0m));
|
s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m / 2.0m, 5100.0m / 2.0m));
|
||||||
s.GoToStore(StoreNavPages.Payment);
|
s.GoToStore();
|
||||||
s.Driver.FindElement(By.Id("BOLT11Expiration")).Clear();
|
s.Driver.FindElement(By.Id("BOLT11Expiration")).Clear();
|
||||||
s.Driver.FindElement(By.Id("BOLT11Expiration")).SendKeys("5" + Keys.Enter);
|
s.Driver.FindElement(By.Id("BOLT11Expiration")).SendKeys("5" + Keys.Enter);
|
||||||
s.GoToInvoice(invoice.Id);
|
s.GoToInvoice(invoice.Id);
|
||||||
|
@ -438,7 +438,7 @@ namespace BTCPayServer.Tests
|
||||||
s.GoToInvoiceCheckout(invoiceId);
|
s.GoToInvoiceCheckout(invoiceId);
|
||||||
s.Driver.FindElement(By.ClassName("payment__currencies_noborder"));
|
s.Driver.FindElement(By.ClassName("payment__currencies_noborder"));
|
||||||
s.GoToHome();
|
s.GoToHome();
|
||||||
s.GoToStore(StoreNavPages.Payment);
|
s.GoToStore();
|
||||||
s.AddDerivationScheme("LTC");
|
s.AddDerivationScheme("LTC");
|
||||||
s.AddLightningNode(LightningConnectionType.CLightning);
|
s.AddLightningNode(LightningConnectionType.CLightning);
|
||||||
//there should be three now
|
//there should be three now
|
||||||
|
|
|
@ -204,7 +204,7 @@ namespace BTCPayServer.Tests
|
||||||
s.GoToRegister();
|
s.GoToRegister();
|
||||||
s.RegisterNewUser();
|
s.RegisterNewUser();
|
||||||
s.CreateNewStore();
|
s.CreateNewStore();
|
||||||
s.GoToStore(StoreNavPages.Payment);
|
s.GoToStore();
|
||||||
s.AddDerivationScheme();
|
s.AddDerivationScheme();
|
||||||
var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com");
|
var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com");
|
||||||
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
||||||
|
|
|
@ -156,7 +156,6 @@ namespace BTCPayServer.Tests
|
||||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||||
Driver.FindElement(By.Id($"SectionNav-{StoreNavPages.General.ToString()}")).Click();
|
Driver.FindElement(By.Id($"SectionNav-{StoreNavPages.General.ToString()}")).Click();
|
||||||
var storeId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value");
|
var storeId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value");
|
||||||
Driver.FindElement(By.Id($"SectionNav-{StoreNavPages.Payment.ToString()}")).Click();
|
|
||||||
if (keepId)
|
if (keepId)
|
||||||
StoreId = storeId;
|
StoreId = storeId;
|
||||||
return (name, storeId);
|
return (name, storeId);
|
||||||
|
@ -397,7 +396,6 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
public void GoToLightningSettings(string cryptoCode = "BTC")
|
public void GoToLightningSettings(string cryptoCode = "BTC")
|
||||||
{
|
{
|
||||||
GoToStore(StoreNavPages.Payment);
|
|
||||||
Driver.FindElement(By.Id($"StoreNav-Lightning{cryptoCode}")).Click();
|
Driver.FindElement(By.Id($"StoreNav-Lightning{cryptoCode}")).Click();
|
||||||
// if Lightning is already set up we need to navigate to the settings
|
// if Lightning is already set up we need to navigate to the settings
|
||||||
if (Driver.PageSource.Contains("id=\"SectionNav-LightningSettings\""))
|
if (Driver.PageSource.Contains("id=\"SectionNav-LightningSettings\""))
|
||||||
|
|
|
@ -404,7 +404,7 @@ namespace BTCPayServer.Tests
|
||||||
(string storeName, string storeId) = s.CreateNewStore();
|
(string storeName, string storeId) = s.CreateNewStore();
|
||||||
var storeUrl = $"/stores/{storeId}";
|
var storeUrl = $"/stores/{storeId}";
|
||||||
|
|
||||||
s.GoToStore(StoreNavPages.Payment);
|
s.GoToStore();
|
||||||
Assert.Contains(storeName, s.Driver.PageSource);
|
Assert.Contains(storeName, s.Driver.PageSource);
|
||||||
|
|
||||||
// verify steps for wallet setup are displayed correctly
|
// verify steps for wallet setup are displayed correctly
|
||||||
|
@ -793,7 +793,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource);
|
Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource);
|
||||||
|
|
||||||
TestLogs.LogInformation("Let's see if we can generate an event");
|
TestLogs.LogInformation("Let's see if we can generate an event");
|
||||||
s.GoToStore(StoreNavPages.Payment);
|
s.GoToStore();
|
||||||
s.AddDerivationScheme();
|
s.AddDerivationScheme();
|
||||||
s.CreateInvoice();
|
s.CreateInvoice();
|
||||||
var request = await server.GetNextRequest();
|
var request = await server.GetNextRequest();
|
||||||
|
@ -929,7 +929,7 @@ namespace BTCPayServer.Tests
|
||||||
var result =
|
var result =
|
||||||
await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
|
await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
|
||||||
Assert.True(result.IsWatchOnly);
|
Assert.True(result.IsWatchOnly);
|
||||||
s.GoToStore(storeId, StoreNavPages.Payment);
|
s.GoToStore(storeId);
|
||||||
var mnemonic = s.GenerateWallet(cryptoCode, "", true, true);
|
var mnemonic = s.GenerateWallet(cryptoCode, "", true, true);
|
||||||
|
|
||||||
//lets import and save private keys
|
//lets import and save private keys
|
||||||
|
@ -1302,7 +1302,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
s.RegisterNewUser(true);
|
s.RegisterNewUser(true);
|
||||||
s.CreateNewStore();
|
s.CreateNewStore();
|
||||||
s.GoToStore(StoreNavPages.Payment);
|
s.GoToStore();
|
||||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||||
s.GoToLightningSettings();
|
s.GoToLightningSettings();
|
||||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
|
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
|
||||||
|
@ -1345,7 +1345,6 @@ namespace BTCPayServer.Tests
|
||||||
s.RegisterNewUser(true);
|
s.RegisterNewUser(true);
|
||||||
(_, string storeId) = s.CreateNewStore();
|
(_, string storeId) = s.CreateNewStore();
|
||||||
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode).NBitcoinNetwork;
|
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode).NBitcoinNetwork;
|
||||||
s.GoToStore(StoreNavPages.Payment);
|
|
||||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||||
s.GoToLightningSettings();
|
s.GoToLightningSettings();
|
||||||
// LNURL is true by default
|
// LNURL is true by default
|
||||||
|
@ -1558,7 +1557,6 @@ namespace BTCPayServer.Tests
|
||||||
//ensure ln address is not available as Lightning is not enable
|
//ensure ln address is not available as Lightning is not enable
|
||||||
s.Driver.AssertElementNotFound(By.Id("StoreNav-LightningAddress"));
|
s.Driver.AssertElementNotFound(By.Id("StoreNav-LightningAddress"));
|
||||||
|
|
||||||
s.GoToStore(s.StoreId, StoreNavPages.Payment);
|
|
||||||
s.AddLightningNode(LightningConnectionType.LndREST, false);
|
s.AddLightningNode(LightningConnectionType.LndREST, false);
|
||||||
|
|
||||||
s.Driver.FindElement(By.Id("StoreNav-LightningAddress")).Click();
|
s.Driver.FindElement(By.Id("StoreNav-LightningAddress")).Click();
|
||||||
|
|
|
@ -133,13 +133,13 @@ namespace BTCPayServer.Tests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ModifyPayment(Action<PaymentViewModel> modify)
|
public async Task ModifyPayment(Action<GeneralSettingsViewModel> modify)
|
||||||
{
|
{
|
||||||
var storeController = GetController<UIStoresController>();
|
var storeController = GetController<UIStoresController>();
|
||||||
var response = storeController.Payment();
|
var response = storeController.GeneralSettings();
|
||||||
PaymentViewModel payment = (PaymentViewModel)((ViewResult)response).Model;
|
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model;
|
||||||
modify(payment);
|
modify(settings);
|
||||||
await storeController.Payment(payment);
|
await storeController.GeneralSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ModifyWalletSettings(Action<WalletSettingsViewModel> modify)
|
public async Task ModifyWalletSettings(Action<WalletSettingsViewModel> modify)
|
||||||
|
|
|
@ -275,13 +275,13 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
// Set tolerance to 50%
|
// Set tolerance to 50%
|
||||||
var stores = user.GetController<UIStoresController>();
|
var stores = user.GetController<UIStoresController>();
|
||||||
var response = stores.Payment();
|
var response = stores.GeneralSettings();
|
||||||
var vm = Assert.IsType<PaymentViewModel>(Assert.IsType<ViewResult>(response).Model);
|
var vm = Assert.IsType<GeneralSettingsViewModel>(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.Payment(vm).Result);
|
Assert.IsType<RedirectToActionResult>(stores.GeneralSettings(vm).Result);
|
||||||
|
|
||||||
var invoice = user.BitPay.CreateInvoice(
|
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||||
new Invoice()
|
new Invoice()
|
||||||
{
|
{
|
||||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||||
|
@ -295,7 +295,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
// Pays 75%
|
// Pays 75%
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||||
tester.ExplorerNode.SendToAddress(invoiceAddress,
|
await tester.ExplorerNode.SendToAddressAsync(invoiceAddress,
|
||||||
Money.Satoshis(invoice.BtcDue.Satoshi * 0.75m));
|
Money.Satoshis(invoice.BtcDue.Satoshi * 0.75m));
|
||||||
|
|
||||||
TestUtils.Eventually(() =>
|
TestUtils.Eventually(() =>
|
||||||
|
@ -415,13 +415,13 @@ namespace BTCPayServer.Tests
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
await tester.EnsureChannelsSetup();
|
await tester.EnsureChannelsSetup();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess(true);
|
await user.GrantAccessAsync(true);
|
||||||
var storeController = user.GetController<UIStoresController>();
|
var storeController = user.GetController<UIStoresController>();
|
||||||
var storeResponse = storeController.Payment();
|
var storeResponse = storeController.GeneralSettings();
|
||||||
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"));
|
||||||
|
|
||||||
var testResult = storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
|
storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
|
||||||
{
|
{
|
||||||
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
|
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
|
||||||
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
|
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(StoreNavPages.Payment) @ViewData.IsActivePage(StoreNavPages.Rates) @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance) @ViewData.IsActivePage(StoreNavPages.General) @ViewData.IsActivePage(StoreNavPages.Tokens) @ViewData.IsActivePage(StoreNavPages.Users) @ViewData.IsActivePage(StoreNavPages.Integrations) @ViewData.IsActivePage(StoreNavPages.Webhooks)" id="StoreNav-StoreSettings">
|
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(StoreNavPages.Rates) @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance) @ViewData.IsActivePage(StoreNavPages.General) @ViewData.IsActivePage(StoreNavPages.Tokens) @ViewData.IsActivePage(StoreNavPages.Users) @ViewData.IsActivePage(StoreNavPages.Integrations) @ViewData.IsActivePage(StoreNavPages.Webhooks)" id="StoreNav-StoreSettings">
|
||||||
<vc:icon symbol="settings"/>
|
<vc:icon symbol="settings"/>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -101,7 +101,7 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
StoreId = store.Id,
|
StoreId = store.Id,
|
||||||
StoreName = store.StoreName,
|
StoreName = store.StoreName,
|
||||||
StoreLink = Url.Action(nameof(UIStoresController.Payment), "UIStores", new { storeId = store.Id }),
|
StoreLink = Url.Action(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId = store.Id }),
|
||||||
PaymentRequestLink = Url.Action(nameof(UIPaymentRequestController.ViewPaymentRequest), "UIPaymentRequest", new { payReqId = invoice.Metadata.PaymentRequestId }),
|
PaymentRequestLink = Url.Action(nameof(UIPaymentRequestController.ViewPaymentRequest), "UIPaymentRequest", new { payReqId = invoice.Metadata.PaymentRequestId }),
|
||||||
Id = invoice.Id,
|
Id = invoice.Id,
|
||||||
State = invoiceState,
|
State = invoiceState,
|
||||||
|
@ -899,7 +899,7 @@ namespace BTCPayServer.Controllers
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
{
|
{
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(UIStoresController.Payment), "UIStores", new { storeId = store.Id })}' class='alert-link'>set up a payment method</a> first",
|
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(UIStoresController.GeneralSettings), "UIStores", new { storeId = store.Id })}' class='alert-link'>set up a payment method</a> first",
|
||||||
AllowDismiss = false
|
AllowDismiss = false
|
||||||
});
|
});
|
||||||
return View(model);
|
return View(model);
|
||||||
|
|
|
@ -158,11 +158,7 @@ namespace BTCPayServer.Controllers
|
||||||
switch (command)
|
switch (command)
|
||||||
{
|
{
|
||||||
case "save":
|
case "save":
|
||||||
var storeBlob = store.GetStoreBlob();
|
|
||||||
storeBlob.Hints.Lightning = false;
|
|
||||||
|
|
||||||
var lnurl = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay);
|
var lnurl = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay);
|
||||||
store.SetStoreBlob(storeBlob);
|
|
||||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||||
store.SetSupportedPaymentMethod(lnurl, new LNURLPaySupportedPaymentMethod()
|
store.SetSupportedPaymentMethod(lnurl, new LNURLPaySupportedPaymentMethod()
|
||||||
{
|
{
|
||||||
|
|
|
@ -163,7 +163,6 @@ namespace BTCPayServer.Controllers
|
||||||
await wallet.TrackAsync(strategy.AccountDerivation);
|
await wallet.TrackAsync(strategy.AccountDerivation);
|
||||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||||
storeBlob.SetExcluded(paymentMethodId, false);
|
storeBlob.SetExcluded(paymentMethodId, false);
|
||||||
storeBlob.Hints.Wallet = false;
|
|
||||||
storeBlob.PayJoinEnabled = strategy.IsHotWallet && !(vm.SetupRequest?.PayJoinEnabled is false);
|
storeBlob.PayJoinEnabled = strategy.IsHotWallet && !(vm.SetupRequest?.PayJoinEnabled is false);
|
||||||
store.SetStoreBlob(storeBlob);
|
store.SetStoreBlob(storeBlob);
|
||||||
}
|
}
|
||||||
|
@ -362,7 +361,7 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated.";
|
TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated.";
|
||||||
|
|
||||||
return RedirectToAction(nameof(Payment), new { storeId });
|
return RedirectToAction(nameof(GeneralSettings), new { storeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{storeId}/onchain/{cryptoCode}/settings")]
|
[HttpGet("{storeId}/onchain/{cryptoCode}/settings")]
|
||||||
|
@ -734,7 +733,7 @@ namespace BTCPayServer.Controllers
|
||||||
TempData[WellKnownTempData.SuccessMessage] =
|
TempData[WellKnownTempData.SuccessMessage] =
|
||||||
$"On-Chain payment for {network.CryptoCode} has been removed.";
|
$"On-Chain payment for {network.CryptoCode} has been removed.";
|
||||||
|
|
||||||
return RedirectToAction(nameof(Payment), new { storeId });
|
return RedirectToAction(nameof(GeneralSettings), new { storeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy)
|
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy)
|
||||||
|
|
|
@ -104,7 +104,6 @@ namespace BTCPayServer.Controllers
|
||||||
private readonly EventAggregator _EventAggregator;
|
private readonly EventAggregator _EventAggregator;
|
||||||
private readonly NBXplorerDashboard _Dashboard;
|
private readonly NBXplorerDashboard _Dashboard;
|
||||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||||
public string CreatedStoreId { get; set; }
|
|
||||||
|
|
||||||
[TempData]
|
[TempData]
|
||||||
public bool StoreNotConfigured
|
public bool StoreNotConfigured
|
||||||
|
@ -589,58 +588,6 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{storeId}/payment")]
|
|
||||||
public IActionResult Payment()
|
|
||||||
{
|
|
||||||
var store = HttpContext.GetStoreData();
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var storeBlob = store.GetStoreBlob();
|
|
||||||
var vm = new PaymentViewModel
|
|
||||||
{
|
|
||||||
Id = store.Id,
|
|
||||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
|
||||||
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
|
||||||
PaymentTolerance = storeBlob.PaymentTolerance,
|
|
||||||
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
|
|
||||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
|
||||||
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays
|
|
||||||
};
|
|
||||||
|
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{storeId}/payment")]
|
|
||||||
public async Task<IActionResult> Payment(PaymentViewModel model, string command = null)
|
|
||||||
{
|
|
||||||
bool needUpdate = false;
|
|
||||||
var blob = CurrentStore.GetStoreBlob();
|
|
||||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
|
||||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
|
||||||
blob.PaymentTolerance = model.PaymentTolerance;
|
|
||||||
blob.DefaultCurrency = model.DefaultCurrency;
|
|
||||||
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
|
||||||
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
|
|
||||||
|
|
||||||
if (CurrentStore.SetStoreBlob(blob))
|
|
||||||
{
|
|
||||||
needUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needUpdate)
|
|
||||||
{
|
|
||||||
await _Repo.UpdateStore(CurrentStore);
|
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated";
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction(nameof(Payment), new
|
|
||||||
{
|
|
||||||
storeId = CurrentStore.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{storeId}/settings")]
|
[HttpGet("{storeId}/settings")]
|
||||||
public IActionResult GeneralSettings()
|
public IActionResult GeneralSettings()
|
||||||
{
|
{
|
||||||
|
@ -648,11 +595,18 @@ namespace BTCPayServer.Controllers
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
var storeBlob = store.GetStoreBlob();
|
||||||
var vm = new GeneralSettingsViewModel
|
var vm = new GeneralSettingsViewModel
|
||||||
{
|
{
|
||||||
Id = store.Id,
|
Id = store.Id,
|
||||||
StoreName = store.StoreName,
|
StoreName = store.StoreName,
|
||||||
StoreWebsite = store.StoreWebsite,
|
StoreWebsite = store.StoreWebsite,
|
||||||
|
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||||
|
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
||||||
|
PaymentTolerance = storeBlob.PaymentTolerance,
|
||||||
|
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
|
||||||
|
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||||
|
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays,
|
||||||
CanDelete = _Repo.CanDeleteStores()
|
CanDelete = _Repo.CanDeleteStores()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -675,6 +629,19 @@ namespace BTCPayServer.Controllers
|
||||||
CurrentStore.StoreWebsite = model.StoreWebsite;
|
CurrentStore.StoreWebsite = model.StoreWebsite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var blob = CurrentStore.GetStoreBlob();
|
||||||
|
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||||
|
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||||
|
blob.PaymentTolerance = model.PaymentTolerance;
|
||||||
|
blob.DefaultCurrency = model.DefaultCurrency;
|
||||||
|
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
||||||
|
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
|
||||||
|
|
||||||
|
if (CurrentStore.SetStoreBlob(blob))
|
||||||
|
{
|
||||||
|
needUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (needUpdate)
|
if (needUpdate)
|
||||||
{
|
{
|
||||||
await _Repo.UpdateStore(CurrentStore);
|
await _Repo.UpdateStore(CurrentStore);
|
||||||
|
@ -1002,7 +969,7 @@ namespace BTCPayServer.Controllers
|
||||||
CurrentStore.SetStoreBlob(blob);
|
CurrentStore.SetStoreBlob(blob);
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Feature disabled";
|
TempData[WellKnownTempData.SuccessMessage] = "Feature disabled";
|
||||||
await _Repo.UpdateStore(CurrentStore);
|
await _Repo.UpdateStore(CurrentStore);
|
||||||
return RedirectToAction(nameof(Payment), new { storeId = storeId });
|
return RedirectToAction(nameof(GeneralSettings), new { storeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("{storeId}/paybutton")]
|
[Route("{storeId}/paybutton")]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
@ -5,12 +7,13 @@ using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using ExchangeSharp;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
|
@ -20,20 +23,30 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
private readonly StoreRepository _repo;
|
private readonly StoreRepository _repo;
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly RateFetcher _rateFactory;
|
||||||
|
public string CreatedStoreId { get; set; }
|
||||||
|
|
||||||
public UIUserStoresController(
|
public UIUserStoresController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
StoreRepository storeRepository)
|
StoreRepository storeRepository,
|
||||||
|
RateFetcher rateFactory)
|
||||||
{
|
{
|
||||||
_repo = storeRepository;
|
_repo = storeRepository;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_rateFactory = rateFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("create")]
|
[HttpGet("create")]
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
||||||
public IActionResult CreateStore()
|
public IActionResult CreateStore()
|
||||||
{
|
{
|
||||||
return View();
|
var vm = new CreateStoreViewModel
|
||||||
|
{
|
||||||
|
DefaultCurrency = StoreBlob.StandardDefaultCurrency,
|
||||||
|
Exchanges = GetExchangesSelectList(CoinGeckoRateProvider.CoinGeckoName)
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("create")]
|
[HttpPost("create")]
|
||||||
|
@ -42,10 +55,13 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
|
vm.Exchanges = GetExchangesSelectList(vm.PreferredExchange);
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
var store = await _repo.CreateStore(GetUserId(), vm.Name);
|
|
||||||
|
var store = await _repo.CreateStore(GetUserId(), vm.Name, vm.DefaultCurrency, vm.PreferredExchange);
|
||||||
CreatedStoreId = store.Id;
|
CreatedStoreId = store.Id;
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
|
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
|
||||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new
|
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new
|
||||||
{
|
{
|
||||||
|
@ -53,11 +69,6 @@ namespace BTCPayServer.Controllers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CreatedStoreId
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{storeId}/me/delete")]
|
[HttpGet("{storeId}/me/delete")]
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
|
||||||
public IActionResult DeleteStore(string storeId)
|
public IActionResult DeleteStore(string storeId)
|
||||||
|
@ -82,5 +93,14 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetUserId() => _userManager.GetUserId(User);
|
private string GetUserId() => _userManager.GetUserId(User);
|
||||||
|
|
||||||
|
private SelectList GetExchangesSelectList(string selected) {
|
||||||
|
var exchanges = _rateFactory.RateProviderFactory
|
||||||
|
.GetSupportedExchanges()
|
||||||
|
.Where(r => !string.IsNullOrWhiteSpace(r.Name))
|
||||||
|
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var chosen = exchanges.FirstOrDefault(f => f.Id == selected) ?? exchanges.First();
|
||||||
|
return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.Name), chosen.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
public class StoreBlob
|
public class StoreBlob
|
||||||
{
|
{
|
||||||
|
public static string StandardDefaultCurrency = "USD";
|
||||||
|
|
||||||
public StoreBlob()
|
public StoreBlob()
|
||||||
{
|
{
|
||||||
InvoiceExpiration = TimeSpan.FromMinutes(15);
|
InvoiceExpiration = TimeSpan.FromMinutes(15);
|
||||||
|
@ -28,7 +30,6 @@ namespace BTCPayServer.Data
|
||||||
PaymentMethodCriteria = new List<PaymentMethodCriteria>();
|
PaymentMethodCriteria = new List<PaymentMethodCriteria>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||||
public NetworkFeeMode NetworkFeeMode { get; set; }
|
public NetworkFeeMode NetworkFeeMode { get; set; }
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@ namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(_DefaultCurrency) ? "USD" : _DefaultCurrency;
|
return string.IsNullOrEmpty(_DefaultCurrency) ? StandardDefaultCurrency : _DefaultCurrency;
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
@ -169,8 +170,6 @@ namespace BTCPayServer.Data
|
||||||
public EmailSettings EmailSettings { get; set; }
|
public EmailSettings EmailSettings { get; set; }
|
||||||
public bool PayJoinEnabled { get; set; }
|
public bool PayJoinEnabled { get; set; }
|
||||||
|
|
||||||
public StoreHints Hints { get; set; }
|
|
||||||
|
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||||
|
|
||||||
|
@ -179,12 +178,6 @@ namespace BTCPayServer.Data
|
||||||
[JsonConverter(typeof(TimeSpanJsonConverter.Days))]
|
[JsonConverter(typeof(TimeSpanJsonConverter.Days))]
|
||||||
public TimeSpan RefundBOLT11Expiration { get; set; }
|
public TimeSpan RefundBOLT11Expiration { get; set; }
|
||||||
|
|
||||||
public class StoreHints
|
|
||||||
{
|
|
||||||
public bool Wallet { get; set; }
|
|
||||||
public bool Lightning { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPaymentFilter GetExcludedPaymentMethods()
|
public IPaymentFilter GetExcludedPaymentMethods()
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
|
|
@ -50,9 +50,6 @@ namespace BTCPayServer.Data
|
||||||
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
|
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
|
||||||
if (result.PreferredExchange == null)
|
if (result.PreferredExchange == null)
|
||||||
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
|
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
|
||||||
|
|
||||||
if (result.Hints == null)
|
|
||||||
result.Hints = new StoreBlob.StoreHints();
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
{
|
{
|
||||||
|
@ -7,9 +8,17 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
[MinLength(1)]
|
[MinLength(1)]
|
||||||
public string Name
|
public string Name { get; set; }
|
||||||
{
|
|
||||||
get; set;
|
[Required]
|
||||||
}
|
[MaxLength(10)]
|
||||||
|
[Display(Name = "Default currency")]
|
||||||
|
public string DefaultCurrency { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Display(Name = "Preferred Price Source")]
|
||||||
|
public string PreferredExchange { get; set; }
|
||||||
|
|
||||||
|
public SelectList Exchanges { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Validation;
|
using BTCPayServer.Validation;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
|
@ -21,5 +22,27 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
public string StoreWebsite { get; set; }
|
public string StoreWebsite { get; set; }
|
||||||
|
|
||||||
public bool CanDelete { get; set; }
|
public bool CanDelete { 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 = "Add additional fee (network fee) to invoice …")]
|
||||||
|
public NetworkFeeMode NetworkFeeMode { 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; }
|
||||||
|
|
||||||
|
[Display(Name = "Minimum acceptable expiration time for BOLT11 for refunds")]
|
||||||
|
[Range(1, 365 * 10)]
|
||||||
|
public long BOLT11Expiration { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,27 +16,15 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
public string Rule { get; set; }
|
public string Rule { get; set; }
|
||||||
public bool Error { get; set; }
|
public bool Error { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange)
|
public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange)
|
||||||
{
|
{
|
||||||
var defaultStore = preferredExchange ?? CoinGeckoRateProvider.CoinGeckoName;
|
var defaultStore = preferredExchange ?? CoinGeckoRateProvider.CoinGeckoName;
|
||||||
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, a.SourceId, GetName(a), a.Url, a.Source)).ToArray();
|
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, a.SourceId, a.DisplayName, a.Url, a.Source)).ToArray();
|
||||||
var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault();
|
var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault();
|
||||||
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
|
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
|
||||||
PreferredExchange = chosen.Id;
|
PreferredExchange = chosen?.Id;
|
||||||
RateSource = chosen.Url;
|
RateSource = chosen?.Url;
|
||||||
}
|
|
||||||
|
|
||||||
private string GetName(AvailableRateProvider a)
|
|
||||||
{
|
|
||||||
switch (a.Source)
|
|
||||||
{
|
|
||||||
case Rating.RateSource.Direct:
|
|
||||||
return a.Name;
|
|
||||||
case Rating.RateSource.Coingecko:
|
|
||||||
return $"{a.Name} (via CoinGecko)";
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException(a.Source.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TestResultViewModel> TestRateRules { get; set; }
|
public List<TestResultViewModel> TestRateRules { get; set; }
|
||||||
|
@ -56,19 +44,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
|
|
||||||
[Display(Name = "Add Exchange Rate Spread")]
|
[Display(Name = "Add Exchange Rate Spread")]
|
||||||
[Range(0.0, 100.0)]
|
[Range(0.0, 100.0)]
|
||||||
public double Spread
|
public double Spread { get; set; }
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Display(Name = "Preferred Price Source")]
|
[Display(Name = "Preferred Price Source")]
|
||||||
public string PreferredExchange { get; set; }
|
public string PreferredExchange { get; set; }
|
||||||
|
|
||||||
public string RateSource
|
public string RateSource { get; set; }
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,17 +152,6 @@ namespace BTCPayServer.Services.Stores
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetNewStoreHints(ref StoreData storeData)
|
|
||||||
{
|
|
||||||
var blob = storeData.GetStoreBlob();
|
|
||||||
blob.Hints = new Data.StoreBlob.StoreHints
|
|
||||||
{
|
|
||||||
Wallet = true,
|
|
||||||
Lightning = true
|
|
||||||
};
|
|
||||||
storeData.SetStoreBlob(blob);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateStore(string ownerId, StoreData storeData)
|
public async Task CreateStore(string ownerId, StoreData storeData)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(storeData.Id))
|
if (!string.IsNullOrEmpty(storeData.Id))
|
||||||
|
@ -179,17 +168,19 @@ namespace BTCPayServer.Services.Stores
|
||||||
Role = StoreRoles.Owner,
|
Role = StoreRoles.Owner,
|
||||||
};
|
};
|
||||||
|
|
||||||
SetNewStoreHints(ref storeData);
|
|
||||||
|
|
||||||
ctx.Add(storeData);
|
ctx.Add(storeData);
|
||||||
ctx.Add(userStore);
|
ctx.Add(userStore);
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StoreData> CreateStore(string ownerId, string name)
|
public async Task<StoreData> CreateStore(string ownerId, string name, string defaultCurrency, string preferredExchange)
|
||||||
{
|
{
|
||||||
var store = new StoreData() { StoreName = name };
|
var store = new StoreData { StoreName = name };
|
||||||
SetNewStoreHints(ref store);
|
var blob = store.GetStoreBlob();
|
||||||
|
blob.DefaultCurrency = defaultCurrency;
|
||||||
|
blob.PreferredExchange = preferredExchange;
|
||||||
|
store.SetStoreBlob(blob);
|
||||||
|
|
||||||
await CreateStore(ownerId, store);
|
await CreateStore(ownerId, store);
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||||
@{
|
@{
|
||||||
var store = Context.GetStoreData();
|
var store = Context.GetStoreData();
|
||||||
|
var cryptoCode = "BTC";
|
||||||
var isLightningEnabled = store.IsLightningEnabled(BTCPayNetworkProvider);
|
var isLightningEnabled = store.IsLightningEnabled(BTCPayNetworkProvider);
|
||||||
var isLNUrlEnabled = store.IsLNUrlEnabled(BTCPayNetworkProvider);
|
var isLNUrlEnabled = store.IsLNUrlEnabled(BTCPayNetworkProvider);
|
||||||
var possible =
|
var possible =
|
||||||
isLightningEnabled &&
|
isLightningEnabled &&
|
||||||
isLNUrlEnabled &&
|
isLNUrlEnabled &&
|
||||||
store.GetSupportedPaymentMethods(BTCPayNetworkProvider).OfType<LNURLPaySupportedPaymentMethod>().Any(type => type.CryptoCode == "BTC");
|
store.GetSupportedPaymentMethods(BTCPayNetworkProvider).OfType<LNURLPaySupportedPaymentMethod>().Any(type => type.CryptoCode == cryptoCode);
|
||||||
}
|
}
|
||||||
<li class="list-group-item bg-tile" id="lightning-address-option">
|
<li class="list-group-item bg-tile" id="lightning-address-option">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
{
|
{
|
||||||
if (!isLightningEnabled)
|
if (!isLightningEnabled)
|
||||||
{
|
{
|
||||||
<a asp-action="Payment" asp-controller="UIStores" asp-route-storeId="@store.Id" class="btn btn-link p-0">
|
<a asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-cryptoCode="@cryptoCode" asp-route-storeId="@store.Id" class="btn btn-link p-0">
|
||||||
You need to setup Lightning first
|
You need to setup Lightning first
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
<td>
|
<td>
|
||||||
@if (app.IsOwner)
|
@if (app.IsOwner)
|
||||||
{
|
{
|
||||||
<span><a asp-action="Payment" asp-controller="UIStores" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
|
<span><a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,14 +24,13 @@
|
||||||
{
|
{
|
||||||
supported = null;
|
supported = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (supported is null)
|
@if (supported is null)
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert">
|
<div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert">
|
||||||
LNURL is not enabled on your store, which this print feature relies on.
|
LNURL is not enabled on your store, which this print feature relies on.
|
||||||
<a asp-action="Payment" asp-controller="UIStores" asp-route-storeId="@Model.Store.Id" class="alert-link p-0">
|
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="alert-link p-0">
|
||||||
Enable LNURL
|
Enable LNURL
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -63,7 +63,7 @@ else
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a asp-controller="UIStores" asp-action="Payment" asp-route-storeId="@Model.StoreId" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.StoreId" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||||
<vc:icon symbol="new-wallet"/>
|
<vc:icon symbol="new-wallet"/>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h5 class="mb-0">Set up a wallet</h5>
|
<h5 class="mb-0">Set up a wallet</h5>
|
||||||
|
@ -95,7 +95,7 @@ else
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a asp-controller="UIStores" asp-action="Payment" asp-route-storeId="@Model.StoreId" id="SetupGuide-Lightning" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.StoreId" id="SetupGuide-Lightning" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||||
<vc:icon symbol="new-wallet"/>
|
<vc:icon symbol="new-wallet"/>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h5 class="mb-0">Set up a Lightning node</h5>
|
<h5 class="mb-0">Set up a Lightning node</h5>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-10 col-xl-8">
|
||||||
@if (!ViewContext.ModelState.IsValid)
|
@if (!ViewContext.ModelState.IsValid)
|
||||||
{
|
{
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
@ -27,8 +27,64 @@
|
||||||
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
|
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3 class="mt-5 mb-3">Payment</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="DefaultCurrency" class="form-label"></label>
|
||||||
|
<input asp-for="DefaultCurrency" class="form-control" currency-selection style="max-width:10ch;" />
|
||||||
|
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<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="BOLT11Expiration" class="form-label"></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input asp-for="BOLT11Expiration" class="form-control" style="max-width:10ch;"/>
|
||||||
|
<span class="input-group-text">days</span>
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="BOLT11Expiration" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button name="command" type="submit" class="btn btn-primary mt-2" value="Save" id="Save">Save</button>
|
<button name="command" type="submit" class="btn btn-primary mt-2" value="Save" id="Save">Save</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h3 class="mt-5 mb-3">Services</h3>
|
<h3 class="mt-5 mb-3">Services</h3>
|
||||||
<div class="table-responsive-md">
|
<div class="table-responsive-md">
|
||||||
<table class="table table-hover mt-1 mb-5">
|
<table class="table table-hover mt-1 mb-5">
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
@model PaymentViewModel
|
|
||||||
@{
|
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
|
||||||
ViewData.SetActivePage(StoreNavPages.Payment, "Wallets", Context.GetStoreData().Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-10 col-xl-9">
|
|
||||||
@if (!ViewContext.ModelState.IsValid)
|
|
||||||
{
|
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
|
||||||
}
|
|
||||||
<form method="post">
|
|
||||||
<h3 class="mb-3">Payment</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="DefaultCurrency" class="form-label"></label>
|
|
||||||
<input asp-for="DefaultCurrency" currency-selection class="form-control" style="max-width:10ch;" />
|
|
||||||
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<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 w-auto">
|
|
||||||
<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="BOLT11Expiration" class="form-label"></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input asp-for="BOLT11Expiration" class="form-control" style="max-width:10ch;"/>
|
|
||||||
<span class="input-group-text">days</span>
|
|
||||||
</div>
|
|
||||||
<span asp-validation-for="BOLT11Expiration" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<button name="command" type="submit" class="btn btn-primary px-4 mt-3" value="Save" id="Save">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section PageFootContent {
|
|
||||||
<partial name="_ValidationScriptsPartial" />
|
|
||||||
}
|
|
|
@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Stores
|
||||||
{
|
{
|
||||||
public enum StoreNavPages
|
public enum StoreNavPages
|
||||||
{
|
{
|
||||||
Create, Dashboard, Rates, Payment, OnchainSettings, LightningSettings, Lightning, CheckoutAppearance, General, Tokens, Users, PayButton, Integrations, Webhooks, PullPayments, Payouts
|
Create, Dashboard, General, Rates, OnchainSettings, LightningSettings, Lightning, CheckoutAppearance, Tokens, Users, PayButton, Integrations, Webhooks, PullPayments, Payouts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
@section Navbar {
|
@section Navbar {
|
||||||
@await RenderSectionAsync("Navbar", false)
|
@await RenderSectionAsync("Navbar", false)
|
||||||
|
|
||||||
<a asp-controller="UIStores" asp-action="Payment" asp-route-storeId="@Context.GetRouteValue("storeId")" class="cancel">
|
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")" class="cancel">
|
||||||
<vc:icon symbol="close" />
|
<vc:icon symbol="close" />
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
<nav id="SectionNav">
|
<nav id="SectionNav">
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.General))" class="nav-link @ViewData.IsActivePage(StoreNavPages.General)" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.General))" class="nav-link @ViewData.IsActivePage(StoreNavPages.General)" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Payment))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payment)" asp-controller="UIStores" asp-action="Payment" asp-route-storeId="@Context.GetRouteValue("storeId")">Payment</a>
|
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout Appearance</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout Appearance</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
|
||||||
|
|
|
@ -19,6 +19,16 @@
|
||||||
<input asp-for="Name" class="form-control" required />
|
<input asp-for="Name" class="form-control" required />
|
||||||
<span asp-validation-for="Name" class="text-danger"></span>
|
<span asp-validation-for="Name" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="DefaultCurrency" class="form-label" data-required></label>
|
||||||
|
<input asp-for="DefaultCurrency" class="form-control" currency-selection style="max-width:10ch;" />
|
||||||
|
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="PreferredExchange" class="form-label" data-required></label>
|
||||||
|
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-select w-auto"></select>
|
||||||
|
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
<div class="form-group mt-4">
|
<div class="form-group mt-4">
|
||||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
@if (wallet.IsOwner)
|
@if (wallet.IsOwner)
|
||||||
{
|
{
|
||||||
<td><a asp-action="Payment" asp-controller="UIStores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
|
<td><a asp-action="GeneralSettings" asp-controller="UIStores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue