Merge pull request #1990 from btcpayserver/feat/store-hints

Store hints/warnings
This commit is contained in:
rockstardev 2020-10-18 03:11:36 -05:00 committed by GitHub
commit b430afe3e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 379 additions and 274 deletions

View file

@ -42,18 +42,19 @@ namespace BTCPayServer.Tests
{
await Server.StartAsync();
ChromeOptions options = new ChromeOptions();
var isDebug = !Server.PayTester.InContainer;
if (Server.PayTester.InContainer)
{
// this must be first option https://stackoverflow.com/questions/53073411/selenium-webdriverexceptionchrome-failed-to-start-crashed-as-google-chrome-is#comment102570662_53073789
options.AddArgument("no-sandbox");
}
var isDebug = !Server.PayTester.InContainer;
if (!isDebug)
{
options.AddArguments("headless"); // Comment to view browser
options.AddArguments("window-size=1200x1000"); // Comment to view browser
}
options.AddArgument("shm-size=2g");
if (Server.PayTester.InContainer)
{
options.AddArgument("no-sandbox");
}
Driver = new ChromeDriver(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory(), options);
if (isDebug)
{
@ -228,7 +229,11 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Email")).SendKeys(user);
Driver.FindElement(By.Id("Password")).SendKeys(password);
Driver.FindElement(By.Id("LoginButton")).Click();
}
public void GoToStores()
{
Driver.FindElement(By.Id("Stores")).Click();
}
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.Index)
@ -303,7 +308,7 @@ namespace BTCPayServer.Tests
public string CreateInvoice(string storeName, decimal amount = 100, string currency = "USD", string refundEmail = "")
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Driver.FindElement(By.Id("CreateNewInvoice")).Click(); // ocassionally gets stuck for some reason, tried force click and wait for element
Driver.FindElement(By.Id("Amount")).SendKeys(amount.ToString(CultureInfo.InvariantCulture));
var currencyEl = Driver.FindElement(By.Id("Currency"));
currencyEl.Clear();

View file

@ -300,14 +300,30 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
var alice = s.RegisterNewUser();
var store = s.CreateNewStore().storeName;
s.AddDerivationScheme();
var storeData = s.CreateNewStore();
// verify that hints are displayed on the store page
Assert.True(s.Driver.PageSource.Contains("Wallet not setup for the store, please provide Derviation Scheme"),
"Wallet hint not present");
Assert.True(s.Driver.PageSource.Contains("Review settings if you want to receive Lightning payments"),
"Lightning hint not present");
s.GoToStores();
Assert.True(s.Driver.PageSource.Contains("warninghint_" + storeData.storeId),
"Warning hint on list not present");
s.GoToStore(storeData.storeId);
s.AddDerivationScheme(); // wallet hint should be dismissed
s.Driver.AssertNoError();
Assert.Contains(store, s.Driver.PageSource);
Assert.False(s.Driver.PageSource.Contains("Wallet not setup for the store, please provide Derviation Scheme"),
"Wallet hint not dismissed on derivation scheme add");
s.Driver.FindElement(By.Id("dismissLightningHint")).Click(); // dismiss lightning hint
Assert.Contains(storeData.storeName, s.Driver.PageSource);
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
s.GoToInvoices();
var invoiceId = s.CreateInvoice(store);
var invoiceId = s.CreateInvoice(storeData.storeName);
s.AssertHappyMessage();
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
var invoiceUrl = s.Driver.Url;
@ -362,6 +378,11 @@ namespace BTCPayServer.Tests
s.Logout();
LogIn(s, alice);
s.Driver.FindElement(By.Id("Stores")).Click();
// there shouldn't be any hints now
Assert.False(s.Driver.PageSource.Contains("Review settings if you want to receive Lightning payments"),
"Lightning hint should be dismissed at this point");
s.Driver.FindElement(By.LinkText("Remove")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.Id("Stores")).Click();

View file

@ -167,7 +167,7 @@ namespace BTCPayServer.Controllers
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is clicking on continue after changing the config
(!vm.Confirmation && oldConfig != vm.Config) ||
// - The user is clickingon continue without changing config nor enabling/disabling
// - The user is clicking on continue without changing config nor enabling/disabling
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
showAddress = showAddress && strategy != null;
@ -179,6 +179,7 @@ namespace BTCPayServer.Controllers
await wallet.TrackAsync(strategy.AccountDerivation);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.Hints.Wallet = false;
store.SetStoreBlob(storeBlob);
}
catch
@ -202,6 +203,7 @@ namespace BTCPayServer.Controllers
{
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} has been modified.";
}
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else if (!string.IsNullOrEmpty(vm.HintAddress))

View file

@ -137,6 +137,7 @@ namespace BTCPayServer.Controllers
case "save":
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
storeBlob.Hints.Lightning = false;
store.SetStoreBlob(storeBlob);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);

View file

@ -372,8 +372,8 @@ namespace BTCPayServer.Controllers
new PaymentMethodCriteriaViewModel()
{
PaymentMethod = criteria.PaymentMethod.ToString(),
Type = criteria.Above? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan : PaymentMethodCriteriaViewModel.CriteriaType.LessThan,
Value = criteria.Value?.ToString()?? ""
Type = criteria.Above ? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan : PaymentMethodCriteriaViewModel.CriteriaType.LessThan,
Value = criteria.Value?.ToString() ?? ""
}).ToList();
vm.CustomCSS = storeBlob.CustomCSS;
vm.CustomLogo = storeBlob.CustomLogo;
@ -394,7 +394,9 @@ namespace BTCPayServer.Controllers
.Select(o =>
new CheckoutExperienceViewModel.Format()
{
Name = o.ToPrettyString(), Value = o.ToString(), PaymentId = o
Name = o.ToPrettyString(),
Value = o.ToString(),
PaymentId = o
}).ToArray();
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
@ -439,7 +441,7 @@ namespace BTCPayServer.Controllers
.Where(viewModel => !string.IsNullOrEmpty(viewModel.Value)).Select(viewModel =>
{
CurrencyValue.TryParse(viewModel.Value, out var cv);
return new PaymentMethodCriteria() {Above = viewModel.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, Value = cv, PaymentMethod = PaymentMethodId.Parse(viewModel.PaymentMethod)};
return new PaymentMethodCriteria() { Above = viewModel.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, Value = cv, PaymentMethod = PaymentMethodId.Parse(viewModel.PaymentMethod) };
}).ToList();
#pragma warning disable 612
blob.LightningMaxValue = null;
@ -471,32 +473,6 @@ namespace BTCPayServer.Controllers
});
}
[HttpGet]
[Route("{storeId}")]
public IActionResult UpdateStore()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
return View(vm);
}
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
{
@ -556,6 +532,36 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("{storeId}")]
public IActionResult UpdateStore()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
vm.HintWallet = storeBlob.Hints.Wallet;
vm.HintLightning = storeBlob.Hints.Lightning;
return View(vm);
}
[HttpPost]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
@ -625,7 +631,6 @@ namespace BTCPayServer.Controllers
{
storeId = CurrentStore.Id
});
}
[HttpGet]
@ -974,10 +979,30 @@ namespace BTCPayServer.Controllers
{
storeId = CurrentStore.Id
});
}
[HttpPost]
[Route("{storeId}/dismissHint")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> DismissHint(string id)
{
var blob = CurrentStore.GetStoreBlob();
if (id == "Wallet" || id == "Lightning")
{
try
{
var prop = blob.Hints.GetType().GetProperty(id);
prop.SetValue(blob.Hints, false);
}
// disregard parse errors
catch { }
if (CurrentStore.SetStoreBlob(blob))
{
await _Repo.UpdateStore(CurrentStore);
}
}
return Content("ack");
}
}
}

View file

@ -5,6 +5,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using ExchangeSharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -37,6 +38,23 @@ namespace BTCPayServer.Controllers
return View();
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
storeId = store.Id
});
}
public string CreatedStoreId
{
get; set;
@ -108,34 +126,20 @@ namespace BTCPayServer.Controllers
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
var blob = store.GetStoreBlob();
result.Stores.Add(new StoresViewModel.StoreViewModel()
{
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
IsOwner = store.Role == StoreRoles.Owner
IsOwner = store.Role == StoreRoles.Owner,
HintWalletWarning = blob.Hints.Wallet
});
}
return View(result);
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
storeId = store.Id
});
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View file

@ -188,6 +188,13 @@ namespace BTCPayServer.Data
public bool RedirectAutomatically { get; set; }
public bool PayJoinEnabled { get; set; }
public StoreHints Hints { get; set; }
public class StoreHints
{
public bool Wallet { get; set; }
public bool Lightning { get; set; }
}
public IPaymentFilter GetExcludedPaymentMethods()
{
#pragma warning disable CS0618 // Type or member is obsolete

View file

@ -46,6 +46,9 @@ namespace BTCPayServer.Data
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
if (result.PreferredExchange == null)
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
if (result.Hints == null)
result.Hints = new StoreBlob.StoreHints();
return result;
}

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Client.Models;
using BTCPayServer.Validation;
using static BTCPayServer.Data.StoreBlob;
namespace BTCPayServer.Models.StoreViewModels
{
@ -90,6 +91,9 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Enable Payjoin/P2EP")]
public bool PayJoinEnabled { get; set; }
public bool HintWallet { get; set; }
public bool HintLightning { get; set; }
public class LightningNode
{
public string CryptoCode { get; set; }

View file

@ -4,32 +4,15 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoresViewModel
{
public List<StoreViewModel> Stores
{
get; set;
} = new List<StoreViewModel>();
public List<StoreViewModel> Stores { get; set; } = new List<StoreViewModel>();
public class StoreViewModel
{
public string Name
{
get; set;
}
public string WebSite
{
get; set;
}
public string Id
{
get; set;
}
public bool IsOwner
{
get;
set;
}
public string Id { get; set; }
public string Name { get; set; }
public string WebSite { get; set; }
public bool IsOwner { get; set; }
public bool HintWalletWarning { get; set; }
}
}
}

View file

@ -171,7 +171,7 @@ namespace BTCPayServer.Services.Stores
{
StoreDataId = storeData.Id,
ApplicationUserId = ownerId,
Role = StoreRoles.Owner
Role = StoreRoles.Owner,
};
ctx.Add(storeData);
ctx.Add(userStore);
@ -182,6 +182,13 @@ namespace BTCPayServer.Services.Stores
public async Task<StoreData> CreateStore(string ownerId, string name)
{
var store = new StoreData() { StoreName = name };
var blob = store.GetStoreBlob();
blob.Hints = new Data.StoreBlob.StoreHints
{
Wallet = true,
Lightning = true
};
store.SetStoreBlob(blob);
await CreateStore(ownerId, store);
return store;
}

View file

@ -29,7 +29,7 @@
<div class="row">
<div class="col-12 col-sm-4 col-lg-6 mb-3">
<a asp-action="CreateInvoice" class="btn btn-primary mb-1" role="button" id="CreateNewInvoice">
<a id="CreateNewInvoice" asp-action="CreateInvoice" class="btn btn-primary mb-1">
<span class="fa fa-plus"></span>
Create an invoice
</a>

View file

@ -108,20 +108,20 @@
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
<a class="btn btn-secondary" asp-route-id="@this.Context.GetRouteValue("id")" asp-action="ClonePaymentRequest" id="@Model.Id">Clone</a>
@if (!Model.Archived)
{
<a class="btn btn-secondary" data-toggle="tooltip" title="Archive this payment request so that it does not appear in the payment request list by default" asp-route-id="@this.Context.GetRouteValue("id")" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival">Archive</a>
{
<a class="btn btn-secondary" data-toggle="tooltip" title="Archive this payment request so that it does not appear in the payment request list by default" asp-route-id="@this.Context.GetRouteValue("id")" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival">Archive</a>
}
else
{
<a class="btn btn-secondary" data-toggle="tooltip" title="Unarchive this payment request" asp-route-id="@this.Context.GetRouteValue("id")" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival" >Unarchive</a>
<a class="btn btn-secondary" data-toggle="tooltip" title="Unarchive this payment request" asp-route-id="@this.Context.GetRouteValue("id")" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival">Unarchive</a>
}
}
<a class="btn btn-secondary" target="_blank" asp-action="GetPaymentRequests">Back to list</a>
</div>
</form>
<a asp-action="GetPaymentRequests">Back to List</a>
</div>
</div>
</div>

View file

@ -1,11 +1,11 @@
@{
@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
if (!ViewData.ContainsKey("NavPartialName"))
{
ViewData["NavPartialName"] = "_Nav";
}
var title = $"{(ViewData.ContainsKey("MainTitle")? $"{ViewData["MainTitle"]}:" : String.Empty)} {ViewData["Title"]}";
var title = $"{(ViewData.ContainsKey("MainTitle") ? $"{ViewData["MainTitle"]}:" : String.Empty)} {ViewData["Title"]}";
}
<section>

View file

@ -1,4 +1,4 @@
@model BTCPayServer.Models.StoreViewModels.RatesViewModel
@model BTCPayServer.Models.StoreViewModels.RatesViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Rates, "Rates");
@ -159,7 +159,7 @@
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
Current Rates source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
</p>
</div>
<div class="form-group">

View file

@ -1,122 +1,44 @@
@using System.Text.RegularExpressions
@using System.Text.RegularExpressions
@model StoreViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Profile");
ViewData.SetActivePageAndTitle(StoreNavPages.Index, Model.StoreName);
}
<partial name="_StatusMessage" />
@if (!ViewContext.ModelState.IsValid)
{
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
</div>
}
<div class="row">
<div class="col-md-8">
<form method="post">
<div class="mb-5">
<h4 class="mb-3">General</h4>
<div class="form-group">
<label asp-for="Id"></label>
<input asp-for="Id" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="StoreName"></label>
<input asp-for="StoreName" class="form-control" />
<span asp-validation-for="StoreName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreWebsite"></label>
<input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-3">Payment</h4>
<div class="form-group">
<div class="form-check">
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="form-check-input" />
<label asp-for="AnyoneCanCreateInvoice" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#allow-anyone-to-create-invoice" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="PayJoinEnabled" type="checkbox" class="form-check-input"/>
<label asp-for="PayJoinEnabled" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/Payjoin/" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<span asp-validation-for="PayJoinEnabled" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="NetworkFeeMode"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<select asp-for="NetworkFeeMode" class="form-control">
<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">
<div class="mb-2">
<label asp-for="InvoiceExpiration" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#invoice-expires-if-the-full-amount-has-not-been-paid-after-minutes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<div class="input-group">
<input asp-for="InvoiceExpiration" class="form-control" style="max-width:10ch;"/>
<div class="input-group-append">
<span class="input-group-text">minutes</span>
</div>
</div>
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<div class="mb-2">
<label asp-for="MonitoringExpiration" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#payment-invalid-if-transactions-fails-to-confirm-minutes-after-invoice-expiration" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<div class="input-group">
<input asp-for="MonitoringExpiration" class="form-control" style="max-width:10ch;"/>
<div class="input-group-append">
<span class="input-group-text">minutes</span>
</div>
</div>
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<div class="mb-2">
<label asp-for="PaymentTolerance" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#consider-the-invoice-paid-even-if-the-paid-amount-is-less-than-expected" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<div class="input-group">
<input asp-for="PaymentTolerance" class="form-control" style="max-width:10ch;"/>
<div class="input-group-append">
<span class="input-group-text">percent</span>
</div>
</div>
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
</div>
<div class="form-group">
<div class="mb-2">
<label asp-for="SpeedPolicy" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<select asp-for="SpeedPolicy" class="form-control w-auto">
<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>
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-3">Derivation Scheme</h4>
<p>The Derivation Scheme represents the destination of the funds received by your invoice on chain.</p>
<div>
<h4 class="mb-3">Wallet <span class="text-muted small">On-chain payments</span></h4>
@if (Model.HintWallet)
{
<p class="alert alert-warning">
<span class="fa fa-warning"></span>
Wallet not setup for the store, please provide Derviation Scheme
<button type="button" class="close only-for-js" data-dismiss="alert" aria-label="Close"
onclick="return dismissHint('Wallet', '@Model.Id');">
<span aria-hidden="true">&times;</span>
</button>
</p>
}
<p>
The Derivation Scheme <a href="https://docs.btcpayserver.org/FAQ/FAQ-Wallet/#what-is-a-derivation-scheme" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
facilitates generation of the destination addresses for your invoices so funds can be received on-chain.
</p>
<p>
Until wallet is defined, no invoices can be created for this store.
Optionally, you can have a store that only receives Lightning payments, see the next section for more details.
</p>
<table class="table table-sm table-responsive-md mt-0 mb-5">
<thead>
@ -128,7 +50,7 @@
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.DerivationSchemes.OrderBy(scheme => scheme.Collapsed))
@foreach (var scheme in Model.DerivationSchemes.OrderBy(scheme => scheme.Collapsed))
{
<tr class="@(@scheme.Collapsed? "collapsed": "")">
<td>@scheme.Crypto</td>
@ -152,7 +74,7 @@
}
</td>
<td style="text-align:center;">
@if(scheme.Enabled)
@if (scheme.Enabled)
{
<span class="text-success fa fa-check"></span>
}
@ -162,7 +84,7 @@
}
</td>
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value) && scheme.WalletSupported)
@if (!string.IsNullOrWhiteSpace(scheme.Value) && scheme.WalletSupported)
{
<a asp-action="WalletTransactions" asp-controller="Wallets" asp-route-walletId="@scheme.WalletId">Wallet</a><span> - </span>
}
@ -171,20 +93,29 @@
</tr>
}
@if (Model.DerivationSchemes.Any(scheme => scheme.Collapsed))
{
<tr class="only-for-js">
<td colspan="4"><button class="btn btn-link" id="toggle-assets" type="button">Show additional assets</button></td>
</tr>
}
@if (Model.DerivationSchemes.Any(scheme => scheme.Collapsed))
{
<tr class="only-for-js">
<td colspan="4"><button class="btn btn-link" id="toggle-assets" type="button">Show additional assets</button></td>
</tr>
}
</tbody>
</table>
<h4 class="mt-5 mb-3">Lightning nodes (Experimental)</h4>
<h4 class="mt-5 mb-3">Lightning <span class="text-muted small">Off-chain payments</span></h4>
@if (Model.HintLightning)
{
<p class="alert alert-warning">
<span class="fa fa-warning"></span>
Review settings if you want to receive Lightning payments
<button id="dismissLightningHint" type="button" class="close only-for-js" data-dismiss="alert" aria-label="Close"
onclick="return dismissHint('Lightning', '@Model.Id');">
<span aria-hidden="true">&times;</span>
</button>
</p>
}
<p>
A connection to a lightning charge node is required to generate lightning network enabled invoices.
<br />
This is experimental and not advised for production.
A connection to a Lightning Node is required to generate Lightning Network enabled invoices.
</p>
<table class="table table-sm table-responsive-md">
@ -197,7 +128,7 @@
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.LightningNodes)
@foreach (var scheme in Model.LightningNodes)
{
<tr>
<td>@scheme.CryptoCode</td>
@ -218,15 +149,110 @@
</tbody>
</table>
<div class="form-group mb-5">
<label asp-for="LightningDescriptionTemplate"></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>
<form method="post">
<div class="form-group mb-5">
<label asp-for="LightningDescriptionTemplate"></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>
<h4 class="mt-5 mb-3">Store settings</h4>
<div class="form-group">
<label asp-for="Id"></label>
<input asp-for="Id" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="StoreName"></label>
<input asp-for="StoreName" class="form-control" />
<span asp-validation-for="StoreName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreWebsite"></label>
<input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="form-check-input" />
<label asp-for="AnyoneCanCreateInvoice" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#allow-anyone-to-create-invoice" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="PayJoinEnabled" type="checkbox" class="form-check-input" />
<label asp-for="PayJoinEnabled" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/Payjoin/" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<span asp-validation-for="PayJoinEnabled" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="NetworkFeeMode"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<select asp-for="NetworkFeeMode" class="form-control">
<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">
<div class="mb-2">
<label asp-for="InvoiceExpiration" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#invoice-expires-if-the-full-amount-has-not-been-paid-after-minutes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<div class="input-group">
<input asp-for="InvoiceExpiration" class="form-control" style="max-width:10ch;" />
<div class="input-group-append">
<span class="input-group-text">minutes</span>
</div>
</div>
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<div class="mb-2">
<label asp-for="MonitoringExpiration" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#payment-invalid-if-transactions-fails-to-confirm-minutes-after-invoice-expiration" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<div class="input-group">
<input asp-for="MonitoringExpiration" class="form-control" style="max-width:10ch;" />
<div class="input-group-append">
<span class="input-group-text">minutes</span>
</div>
</div>
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<div class="mb-2">
<label asp-for="PaymentTolerance" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#consider-the-invoice-paid-even-if-the-paid-amount-is-less-than-expected" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<div class="input-group">
<input asp-for="PaymentTolerance" class="form-control" style="max-width:10ch;" />
<div class="input-group-append">
<span class="input-group-text">percent</span>
</div>
</div>
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
</div>
<div class="form-group">
<div class="mb-2">
<label asp-for="SpeedPolicy" class="d-inline"></label>
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Stores/#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<select asp-for="SpeedPolicy" class="form-control w-auto">
<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>
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
<button name="command" type="submit" class="btn btn-primary mt-3" value="Save" id="Save">Save Store Settings</button>
</form>
<h4 class="mt-5 mb-3">Additional Payment methods</h4>
<table class="table table-sm table-responsive-md mt-1 mb-5">
@ -238,23 +264,23 @@
</tr>
</thead>
<tbody>
@foreach (var scheme in Model.ThirdPartyPaymentMethods)
{
<tr>
<td>@scheme.Provider</td>
<td class="text-center">
@if (scheme.Enabled)
{
<span class="text-success fa fa-check"></span>
}
else
{
<span class="text-danger fa fa-times"></span>
}
</td>
<td class="text-right"><a asp-action="@scheme.Action" id='Modify-@scheme.Provider' asp-route-storeId="@this.Context.GetRouteValue("storeId")">Modify</a></td>
</tr>
}
@foreach (var scheme in Model.ThirdPartyPaymentMethods)
{
<tr>
<td>@scheme.Provider</td>
<td class="text-center">
@if (scheme.Enabled)
{
<span class="text-success fa fa-check"></span>
}
else
{
<span class="text-danger fa fa-times"></span>
}
</td>
<td class="text-right"><a asp-action="@scheme.Action" id='Modify-@scheme.Provider' asp-route-storeId="@this.Context.GetRouteValue("storeId")">Modify</a></td>
</tr>
}
</tbody>
</table>
@ -265,8 +291,8 @@
<th>Service</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
</thead>
<tbody>
<tr>
<td>
Email
@ -284,8 +310,7 @@
<a class="btn btn-outline-danger mb-5" asp-action="DeleteStore" asp-route-storeId="@Model.Id">Delete this store</a>
</div>
}
<button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save</button>
</form>
</div>
</div>
</div>
@ -293,12 +318,12 @@
@await Html.PartialAsync("_ValidationScriptsPartial")
<script>
$(document).ready(function(){
$(".collapsed").hide();
$("#toggle-assets").click(function() {
$(".collapsed").show();
$(this).parents("tr").hide();
});
});
$(document).ready(function () {
$(".collapsed").hide();
$("#toggle-assets").click(function () {
$(".collapsed").show();
$(this).parents("tr").hide();
});
});
</script>
}

View file

@ -13,3 +13,12 @@
}
</div>
<script type="text/javascript">
function dismissHint(hintId, storeId) {
var url = "@Url.Action("DismissHint", "Stores", new { storeId = "store_Id", id="hint_Id" })"
.replace("hint_Id", hintId)
.replace("store_Id", storeId);
$.post(url);
return true;
}
</script>

View file

@ -1,4 +1,4 @@
@model StoresViewModel
@model StoresViewModel
@{
ViewData["Title"] = "Stores";
var storeNameSortOrder = (string)ViewData["StoreNameSortOrder"];
@ -11,11 +11,11 @@
<div class="container">
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" />
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12 section-heading">
@ -37,25 +37,21 @@
<thead>
<tr>
<th>
<a
asp-action="ListStores"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName"
class="text-nowrap"
title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
<a asp-action="ListStores"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName"
class="text-nowrap"
title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)">
Name
<span class="fa @(storeNameSortOrder == "asc" ? "fa-sort-alpha-desc" : storeNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th>
<a
asp-action="ListStores"
asp-route-sortOrder="@(storeWebsiteSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreWebsite"
class="text-nowrap"
title="@(storeWebsiteSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
<a asp-action="ListStores"
asp-route-sortOrder="@(storeWebsiteSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreWebsite"
class="text-nowrap"
title="@(storeWebsiteSortOrder == "desc" ? sortByDesc : sortByAsc)">
Website
<span class="fa @(storeWebsiteSortOrder == "asc" ? "fa-sort-alpha-desc" : storeWebsiteSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
@ -67,7 +63,20 @@
@foreach (var store in Model.Stores)
{
<tr id="store-@store.Id">
<td>@store.Name</td>
<td>
@if (store.IsOwner)
{
<a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@store.Id">@store.Name</a>
}
else
{
@store.Name
}
@if (store.HintWalletWarning)
{
<span class="fa fa-warning text-warning" title="Wallet not setup for this store" id="warninghint_@store.Id"></span>
}
</td>
<td>
@if (!string.IsNullOrEmpty(store.WebSite))
{