Store-centric UI (#3091)

* Update layout structure and header

* Implement store selector

* Simplify homepage

* Update layout

* Use dropdown for store selector

* Hide global nav in store context

* Horizontal section nav

* Remove outer section and container from content views

* Update nav

* Set store context for invoice and payment request lists

* Test fixes

* Persist menu collapse state on client-side

* MainNav as view component

* Update app routes to incorporate store context

* Test fixes

* Display ticker for altcoins build only

* Plugins nav

* Incorporate category for active page as well

* Update invoice icon

* Add apps list to nav

* Add store context to app type controllers

* Incorporate id for active page as well

* Test fixes

* AppsController cleanup

* Nav: Display only apps for the current store

* Remove leftover from merge

* Nav styles optimization

* Left-align content container

* Increase sidebar padding on desktop

* Use min-width for store selector menu

* Store settings nav update

* Update app and payment request routes

* Test fixes

* Refactor MainNav component to use StoresController

* Set store context for invoice actions

* Cleanups

* Remove CurrentStore checks

The response will be "Access denied" in case the CookieAuthorizationHandler cannot resolve the store.

* Remove unnecessary store context setters

* Test fix
This commit is contained in:
d11n
2021-12-11 04:32:23 +01:00
committed by GitHub
parent 2b1436e303
commit f8e6b51e9d
79 changed files with 3782 additions and 3446 deletions

View File

@@ -9,6 +9,7 @@ namespace BTCPayServer.Abstractions.Extensions
{ {
private const string ACTIVE_CATEGORY_KEY = "ActiveCategory"; private const string ACTIVE_CATEGORY_KEY = "ActiveCategory";
private const string ACTIVE_PAGE_KEY = "ActivePage"; private const string ACTIVE_PAGE_KEY = "ActivePage";
private const string ACTIVE_ID_KEY = "ActiveId";
public static void SetActivePageAndTitle<T>(this ViewDataDictionary viewData, T activePage, string title = null, string mainTitle = null) public static void SetActivePageAndTitle<T>(this ViewDataDictionary viewData, T activePage, string title = null, string mainTitle = null)
where T : IConvertible where T : IConvertible
@@ -28,25 +29,38 @@ namespace BTCPayServer.Abstractions.Extensions
viewData[ACTIVE_CATEGORY_KEY] = activeCategory; viewData[ACTIVE_CATEGORY_KEY] = activeCategory;
} }
public static string IsActiveCategory<T>(this ViewDataDictionary viewData, T category) // TODO: Refactor this and merge it with SetActivePage
public static void SetActiveId<T>(this ViewDataDictionary viewData, T activeId)
{
viewData[ACTIVE_ID_KEY] = activeId;
}
public static string IsActiveCategory<T>(this ViewDataDictionary viewData, T category, object id = null)
{ {
if (!viewData.ContainsKey(ACTIVE_CATEGORY_KEY)) if (!viewData.ContainsKey(ACTIVE_CATEGORY_KEY))
{ {
return null; return null;
} }
var activeId = viewData[ACTIVE_ID_KEY];
var activeCategory = (T)viewData[ACTIVE_CATEGORY_KEY]; var activeCategory = (T)viewData[ACTIVE_CATEGORY_KEY];
return category.Equals(activeCategory) ? "active" : null; var categoryMatch = category.Equals(activeCategory);
var idMatch = id == null || activeId == null || id.Equals(activeId);
return categoryMatch && idMatch ? "active" : null;
} }
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page) public static string IsActivePage<T>(this ViewDataDictionary viewData, T page, object id = null)
where T : IConvertible where T : IConvertible
{ {
if (!viewData.ContainsKey(ACTIVE_PAGE_KEY)) if (!viewData.ContainsKey(ACTIVE_PAGE_KEY))
{ {
return null; return null;
} }
var activeId = viewData[ACTIVE_ID_KEY];
var activePage = (T)viewData[ACTIVE_PAGE_KEY]; var activePage = (T)viewData[ACTIVE_PAGE_KEY];
return page.Equals(activePage) ? "active" : null; var activeCategory = viewData[ACTIVE_CATEGORY_KEY];
var categoryAndPageMatch = activeCategory.Equals(activePage.GetType()) && page.Equals(activePage);
var idMatch = id == null || activeId == null || id.Equals(activeId);
return categoryAndPageMatch && idMatch ? "active" : null;
} }
public static HtmlString ToBrowserDate(this DateTimeOffset date) public static HtmlString ToBrowserDate(this DateTimeOffset date)

View File

@@ -1,21 +1,21 @@
@model BTCPayServer.Plugins.Test.TestPluginPageViewModel @model BTCPayServer.Plugins.Test.TestPluginPageViewModel
<section>
<div class="container">
<h1>Challenge Completed!!</h1>
Here is also an image loaded from the plugin<br/>
<a href="https://twitter.com/NicolasDorier/status/1307221679014256640">
<img src="/Resources/img/screengrab.png"/>
</a>
<div class="row"> <div class="container">
<h2>Persisted Data</h2> <h1>Challenge Completed!!</h1>
<p>The following is data persisted to the configured database but in an isolated DbContext. Every time you start BTCPay Server with this plugin enabled, a timestamp is logged.</p> Here is also an image loaded from the plugin<br/>
<ul class="list-group"> <a href="https://twitter.com/NicolasDorier/status/1307221679014256640">
@foreach (var item in Model.Data) <img src="/Resources/img/screengrab.png"/>
{ </a>
<li class="list-group-item">@item.Id at @item.Timestamp.ToString("F")</li>
} <div class="row">
</ul> <h2>Persisted Data</h2>
</div> <p>The following is data persisted to the configured database but in an isolated DbContext. Every time you start BTCPay Server with this plugin enabled, a timestamp is logged.</p>
<ul class="list-group">
@foreach (var item in Model.Data)
{
<li class="list-group-item">@item.Id at @item.Timestamp.ToString("F")</li>
}
</ul>
</div> </div>
</section> </div>

View File

@@ -607,15 +607,15 @@ namespace BTCPayServer.Tests
tester.ActivateLTC(); tester.ActivateLTC();
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC"); user.RegisterDerivationScheme("LTC");
var apps = user.GetController<AppsController>(); var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
vm.AppName = "test"; vm.AppName = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString(); vm.SelectedAppType = AppType.PointOfSale.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result); Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model) var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model)
.Apps[0].Id; .Apps[0].Id;
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model); .IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
@@ -658,13 +658,11 @@ donation:
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result); .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
// //
var invoices = user.BitPay.GetInvoices(); var invoices = await user.BitPay.GetInvoicesAsync();
var orangeInvoice = invoices.First(); var orangeInvoice = invoices.First();
Assert.Equal(10.00m, orangeInvoice.Price); Assert.Equal(10.00m, orangeInvoice.Price);
Assert.Equal("CAD", orangeInvoice.Currency); Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc); Assert.Equal("orange", orangeInvoice.ItemDesc);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result); .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
@@ -673,7 +671,6 @@ donation:
Assert.NotNull(appleInvoice); Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc); Assert.Equal("good apple", appleInvoice.ItemDesc);
// testing custom amount // testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps var action = Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result); .ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
@@ -772,7 +769,7 @@ noninventoryitem:
Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem"))); Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem")));
Assert.NotNull(inventoryItemInvoice); Assert.NotNull(inventoryItemInvoice);
//let's mark the inventoryitem invoice as invalid, thsi should return the item to back in stock //let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
var controller = tester.PayTester.GetController<InvoiceController>(user.UserId, user.StoreId); var controller = tester.PayTester.GetController<InvoiceController>(user.UserId, user.StoreId);
var appService = tester.PayTester.GetService<AppService>(); var appService = tester.PayTester.GetService<AppService>();
var eventAggregator = tester.PayTester.GetService<EventAggregator>(); var eventAggregator = tester.PayTester.GetService<EventAggregator>();
@@ -786,9 +783,7 @@ noninventoryitem:
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory); appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000); }, 10000);
//test payment methods option //test payment methods option
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model); .IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello"; vmpos.Title = "hello";
@@ -858,7 +853,6 @@ g:
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup); Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup); Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Static, null, null, null, null, null, "g").Result); .ViewPointOfSale(appId, PosViewType.Static, null, null, null, null, null, "g").Result);
invoices = user.BitPay.GetInvoices(); invoices = user.BitPay.GetInvoices();

View File

@@ -41,7 +41,7 @@ namespace BTCPayServer.Tests
var tester = s.Server; var tester = s.Server;
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
await user.MakeAdmin(false); await user.MakeAdmin(false);
s.GoToLogin(); s.GoToLogin();
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);

View File

@@ -29,21 +29,21 @@ namespace BTCPayServer.Tests
{ {
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
var user2 = tester.NewAccount(); var user2 = tester.NewAccount();
user2.GrantAccess(); await user2.GrantAccessAsync();
var apps = user.GetController<AppsController>(); var apps = user.GetController<AppsController>();
var apps2 = user2.GetController<AppsController>(); var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
Assert.NotNull(vm.SelectedAppType); Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.AppName); Assert.Null(vm.AppName);
vm.AppName = "test"; vm.AppName = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString(); vm.SelectedAppType = AppType.Crowdfund.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result); var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName); Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model); var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var appList2 = var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model); Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
Assert.Single(appList.Apps); Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps); Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName); Assert.Equal("test", appList.Apps[0].AppName);
@@ -54,13 +54,11 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result); Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result); redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName); Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model); appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps); Assert.Empty(appList.Apps);
} }
} }
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public async Task CanContributeOnlyWhenAllowed() public async Task CanContributeOnlyWhenAllowed()
@@ -69,14 +67,14 @@ namespace BTCPayServer.Tests
{ {
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>(); var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
vm.AppName = "test"; vm.AppName = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString(); vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result); Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model) var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model)
.Apps[0].Id; .Apps[0].Id;
//Scenario 1: Not Enabled - Not Allowed //Scenario 1: Not Enabled - Not Allowed
@@ -91,7 +89,6 @@ namespace BTCPayServer.Tests
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>(); var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
var publicApps = user.GetController<AppsPublicController>(); var publicApps = user.GetController<AppsPublicController>();
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund() Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{ {
Amount = new decimal(0.01) Amount = new decimal(0.01)
@@ -119,7 +116,6 @@ namespace BTCPayServer.Tests
}, default)); }, default));
//Scenario 4: Enabled But End Date < Now - Not Allowed //Scenario 4: Enabled But End Date < Now - Not Allowed
crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2); crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1); crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
crowdfundViewModel.Enabled = true; crowdfundViewModel.Enabled = true;
@@ -130,7 +126,6 @@ namespace BTCPayServer.Tests
Amount = new decimal(0.01) Amount = new decimal(0.01)
}, default)); }, default));
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed //Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2); crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate = DateTime.Today.AddDays(2); crowdfundViewModel.EndDate = DateTime.Today.AddDays(2);
@@ -149,7 +144,6 @@ namespace BTCPayServer.Tests
{ {
Amount = new decimal(0.05) Amount = new decimal(0.05)
}, default)); }, default));
} }
} }
@@ -165,11 +159,11 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Never); await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var apps = user.GetController<AppsController>(); var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
vm.AppName = "test"; vm.AppName = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString(); vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result); Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model) var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model)
.Apps[0].Id; .Apps[0].Id;
TestLogs.LogInformation("We create an invoice with a hardcap"); TestLogs.LogInformation("We create an invoice with a hardcap");
@@ -189,7 +183,6 @@ namespace BTCPayServer.Tests
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model); .IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount); Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount);
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate); Assert.Equal(crowdfundViewModel.EndDate, model.EndDate);
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate); Assert.Equal(crowdfundViewModel.StartDate, model.StartDate);
@@ -198,10 +191,9 @@ namespace BTCPayServer.Tests
Assert.Equal(0m, model.Info.CurrentPendingAmount); Assert.Equal(0m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage); Assert.Equal(0m, model.Info.ProgressPercentage);
TestLogs.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap"); TestLogs.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
TestLogs.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution"); TestLogs.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
var invoice = user.BitPay.CreateInvoice(new Invoice() var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
{ {
Buyer = new Buyer() { email = "test@fwf.com" }, Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m, Price = 1m,
@@ -212,9 +204,8 @@ namespace BTCPayServer.Tests
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
model = Assert.IsType<ViewCrowdfundViewModel>(Assert model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model); .IsType<ViewResult>(publicApps.ViewCrowdfund(appId, string.Empty).Result).Model);
Assert.Equal(0m, model.Info.CurrentAmount); Assert.Equal(0m, model.Info.CurrentAmount);
Assert.Equal(1m, model.Info.CurrentPendingAmount); Assert.Equal(1m, model.Info.CurrentPendingAmount);
@@ -242,9 +233,9 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel, "save").Result); Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel, "save").Result);
TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged"); TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = user.BitPay.CreateInvoice(new Invoice() invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
{ {
Buyer = new Buyer() { email = "test@fwf.com" }, Buyer = new Buyer { email = "test@fwf.com" },
Price = 1m, Price = 1m,
Currency = "BTC", Currency = "BTC",
PosData = "posData", PosData = "posData",
@@ -259,9 +250,9 @@ namespace BTCPayServer.Tests
crowdfundViewModel.EnforceTargetAmount = false; crowdfundViewModel.EnforceTargetAmount = false;
crowdfundViewModel.UseAllStoreInvoices = true; crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel, "save").Result); Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel, "save").Result);
invoice = user.BitPay.CreateInvoice(new Invoice() invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
{ {
Buyer = new Buyer() { email = "test@fwf.com" }, Buyer = new Buyer { email = "test@fwf.com" },
Price = 1m, Price = 1m,
Currency = "BTC", Currency = "BTC",
PosData = "posData", PosData = "posData",
@@ -271,20 +262,15 @@ namespace BTCPayServer.Tests
}, Facade.Merchant); }, Facade.Merchant);
Assert.Equal(0m, model.Info.CurrentPendingAmount); Assert.Equal(0m, model.Info.CurrentPendingAmount);
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m)); await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, Money.Coins(0.5m));
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.2m)); await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, Money.Coins(0.2m));
TestUtils.Eventually(() => TestUtils.Eventually(() =>
{ {
model = Assert.IsType<ViewCrowdfundViewModel>(Assert model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model); .IsType<ViewResult>(publicApps.ViewCrowdfund(appId, string.Empty).Result).Model);
Assert.Equal(0.7m, model.Info.CurrentPendingAmount); Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
}); });
} }
} }
} }
} }

View File

@@ -28,7 +28,6 @@ namespace BTCPayServer.Tests
public static void AssertNoError(this IWebDriver driver) public static void AssertNoError(this IWebDriver driver)
{ {
Assert.NotEmpty(driver.FindElements(By.ClassName("navbar-brand")));
if (!driver.PageSource.Contains("alert-danger")) return; if (!driver.PageSource.Contains("alert-danger")) return;
foreach (var dangerAlert in driver.FindElements(By.ClassName("alert-danger"))) foreach (var dangerAlert in driver.FindElements(By.ClassName("alert-danger")))
Assert.False(dangerAlert.Displayed, $"No alert should be displayed, but found this on {driver.Url}: {dangerAlert.Text}"); Assert.False(dangerAlert.Displayed, $"No alert should be displayed, but found this on {driver.Url}: {dangerAlert.Text}");

View File

@@ -25,14 +25,14 @@ namespace BTCPayServer.Tests
{ {
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>(); var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
vm.AppName = "test"; vm.AppName = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString(); vm.SelectedAppType = AppType.PointOfSale.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result); Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model) var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model)
.Apps[0].Id; .Apps[0].Id;
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model); .IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);

View File

@@ -1,16 +1,11 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using NBitcoin;
using NBitpayClient; using NBitpayClient;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@@ -32,16 +27,17 @@ namespace BTCPayServer.Tests
{ {
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount(); var user2 = tester.NewAccount();
user2.GrantAccess();
await user2.GrantAccessAsync();
var paymentRequestController = user.GetController<PaymentRequestController>(); var paymentRequestController = user.GetController<PaymentRequestController>();
var guestpaymentRequestController = user2.GetController<PaymentRequestController>(); var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
var request = new UpdatePaymentRequestViewModel() var request = new UpdatePaymentRequestViewModel
{ {
Title = "original juice", Title = "original juice",
Currency = "BTC", Currency = "BTC",
@@ -49,14 +45,13 @@ namespace BTCPayServer.Tests
StoreId = user.StoreId, StoreId = user.StoreId,
Description = "description" Description = "description"
}; };
var id = (Assert var id = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(null, request)) .IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(null, request))
.RouteValues.Values.First().ToString()); .RouteValues.Values.Last().ToString();
// Permission guard for guests editing
//permission guard for guests editing
Assert Assert
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(id)); .IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id));
request.Title = "update"; request.Title = "update";
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request)); Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request));
@@ -70,8 +65,7 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewPaymentRequestViewModel>(Assert Assert.IsType<ViewPaymentRequestViewModel>(Assert
.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model); .IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model);
//Archive // Archive
Assert Assert
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id)); .IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
Assert.True(Assert Assert.True(Assert
@@ -80,8 +74,9 @@ namespace BTCPayServer.Tests
Assert.Empty(Assert Assert.Empty(Assert
.IsType<ListPaymentRequestsViewModel>(Assert .IsType<ListPaymentRequestsViewModel>(Assert
.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests()).Model).Items); .IsType<ViewResult>(await paymentRequestController.GetPaymentRequests(user.StoreId)).Model).Items);
//unarchive
// Unarchive
Assert Assert
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id)); .IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
@@ -91,7 +86,7 @@ namespace BTCPayServer.Tests
Assert.Single(Assert Assert.Single(Assert
.IsType<ListPaymentRequestsViewModel>(Assert .IsType<ListPaymentRequestsViewModel>(Assert
.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests()).Model).Items); .IsType<ViewResult>(await paymentRequestController.GetPaymentRequests(user.StoreId)).Model).Items);
} }
} }
@@ -103,7 +98,7 @@ namespace BTCPayServer.Tests
{ {
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>(); var paymentRequestController = user.GetController<PaymentRequestController>();
@@ -122,7 +117,7 @@ namespace BTCPayServer.Tests
}; };
var response = Assert var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result) .IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First(); .RouteValues.Last();
var invoiceId = Assert var invoiceId = Assert
.IsType<OkObjectResult>( .IsType<OkObjectResult>(
@@ -153,7 +148,7 @@ namespace BTCPayServer.Tests
response = Assert response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result) .IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First(); .RouteValues.Last();
Assert Assert
.IsType<BadRequestObjectResult>( .IsType<BadRequestObjectResult>(
@@ -187,7 +182,7 @@ namespace BTCPayServer.Tests
}; };
var response = Assert var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result) .IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First(); .RouteValues.Last();
var invoiceId = response.Value.ToString(); var invoiceId = response.Value.ToString();
await paymentRequestController.PayPaymentRequest(invoiceId, false); await paymentRequestController.PayPaymentRequest(invoiceId, false);
Assert.IsType<BadRequestObjectResult>(await Assert.IsType<BadRequestObjectResult>(await
@@ -197,7 +192,7 @@ namespace BTCPayServer.Tests
response = Assert response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result) .IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First(); .RouteValues.Last();
var paymentRequestId = response.Value.ToString(); var paymentRequestId = response.Value.ToString();
@@ -231,7 +226,7 @@ namespace BTCPayServer.Tests
.Value .Value
.ToString(); .ToString();
invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); await user.BitPay.GetInvoiceAsync(invoiceId, Facade.Merchant);
//a hack to generate invoices for the payment request is to manually create an invoice with an order id that matches: //a hack to generate invoices for the payment request is to manually create an invoice with an order id that matches:
user.BitPay.CreateInvoice(new Invoice(1, "USD") user.BitPay.CreateInvoice(new Invoice(1, "USD")

View File

@@ -2,14 +2,10 @@ using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning; using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage; using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server; using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores; using BTCPayServer.Views.Stores;
@@ -146,14 +142,14 @@ namespace BTCPayServer.Tests
public (string storeName, string storeId) CreateNewStore(bool keepId = true) public (string storeName, string storeId) CreateNewStore(bool keepId = true)
{ {
Driver.WaitForElement(By.Id("Stores")).Click(); Driver.WaitForElement(By.Id("StoreSelectorToggle")).Click();
Driver.WaitForElement(By.Id("CreateStore")).Click(); Driver.WaitForElement(By.Id("StoreSelectorMenuItem-Create")).Click();
var name = "Store" + RandomUtils.GetUInt64(); var name = "Store" + RandomUtils.GetUInt64();
Driver.WaitForElement(By.Id("Name")).SendKeys(name); Driver.WaitForElement(By.Id("Name")).SendKeys(name);
Driver.WaitForElement(By.Id("Create")).Click(); Driver.WaitForElement(By.Id("Create")).Click();
Driver.FindElement(By.Id($"Nav-{StoreNavPages.GeneralSettings.ToString()}")).Click(); Driver.FindElement(By.Id($"SectionNav-{StoreNavPages.GeneralSettings.ToString()}")).Click();
var storeId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value"); var storeId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value");
Driver.FindElement(By.Id($"Nav-{StoreNavPages.PaymentMethods.ToString()}")).Click(); Driver.FindElement(By.Id($"SectionNav-{StoreNavPages.PaymentMethods.ToString()}")).Click();
if (keepId) if (keepId)
StoreId = storeId; StoreId = storeId;
return (name, storeId); return (name, storeId);
@@ -279,9 +275,9 @@ namespace BTCPayServer.Tests
} }
public Logging.ILog TestLogs => Server.TestLogs; public Logging.ILog TestLogs => Server.TestLogs;
public void ClickOnAllSideMenus() public void ClickOnAllSectionLinks()
{ {
var links = Driver.FindElements(By.CssSelector(".nav .nav-link")).Select(c => c.GetAttribute("href")).ToList(); var links = Driver.FindElements(By.CssSelector("#SectionNav .nav-link")).Select(c => c.GetAttribute("href")).ToList();
Driver.AssertNoError(); Driver.AssertNoError();
foreach (var l in links) foreach (var l in links)
{ {
@@ -315,6 +311,11 @@ namespace BTCPayServer.Tests
Assert.Contains("404 - Page not found</h1>", Driver.PageSource); Assert.Contains("404 - Page not found</h1>", Driver.PageSource);
} }
internal void AssertAccessDenied()
{
Assert.Contains("Access denied</h", Driver.PageSource);
}
public void GoToHome() public void GoToHome()
{ {
Driver.Navigate().GoToUrl(ServerUri); Driver.Navigate().GoToUrl(ServerUri);
@@ -331,24 +332,29 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Password")).SendKeys(password); Driver.FindElement(By.Id("Password")).SendKeys(password);
Driver.FindElement(By.Id("LoginButton")).Click(); Driver.FindElement(By.Id("LoginButton")).Click();
} }
public void GoToApps() public void GoToApps()
{ {
Driver.FindElement(By.Id("Apps")).Click(); Driver.FindElement(By.Id("StoreNav-Apps")).Click();
}
public void GoToStores()
{
Driver.FindElement(By.Id("Stores")).Click();
} }
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.PaymentMethods) public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.PaymentMethods)
{ {
GoToHome(); GoToHome();
Driver.WaitForAndClick(By.Id("Stores")); Driver.WaitForAndClick(By.Id("StoreSelectorToggle"));
Driver.FindElement(By.Id($"update-store-{storeId}")).Click(); Driver.WaitForAndClick(By.Id($"StoreSelectorMenuItem-{storeId}"));
if (storeNavPage != StoreNavPages.PaymentMethods) if (storeNavPage != StoreNavPages.PaymentMethods)
{ {
Driver.FindElement(By.Id($"Nav-{storeNavPage.ToString()}")).Click(); // FIXME: Review and optimize this once we decided on where which items belong
try
{
Driver.FindElement(By.Id($"StoreNav-{storeNavPage.ToString()}")).Click();
}
catch (NoSuchElementException)
{
Driver.FindElement(By.Id($"SectionNav-{storeNavPage.ToString()}")).Click();
}
} }
} }
@@ -366,22 +372,23 @@ namespace BTCPayServer.Tests
public void GoToInvoiceCheckout(string invoiceId) public void GoToInvoiceCheckout(string invoiceId)
{ {
Driver.FindElement(By.Id("Invoices")).Click(); Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click(); Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click();
CheckForJSErrors(); CheckForJSErrors();
} }
public void GoToInvoices() public void GoToInvoices()
{ {
Driver.FindElement(By.Id("Invoices")).Click(); GoToHome();
Driver.FindElement(By.Id("Nav-Invoices")).Click();
} }
public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index) public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index)
{ {
Driver.FindElement(By.Id("MySettings")).Click(); Driver.FindElement(By.Id("Nav-Account")).Click();
if (navPages != ManageNavPages.Index) if (navPages != ManageNavPages.Index)
{ {
Driver.FindElement(By.Id(navPages.ToString())).Click(); Driver.FindElement(By.Id($"SectionNav-{navPages.ToString()}")).Click();
} }
} }
@@ -477,7 +484,7 @@ namespace BTCPayServer.Tests
Driver.Navigate().GoToUrl(new Uri(ServerUri, $"wallets/{walletId}")); Driver.Navigate().GoToUrl(new Uri(ServerUri, $"wallets/{walletId}"));
if (navPages != WalletsNavPages.Transactions) if (navPages != WalletsNavPages.Transactions)
{ {
Driver.FindElement(By.Id($"Wallet{navPages}")).Click(); Driver.FindElement(By.Id($"SectionNav-{navPages}")).Click();
} }
} }
@@ -488,10 +495,10 @@ namespace BTCPayServer.Tests
public void GoToServer(ServerNavPages navPages = ServerNavPages.Index) public void GoToServer(ServerNavPages navPages = ServerNavPages.Index)
{ {
Driver.FindElement(By.Id("ServerSettings")).Click(); Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
if (navPages != ServerNavPages.Index) if (navPages != ServerNavPages.Index)
{ {
Driver.FindElement(By.Id($"Server-{navPages}")).Click(); Driver.FindElement(By.Id($"SectionNav-{navPages}")).Click();
} }
} }

View File

@@ -3,8 +3,6 @@ using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
@@ -13,15 +11,11 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Lightning.Charge;
using BTCPayServer.Lightning.CLightning; using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning.LND;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server; using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores; using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets; using BTCPayServer.Views.Wallets;
@@ -38,8 +32,6 @@ using OpenQA.Selenium.Support.UI;
using Renci.SshNet.Security.Cryptography; using Renci.SshNet.Security.Cryptography;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using Xunit.Sdk;
using CreateInvoiceRequest = BTCPayServer.Lightning.Charge.CreateInvoiceRequest;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@@ -60,9 +52,9 @@ namespace BTCPayServer.Tests
{ {
await s.StartAsync(); await s.StartAsync();
s.RegisterNewUser(true); s.RegisterNewUser(true);
s.Driver.FindElement(By.Id("ServerSettings")).Click(); s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
s.Driver.AssertNoError(); s.Driver.AssertNoError();
s.ClickOnAllSideMenus(); s.ClickOnAllSectionLinks();
s.Driver.FindElement(By.LinkText("Services")).Click(); s.Driver.FindElement(By.LinkText("Services")).Click();
TestLogs.LogInformation("Let's check if we can access the logs"); TestLogs.LogInformation("Let's check if we can access the logs");
@@ -82,7 +74,7 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning(); s.Server.ActivateLightning();
await s.StartAsync(); await s.StartAsync();
s.RegisterNewUser(true); s.RegisterNewUser(true);
s.Driver.FindElement(By.Id("ServerSettings")).Click(); s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
s.Driver.AssertNoError(); s.Driver.AssertNoError();
s.Driver.FindElement(By.LinkText("Services")).Click(); s.Driver.FindElement(By.LinkText("Services")).Click();
@@ -174,8 +166,8 @@ namespace BTCPayServer.Tests
Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url); Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url);
//Change Password & Log Out //Change Password & Log Out
s.Driver.FindElement(By.Id("MySettings")).Click(); s.Driver.FindElement(By.Id("Nav-Account")).Click();
s.Driver.FindElement(By.Id("ChangePassword")).Click(); s.Driver.FindElement(By.Id("SectionNav-ChangePassword")).Click();
s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456"); s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456");
s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???"); s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???");
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???"); s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???");
@@ -189,8 +181,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("LoginButton")).Click(); s.Driver.FindElement(By.Id("LoginButton")).Click();
Assert.True(s.Driver.PageSource.Contains("Stores"), "Can't Access Stores"); Assert.True(s.Driver.PageSource.Contains("Stores"), "Can't Access Stores");
s.Driver.FindElement(By.Id("MySettings")).Click(); s.Driver.FindElement(By.Id("Nav-Account")).Click();
s.ClickOnAllSideMenus(); s.ClickOnAllSectionLinks();
//let's test invite link //let's test invite link
s.Logout(); s.Logout();
@@ -300,7 +292,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit(); s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit();
s.FindAlertMessage(); s.FindAlertMessage();
} }
CanSetupEmailCore(s); CanSetupEmailCore(s);
s.CreateNewStore(); s.CreateNewStore();
s.GoToUrl($"stores/{s.StoreId}/emails"); s.GoToUrl($"stores/{s.StoreId}/emails");
@@ -376,9 +367,6 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not present"); Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not present");
Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present"); Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present");
s.GoToStores();
Assert.True(s.Driver.PageSource.Contains($"warninghint_{storeId}"), "Warning hint on list not present");
s.GoToStore(storeId); s.GoToStore(storeId);
Assert.Contains(storeName, s.Driver.PageSource); Assert.Contains(storeName, s.Driver.PageSource);
Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point"); Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point");
@@ -402,7 +390,7 @@ namespace BTCPayServer.Tests
"Lightning hint should be dismissed at this point"); "Lightning hint should be dismissed at this point");
var storeUrl = s.Driver.Url; var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus(); s.ClickOnAllSectionLinks();
s.GoToInvoices(); s.GoToInvoices();
var invoiceId = s.CreateInvoice(storeName); var invoiceId = s.CreateInvoice(storeName);
s.FindAlertMessage(); s.FindAlertMessage();
@@ -425,19 +413,20 @@ namespace BTCPayServer.Tests
s.GoToInvoices(); s.GoToInvoices();
Assert.Contains(invoiceId, s.Driver.PageSource); Assert.Contains(invoiceId, s.Driver.PageSource);
// When logout we should not be able to access store and invoice details // When logout out we should not be able to access store and invoice details
s.Driver.FindElement(By.Id("Logout")).Click(); s.Driver.FindElement(By.Id("Logout")).Click();
s.Driver.Navigate().GoToUrl(storeUrl); s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url); Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl); s.Driver.Navigate().GoToUrl(invoiceUrl);
Assert.Contains("ReturnUrl", s.Driver.Url); Assert.Contains("ReturnUrl", s.Driver.Url);
s.GoToRegister(); s.GoToRegister();
// When logged we should not be able to access store and invoice details
// When logged in as different user we should not be able to access store and invoice details
var bob = s.RegisterNewUser(); var bob = s.RegisterNewUser();
s.Driver.Navigate().GoToUrl(storeUrl); s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url); Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl); s.Driver.Navigate().GoToUrl(invoiceUrl);
s.AssertNotFound(); s.AssertAccessDenied();
s.GoToHome(); s.GoToHome();
s.Logout(); s.Logout();
@@ -458,11 +447,10 @@ namespace BTCPayServer.Tests
// Alice should be able to delete the store // Alice should be able to delete the store
s.Logout(); s.Logout();
s.LogIn(alice); s.LogIn(alice);
s.Driver.FindElement(By.Id("Stores")).Click(); s.GoToStore(storeId, StoreNavPages.GeneralSettings);
s.Driver.FindElement(By.LinkText("Delete")).Click(); s.Driver.FindElement(By.Id("DeleteStore")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.FindElement(By.Id("Stores")).Click();
s.Driver.Navigate().GoToUrl(storeUrl); s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url); Assert.Contains("ReturnUrl", s.Driver.Url);
} }
@@ -481,7 +469,7 @@ namespace BTCPayServer.Tests
s.CreateNewStore(); s.CreateNewStore();
s.AddDerivationScheme(); s.AddDerivationScheme();
s.Driver.FindElement(By.Id("Nav-Tokens")).Click(); s.Driver.FindElement(By.Id("SectionNav-Tokens")).Click();
s.Driver.FindElement(By.Id("CreateNewToken")).Click(); s.Driver.FindElement(By.Id("CreateNewToken")).Click();
s.Driver.FindElement(By.Id("RequestPairing")).Click(); s.Driver.FindElement(By.Id("RequestPairing")).Click();
var pairingCode = AssertUrlHasPairingCode(s); var pairingCode = AssertUrlHasPairingCode(s);
@@ -520,13 +508,11 @@ namespace BTCPayServer.Tests
{ {
await s.StartAsync(); await s.StartAsync();
s.RegisterNewUser(); s.RegisterNewUser();
var (storeName, _) = s.CreateNewStore(); (string storeName, _) = s.CreateNewStore();
s.Driver.FindElement(By.Id("Apps")).Click(); s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid()); s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale"); s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
s.Driver.FindElement(By.Id("Create")).Click(); s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click(); s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money"); s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
@@ -560,14 +546,12 @@ namespace BTCPayServer.Tests
{ {
await s.StartAsync(); await s.StartAsync();
s.RegisterNewUser(); s.RegisterNewUser();
var (storeName, _) = s.CreateNewStore(); (string storeName, _) = s.CreateNewStore();
s.AddDerivationScheme(); s.AddDerivationScheme();
s.Driver.FindElement(By.Id("Apps")).Click(); s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid()); s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund"); s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
s.Driver.FindElement(By.Id("Create")).Click(); s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter"); s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC"); s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
@@ -590,13 +574,13 @@ namespace BTCPayServer.Tests
s.CreateNewStore(); s.CreateNewStore();
s.AddDerivationScheme(); s.AddDerivationScheme();
s.Driver.FindElement(By.Id("PaymentRequests")).Click(); s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click(); s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123"); s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700"); s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC"); s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("SaveButton")).Click(); s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.Name("ViewAppButton")).Click(); s.Driver.FindElement(By.Id("ViewAppButton")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text); Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
Assert.Equal("Pay Invoice", Assert.Equal("Pay Invoice",
@@ -796,8 +780,9 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("Let's see if we can delete store with some webhooks inside"); TestLogs.LogInformation("Let's see if we can delete store with some webhooks inside");
s.GoToStore(storeId, StoreNavPages.GeneralSettings); s.GoToStore(storeId, StoreNavPages.GeneralSettings);
s.Driver.FindElement(By.Id("delete-store")).Click(); s.Driver.FindElement(By.Id("DeleteStore")).Click();
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click(); s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage(); s.FindAlertMessage();
} }
} }
@@ -830,23 +815,22 @@ namespace BTCPayServer.Tests
{ {
await s.StartAsync(); await s.StartAsync();
s.RegisterNewUser(true); s.RegisterNewUser(true);
var (storeName, storeId) = s.CreateNewStore(); (string storeName, string storeId) = s.CreateNewStore();
var cryptoCode = "BTC"; const string cryptoCode = "BTC";
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0',
// then try to use the seed to sign the transaction // then try to use the seed to sign the transaction
s.GenerateWallet(cryptoCode, "", true); s.GenerateWallet(cryptoCode, "", true);
//let's test quickly the receive wallet page //let's test quickly the receive wallet page
s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
//you cannot use the Sign with NBX option without saving private keys when generating the wallet. //you cannot use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
s.Driver.FindElement(By.Id("WalletReceive")).Click(); s.Driver.FindElement(By.Id("SectionNav-Receive")).Click();
//generate a receiving address //generate a receiving address
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
@@ -874,9 +858,8 @@ namespace BTCPayServer.Tests
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one //change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GoToStore(storeId); s.GoToStore(storeId);
s.GenerateWallet(cryptoCode, "", true); s.GenerateWallet(cryptoCode, "", true);
s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("SectionNav-Receive")).Click();
s.Driver.FindElement(By.Id("WalletReceive")).Click();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
@@ -905,9 +888,9 @@ namespace BTCPayServer.Tests
Money.Coins(3.0m)); Money.Coins(3.0m));
await s.Server.ExplorerNode.GenerateAsync(1); await s.Server.ExplorerNode.GenerateAsync(1);
s.Driver.FindElement(By.Id("Wallets")).Click(); s.GoToStore(storeId);
s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.ClickOnAllSideMenus(); s.ClickOnAllSectionLinks();
// Make sure wallet info is correct // Make sure wallet info is correct
s.GoToWalletSettings(storeId, cryptoCode); s.GoToWalletSettings(storeId, cryptoCode);
@@ -916,21 +899,20 @@ namespace BTCPayServer.Tests
Assert.Contains("m/84'/1'/0'", Assert.Contains("m/84'/1'/0'",
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value")); s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
// Make sure we can rescan, because we are admin! // Make sure we can rescan, because we are admin!
s.Driver.FindElement(By.Id("WalletRescan")).Click(); s.Driver.FindElement(By.Id("SectionNav-Rescan")).Click();
Assert.Contains("The batch size make sure", s.Driver.PageSource); Assert.Contains("The batch size make sure", s.Driver.PageSource);
// Check the tx sent earlier arrived // Check the tx sent earlier arrived
s.Driver.FindElement(By.Id("WalletTransactions")).Click(); s.Driver.FindElement(By.Id("SectionNav-Transactions")).Click();
var walletTransactionLink = s.Driver.Url; var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource); Assert.Contains(tx.ToString(), s.Driver.PageSource);
// Send to bob // Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click(); s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1); SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SignTransaction")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
@@ -941,9 +923,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url); Assert.Equal(walletTransactionLink, s.Driver.Url);
s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m); SetTransactionOutput(s, 0, jack, 0.01m);
@@ -959,9 +940,8 @@ namespace BTCPayServer.Tests
//let's make bip21 more interesting //let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!"; bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest); var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id("bip21parse")).Click(); s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21); s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept(); s.Driver.SwitchTo().Alert().Accept();
@@ -1126,7 +1106,7 @@ namespace BTCPayServer.Tests
}); });
s.GoToHome(); s.GoToHome();
//offline/external payout test //offline/external payout test
s.Driver.FindElement(By.Id("NotificationsDropdownToggle")).Click(); s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click(); s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click();
var newStore = s.CreateNewStore(); var newStore = s.CreateNewStore();
@@ -1241,7 +1221,6 @@ namespace BTCPayServer.Tests
s.GoToStore(newStore.storeId, StoreNavPages.Payouts); s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click(); s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click(); s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
if (!s.Driver.PageSource.Contains(bolt)) if (!s.Driver.PageSource.Contains(bolt))
{ {
@@ -1272,22 +1251,21 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true); s.RegisterNewUser(true);
var cryptoCode = "BTC"; var cryptoCode = "BTC";
(_, string storeId) = s.CreateNewStore(); (_, string storeId) = s.CreateNewStore();
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode).NBitcoinNetwork;
s.GoToStore(storeId); s.GoToStore(storeId);
s.AddLightningNode(cryptoCode, LightningConnectionType.CLightning, false); s.AddLightningNode(cryptoCode, LightningConnectionType.CLightning, false);
s.GoToLightningSettings(storeId, cryptoCode); s.GoToLightningSettings(storeId, cryptoCode);
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true); s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.GoToApps(); s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Id("SelectedAppType")).Click(); s.Driver.FindElement(By.Id("SelectedAppType")).Click();
s.Driver.FindElement(By.CssSelector("option[value='PointOfSale']")).Click(); s.Driver.FindElement(By.CssSelector("option[value='PointOfSale']")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString()); s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click(); s.Driver.FindElement(By.Id("Create")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); Thread.Sleep(5000);
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("DefaultView")).Click(); s.Driver.FindElement(By.Id("DefaultView")).Click();
s.Driver.FindElement(By.CssSelector("option[value='3']")).Click(); s.Driver.FindElement(By.CssSelector("option[value='3']")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click(); s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click(); s.Driver.FindElement(By.Id("ViewApp")).Click();
var btns = s.Driver.FindElements(By.ClassName("lnurl")); var btns = s.Driver.FindElements(By.ClassName("lnurl"));
@@ -1299,7 +1277,6 @@ namespace BTCPayServer.Tests
Assert.EndsWith(choice, parsed.ToString()); Assert.EndsWith(choice, parsed.ToString());
Assert.IsType<LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient())); Assert.IsType<LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
} }
} }
[Fact] [Fact]
@@ -1611,7 +1588,7 @@ retry:
private static void CanSetupEmailCore(SeleniumTester s) private static void CanSetupEmailCore(SeleniumTester s)
{ {
s.Driver.FindElement(By.Id("QuickFillDropdownToggle")).Click(); s.Driver.FindElement(By.Id("QuickFillDropdownToggle")).Click();
s.Driver.FindElement(By.ClassName("dropdown-item")).Click(); s.Driver.FindElement(By.CssSelector("#quick-fill .dropdown-menu .dropdown-item:first-child")).Click();
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com"); s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();

View File

@@ -959,7 +959,7 @@ namespace BTCPayServer.Tests
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter) private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter)
{ {
var result = var result =
(Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>() (InvoicesModel)((ViewResult)acc.GetController<InvoiceController>()
.ListInvoices(new InvoicesModel { SearchTerm = filter }).Result).Model; .ListInvoices(new InvoicesModel { SearchTerm = filter }).Result).Model;
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId)); Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
} }
@@ -2017,7 +2017,6 @@ namespace BTCPayServer.Tests
} }
} }
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteApps() public async Task CanCreateAndDeleteApps()
@@ -2026,21 +2025,21 @@ namespace BTCPayServer.Tests
{ {
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); await user.GrantAccessAsync();
var user2 = tester.NewAccount(); var user2 = tester.NewAccount();
user2.GrantAccess(); await user2.GrantAccessAsync();
var apps = user.GetController<AppsController>(); var apps = user.GetController<AppsController>();
var apps2 = user2.GetController<AppsController>(); var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
Assert.NotNull(vm.SelectedAppType); Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.AppName); Assert.Null(vm.AppName);
vm.AppName = "test"; vm.AppName = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString(); vm.SelectedAppType = AppType.PointOfSale.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result); var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName); Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model); var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var appList2 = var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model); Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
Assert.Single(appList.Apps); Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps); Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName); Assert.Equal("test", appList.Apps[0].AppName);
@@ -2051,7 +2050,7 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result); Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result); redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName); Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model); appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps); Assert.Empty(appList.Apps);
} }
} }

View File

@@ -0,0 +1,285 @@
@using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.Invoice
@using BTCPayServer.Views.Manage
@using BTCPayServer.Views.PaymentRequest
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@inject SignInManager<ApplicationUser> SignInManager
@inject ISettingsRepository SettingsRepository
@model BTCPayServer.Components.MainNav.MainNavViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
@{
var theme = await SettingsRepository.GetTheme();
}
<nav id="mainNav" class="d-flex flex-column justify-content-between">
<div class="accordion px-3 px-lg-4">
@if (SignInManager.IsSignedIn(User))
{
@if (Model.Store == null)
{
<div class="accordion-item">
<header class="accordion-header" id="Nav-Payments-Header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#Nav-Payments" aria-expanded="true" aria-controls="Nav-Payments">
Payments
<vc:icon symbol="caret-down"/>
</button>
</header>
<div id="Nav-Payments" class="accordion-collapse collapse show" aria-labelledby="Nav-Payments-Header">
<div class="accordion-body">
<ul class="navbar-nav">
<li class="nav-item">
<a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(InvoiceNavPages))" id="Nav-Invoices">
<vc:icon symbol="invoice"/>
<span>Invoices</span>
</a>
</li>
@* FIXME: The wallets item is in here only for the tests *@
<li class="nav-item">
<a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(WalletsNavPages))" id="Nav-Wallets">
<vc:icon symbol="wallet-onchain"/>
<span>Wallets</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<header class="accordion-header" id="Nav-Plugins-Header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#Nav-Plugins" aria-expanded="true" aria-controls="Nav-Plugins">
Plugins
<vc:icon symbol="caret-down"/>
</button>
</header>
<div id="Nav-Plugins" class="accordion-collapse collapse show" aria-labelledby="Nav-Plugins-Header">
<div class="accordion-body">
<ul class="navbar-nav">
<vc:ui-extension-point location="header-nav" model="@Model"/>
@* TODO: Limit this to admins *@
<li class="nav-item">
<a asp-area="" asp-controller="Server" asp-action="ListPlugins" class="nav-link js-scroll-trigger @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-AddPlugin">
<vc:icon symbol="new"/>
<span>Add Plugin</span>
</a>
</li>
</ul>
</div>
</div>
</div>
}
else
{
<div class="accordion-item">
<div class="accordion-body">
<ul class="navbar-nav">
@foreach (var scheme in Model.DerivationSchemes.OrderBy(scheme => scheme.Collapsed))
{
var isSetUp = !string.IsNullOrWhiteSpace(scheme.Value);
<li class="nav-item">
@if (isSetUp)
{
<a asp-area="" asp-controller="Stores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Model.Store.Id" class="nav-link" id="@($"StoreNav-Modify{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "disabled")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} " : "")Wallet</span>
</a>
}
else
{
<a asp-area="" asp-controller="Stores" asp-action="SetupWallet" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Model.Store.Id" class="nav-link" id="@($"StoreNav-Modify{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "disabled")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} " : "")Wallet</span>
</a>
}
</li>
}
@foreach (var scheme in Model.LightningNodes)
{
var isSetUp = !string.IsNullOrWhiteSpace(scheme.Address);
<li class="nav-item">
@if (isSetUp)
{
<a asp-area="" asp-controller="Stores" asp-action="LightningSettings" asp-route-cryptoCode="@scheme.CryptoCode" asp-route-storeId="@Model.Store.Id" class="nav-link" id="@($"StoreNav-Lightning{scheme.CryptoCode}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "disabled")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.CryptoCode} " : "")Lightning</span>
</a>
}
else
{
<a asp-area="" asp-controller="Stores" asp-action="SetupLightningNode" asp-route-cryptoCode="@scheme.CryptoCode" asp-route-storeId="@Model.Store.Id" class="nav-link" id="@($"StoreNav-Lightning{scheme.CryptoCode}")">
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.CryptoCode} " : "")Lightning</span>
</a>
}
</li>
}
</ul>
</div>
</div>
<div class="accordion-item">
<header class="accordion-header" id="Nav-Payments-Header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#Nav-Payments" aria-expanded="true" aria-controls="Nav-Payments">
Payments
<vc:icon symbol="caret-down"/>
</button>
</header>
<div id="Nav-Payments" class="accordion-collapse collapse show" aria-labelledby="Nav-Payments-Header">
<div class="accordion-body">
<ul class="navbar-nav">
@foreach (var scheme in Model.DerivationSchemes.OrderBy(scheme => scheme.Collapsed))
{
var isSetUp = !string.IsNullOrWhiteSpace(scheme.Value);
if (isSetUp && scheme.WalletSupported)
{
<li class="nav-item">
<a asp-area="" asp-controller="Wallets" asp-action="WalletTransactions" asp-route-walletId="@scheme.WalletId" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.Crypto)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
<vc:icon symbol="wallet-onchain"/>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} " : "")Wallet</span>
</a>
</li>
}
}
<li class="nav-item">
<a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(InvoiceNavPages))" id="StoreNav-Invoices">
<vc:icon symbol="invoice"/>
<span>Invoices</span>
</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="PaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
<vc:icon symbol="payment-1"/>
<span>Requests</span>
</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="StorePullPayments" asp-action="PullPayments" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" id="StoreNav-@(nameof(StoreNavPages.PullPayments))">
<vc:icon symbol="payment-2"/>
<span>Pull Payments</span>
</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="StorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" id="StoreNav-@(nameof(StoreNavPages.Payouts))">
<vc:icon symbol="payment-2"/>
<span>Payouts</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<header class="accordion-header" id="Nav-Apps-Header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#Nav-Apps" aria-expanded="true" aria-controls="Nav-Apps">
Apps
<vc:icon symbol="caret-down"/>
</button>
</header>
<div id="Nav-Apps" class="accordion-collapse collapse show" aria-labelledby="Nav-Apps-Header">
<div class="accordion-body">
<ul class="navbar-nav">
@foreach (var app in Model.Apps)
{
<li class="nav-item">
<a asp-area="" asp-controller="Apps" asp-action="@app.Action" asp-route-appId="@app.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(AppsNavPages.Update, app.Id)" id="@($"StoreNav-App-{app.Id}")">
<vc:icon symbol="@app.AppType.ToLower()"/>
<span>@app.AppName</span>
</a>
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="Apps" asp-action="CreateApp" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(AppsNavPages.Create)" id="StoreNav-CreateApp">
<vc:icon symbol="new"/>
<span>New App</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<header class="accordion-header" id="Nav-Manage-Header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#Nav-Manage" aria-expanded="true" aria-controls="Nav-Manage">
Manage
<vc:icon symbol="caret-down"/>
</button>
</header>
<div id="Nav-Manage" class="accordion-collapse collapse show" aria-labelledby="Nav-Manage-Header">
<div class="accordion-body">
<ul class="navbar-nav">
<li class="nav-item">
<a asp-area="" asp-controller="Stores" asp-action="PaymentMethods" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(StoreNavPages))" id="StoreNav-Invoices">
<vc:icon symbol="settings"/>
<span>Settings</span>
</a>
</li>
</ul>
</div>
</div>
</div>
}
<script>
const navCollapsed = window.localStorage.getItem('btcpay-nav-collapsed')
const collapsed = navCollapsed ? JSON.parse(navCollapsed) : []
collapsed.forEach(id => {
const el = document.getElementById(id)
const btn = el && el.previousElementSibling.querySelector(`[aria-controls="${id}"]`)
if (el && btn) {
el.classList.remove('show')
btn.classList.add('collapsed')
btn.setAttribute('aria-expanded', 'false')
}
})
</script>
}
else if (Env.IsSecure)
{
<ul class="navbar-nav">
@if (!(await SettingsRepository.GetPolicies()).LockSubscription)
{
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger" id="Nav-Register">Register</a>
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger" id="Nav-Login">Log in</a>
</li>
</ul>
}
</div>
<ul id="mainNavSettings" class="navbar-nav border-top p-3 px-lg-4">
@if (User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item">
<a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(ServerNavPages))" id="Nav-ServerSettings">
<vc:icon symbol="server-settings"/>
<span>Server Settings</span>
</a>
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-Account">
<vc:icon symbol="account"/>
<span>Account</span>
</a>
</li>
@if (!theme.CustomTheme)
{
<li class="nav-item">
<vc:theme-switch css-class="nav-link"/>
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" class="nav-link js-scroll-trigger" id="Logout">
<i class="fa fa-sign-out"></i>
<span>Logout</span>
</a>
</li>
</ul>
</nav>

View File

@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitcoin.Secp256k1;
namespace BTCPayServer.Components.MainNav
{
public class MainNav : ViewComponent
{
private const string RootName = "Global";
private readonly AppService _appService;
private readonly StoreRepository _storeRepo;
private readonly StoresController _storesController;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly UserManager<ApplicationUser> _userManager;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
public MainNav(
AppService appService,
StoreRepository storeRepo,
StoresController storesController,
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
{
_storeRepo = storeRepo;
_appService = appService;
_userManager = userManager;
_networkProvider = networkProvider;
_storesController = storesController;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var store = ViewContext.HttpContext.GetStoreData();
var vm = new MainNavViewModel { Store = store };
#if ALTCOINS
vm.AltcoinsBuild = true;
#endif
if (store != null)
{
var storeBlob = store.GetStoreBlob();
// Wallets
_storesController.AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
vm.DerivationSchemes = derivationSchemes;
vm.LightningNodes = lightningNodes;
// Apps
var apps = await _appService.GetAllApps(UserId, false, store.Id);
vm.Apps = apps.Select(a => new StoreApp
{
Id = a.Id,
AppName = a.AppName,
AppType = a.AppType,
IsOwner = a.IsOwner
}).ToList();
}
return View(vm);
}
private string UserId => _userManager.GetUserId(HttpContext.User);
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
namespace BTCPayServer.Components.MainNav
{
public class MainNavViewModel
{
public StoreData Store { get; set; }
public List<StoreDerivationScheme> DerivationSchemes { get; set; }
public List<StoreLightningNode> LightningNodes { get; set; }
public List<StoreApp> Apps { get; set; }
public bool AltcoinsBuild { get; set; }
}
public class StoreApp
{
public string Id { get; set; }
public string AppName { get; set; }
public string AppType { get; set; }
public string Action { get => $"Update{AppType}"; }
public bool IsOwner { get; set; }
}
}

View File

@@ -1,23 +1,19 @@
@inject UserManager<ApplicationUser> UserManager @inject UserManager<ApplicationUser> UserManager
@inject ISettingsRepository SettingsRepository @inject ISettingsRepository SettingsRepository
@using BTCPayServer.HostedServices
@using BTCPayServer.Views.Notifications @using BTCPayServer.Views.Notifications
@using BTCPayServer.Abstractions.Extensions @using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts @using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Services
@model BTCPayServer.Components.NotificationsDropdown.NotificationSummaryViewModel @model BTCPayServer.Components.NotificationsDropdown.NotificationSummaryViewModel
@addTagHelper *, BundlerMinifier.TagHelpers @addTagHelper *, BundlerMinifier.TagHelpers
@if (Model.UnseenCount > 0) <div id="Notifications">
{ @if (Model.UnseenCount > 0)
<li class="nav-item dropdown" id="notifications-nav-item"> {
<a class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" href="#" id="NotificationsDropdownToggle" role="button" data-bs-toggle="dropdown"> <button id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications" type="button" data-bs-toggle="dropdown">
<span class="d-inline-block d-lg-none">Notifications</span> <vc:icon symbol="notifications" />
<i class="fa fa-bell d-lg-inline-block d-none"></i> <span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@Model.UnseenCount</span>
<span class="notification-badge badge rounded-pill bg-danger">@Model.UnseenCount</span> </button>
</a> <div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
<div class="dropdown-menu dropdown-menu-end text-center notification-dropdown" aria-labelledby="NotificationsDropdownToggle">
<div class="d-flex align-items-center justify-content-between py-3 px-4 border-bottom border-light"> <div class="d-flex align-items-center justify-content-between py-3 px-4 border-bottom border-light">
<h5 class="m-0">Notifications</h5> <h5 class="m-0">Notifications</h5>
<form id="notificationsForm" asp-controller="Notifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Context.Request.GetCurrentPathWithQueryString()" method="post"> <form id="notificationsForm" asp-controller="Notifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Context.Request.GetCurrentPathWithQueryString()" method="post">
@@ -30,7 +26,6 @@
<div class="me-3"> <div class="me-3">
<vc:icon symbol="note" /> <vc:icon symbol="note" />
</div> </div>
<div class="notification-item__content"> <div class="notification-item__content">
<div class="text-start text-wrap"> <div class="text-start text-wrap">
@notif.Body @notif.Body
@@ -45,13 +40,11 @@
<a asp-controller="Notifications" asp-action="Index">View all</a> <a asp-controller="Notifications" asp-action="Index">View all</a>
</div> </div>
</div> </div>
</li> }
} else
else {
{ <a asp-controller="Notifications" asp-action="Index" id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications">
<li class="nav-item" id="notifications-nav-item"> <vc:icon symbol="notifications" />
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" id="Notifications">
<span class="d-lg-none d-sm-block">Notifications</span><i class="fa fa-bell d-lg-inline-block d-none"></i>
</a> </a>
</li> }
} </div>

View File

@@ -1,9 +1,5 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Models.NotificationViewModels;
namespace BTCPayServer.Components.NotificationsDropdown namespace BTCPayServer.Components.NotificationsDropdown
{ {

View File

@@ -0,0 +1,20 @@
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
<div id="StoreSelector">
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill @(Model.CurrentStoreId == null ? "text-secondary" :"")" type="button" data-bs-toggle="dropdown" aria-expanded="false">@(Model.CurrentStoreId == null ? "Select Store" : Model.CurrentDisplayName)</button>
<ul id="StoreSelectorMenu" class="dropdown-menu" aria-labelledby="StoreSelectorToggle">
@foreach (var option in Model.Options)
{
<li><a asp-controller="Stores" asp-action="PaymentMethods" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@option.Text</a></li>
}
<li><hr class="dropdown-divider"></li>
<li><a asp-controller="UserStores" asp-action="CreateStore" class="dropdown-item" id="StoreSelectorMenuItem-Create">Create Store</a></li>
</ul>
</div>
<noscript>
<span class="h5 mb-0 me-2">@Model.CurrentDisplayName</span>
<a asp-controller="UserStores" asp-action="ListStores">Stores</a>
</noscript>
</div>

View File

@@ -0,0 +1,48 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin.Secp256k1;
namespace BTCPayServer.Components.StoreSelector
{
public class StoreSelector : ViewComponent
{
private const string RootName = "Global";
private readonly StoreRepository _storeRepo;
private readonly UserManager<ApplicationUser> _userManager;
public StoreSelector(StoreRepository storeRepo, UserManager<ApplicationUser> userManager)
{
_storeRepo = storeRepo;
_userManager = userManager;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var userId = _userManager.GetUserId(UserClaimsPrincipal);
var stores = await _storeRepo.GetStoresByUserId(userId);
var currentStore = ViewContext.HttpContext.GetStoreData();
var options = stores
.Select(store => new SelectListItem
{
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id
})
.ToList();
var vm = new StoreSelectorViewModel
{
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName ?? RootName
};
return View(vm);
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Components.StoreSelector
{
public class StoreSelectorViewModel
{
public List<SelectListItem> Options { get; set; }
public string CurrentStoreId { get; set; }
public string CurrentDisplayName { get; set; }
}
}

View File

@@ -1,12 +1,9 @@
@model BTCPayServer.Components.ThemeSwitch.ThemeSwitchViewModel @model BTCPayServer.Components.ThemeSwitch.ThemeSwitchViewModel
<button class="btcpay-theme-switch @Model.CssClass"> <button class="btcpay-theme-switch @Model.CssClass">
<svg class="@(string.IsNullOrEmpty(Model.Responsive) || Model.Responsive == "none" ? "d-inline-block" : $"d-{Model.Responsive}-inline-block d-none")" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10"> <svg class="d-inline-block icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
<path class="btcpay-theme-switch-dark" transform="translate(1 1)" d="M2.72 0A3.988 3.988 0 000 3.78c0 2.21 1.79 4 4 4 1.76 0 3.25-1.14 3.78-2.72-.4.13-.83.22-1.28.22-2.21 0-4-1.79-4-4 0-.45.08-.88.22-1.28z"/> <path class="btcpay-theme-switch-dark" transform="translate(3 3)" d="M2.72 0A3.988 3.988 0 000 3.78c0 2.21 1.79 4 4 4 1.76 0 3.25-1.14 3.78-2.72-.4.13-.83.22-1.28.22-2.21 0-4-1.79-4-4 0-.45.08-.88.22-1.28z"/>
<path class="btcpay-theme-switch-light" transform="translate(1 1)" d="M4 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5S4.28 0 4 0zM1.5 1c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm5 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM4 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zM.5 3.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm7 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM1.5 6c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm5 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM4 7c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5S4.28 7 4 7z"/> <path class="btcpay-theme-switch-light" transform="translate(3 3)" d="M4 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5S4.28 0 4 0zM1.5 1c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm5 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM4 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zM.5 3.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm7 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM1.5 6c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm5 0c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM4 7c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5S4.28 7 4 7z"/>
</svg> </svg>
@if (!string.IsNullOrEmpty(Model.Responsive)) <span class="d-inline-block"><span class="btcpay-theme-switch-dark">Dark theme</span><span class="btcpay-theme-switch-light">Light theme</span></span>
{
<span class="d-@Model.Responsive-none d-inline-block"><span class="btcpay-theme-switch-dark">Dark theme</span><span class="btcpay-theme-switch-light">Light theme</span></span>
}
</button> </button>

View File

@@ -4,12 +4,11 @@ namespace BTCPayServer.Components.ThemeSwitch
{ {
public class ThemeSwitch : ViewComponent public class ThemeSwitch : ViewComponent
{ {
public IViewComponentResult Invoke(string cssClass = null, string responsive = null) public IViewComponentResult Invoke(string cssClass = null)
{ {
var vm = new ThemeSwitchViewModel var vm = new ThemeSwitchViewModel
{ {
CssClass = cssClass, CssClass = cssClass,
Responsive = responsive
}; };
return View(vm); return View(vm);
} }

View File

@@ -3,6 +3,5 @@ namespace BTCPayServer.Components.ThemeSwitch
public class ThemeSwitchViewModel public class ThemeSwitchViewModel
{ {
public string CssClass { get; set; } public string CssClass { get; set; }
public string Responsive { get; set; }
} }
} }

View File

@@ -1,9 +1,11 @@
using System; using System;
using BTCPayServer.Data;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
@@ -21,6 +23,7 @@ namespace BTCPayServer.Controllers
} }
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/crowdfund")] [HttpGet("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId) public async Task<IActionResult> UpdateCrowdfund(string appId)
{ {
@@ -64,8 +67,8 @@ namespace BTCPayServer.Controllers
}; };
return View(vm); return View(vm);
} }
[HttpPost]
[Route("{appId}/settings/crowdfund")] [HttpPost("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command) public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
{ {
var app = await GetOwnedApp(appId, AppType.Crowdfund); var app = await GetOwnedApp(appId, AppType.Crowdfund);
@@ -77,7 +80,7 @@ namespace BTCPayServer.Controllers
try try
{ {
vm.PerksTemplate = _AppService.SerializeTemplate(_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency)); vm.PerksTemplate = _appService.SerializeTemplate(_appService.Parse(vm.PerksTemplate, vm.TargetCurrency));
} }
catch catch
{ {
@@ -155,9 +158,9 @@ namespace BTCPayServer.Controllers
app.TagAllInvoices = vm.UseAllStoreInvoices; app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings); app.SetSettings(newSettings);
await _AppService.UpdateOrCreateApp(app); await _appService.UpdateOrCreateApp(app);
_EventAggregator.Publish(new AppUpdated() _eventAggregator.Publish(new AppUpdated()
{ {
AppId = appId, AppId = appId,
StoreId = app.StoreDataId, StoreId = app.StoreDataId,

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Linq; using System.Linq;
using BTCPayServer.Data;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -88,13 +87,13 @@ namespace BTCPayServer.Controllers
public bool? RedirectAutomatically { get; set; } public bool? RedirectAutomatically { get; set; }
} }
[HttpGet] [HttpGet("{appId}/settings/pos")]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId) public async Task<IActionResult> UpdatePointOfSale(string appId)
{ {
var app = await GetOwnedApp(appId, AppType.PointOfSale); var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null) if (app == null)
return NotFound(); return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>(); var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView; settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false; settings.EnableShoppingCart = false;
@@ -143,8 +142,7 @@ namespace BTCPayServer.Controllers
} }
try try
{ {
var items = _appService.Parse(settings.Template, settings.Currency);
var items = _AppService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">"); builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />"); builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
@@ -162,8 +160,8 @@ namespace BTCPayServer.Controllers
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}"; vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
return View(vm); return View(vm);
} }
[HttpPost]
[Route("{appId}/settings/pos")] [HttpPost("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm) public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{ {
var app = await GetOwnedApp(appId, AppType.PointOfSale); var app = await GetOwnedApp(appId, AppType.PointOfSale);
@@ -178,7 +176,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency"); ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try try
{ {
vm.Template = _AppService.SerializeTemplate(_AppService.Parse(vm.Template, vm.Currency)); vm.Template = _appService.SerializeTemplate(_appService.Parse(vm.Template, vm.Currency));
} }
catch catch
{ {
@@ -211,12 +209,11 @@ namespace BTCPayServer.Controllers
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically), RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically),
RequiresRefundEmail = vm.RequiresRefundEmail, RequiresRefundEmail = vm.RequiresRefundEmail,
}); });
await _AppService.UpdateOrCreateApp(app); await _appService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated"; TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale), new { appId }); return RedirectToAction(nameof(UpdatePointOfSale), new { appId });
} }
private int[] ListSplit(string list, string separator = ",") private int[] ListSplit(string list, string separator = ",")
{ {
if (string.IsNullOrEmpty(list)) if (string.IsNullOrEmpty(list))
@@ -229,7 +226,7 @@ namespace BTCPayServer.Controllers
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]"); Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
list = charsToDestroy.Replace(list, ""); list = charsToDestroy.Replace(list, "");
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
} }
} }
} }

View File

@@ -3,13 +3,10 @@ using BTCPayServer.Data;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -18,65 +15,58 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
[Route("apps")] [Route("apps")]
public partial class AppsController : Controller public partial class AppsController : Controller
{ {
public AppsController( public AppsController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator, EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencies, CurrencyNameTable currencies,
EmailSenderFactory emailSenderFactory, StoreRepository storeRepository,
Services.Stores.StoreRepository storeRepository, AppService appService)
AppService AppService)
{ {
_UserManager = userManager; _userManager = userManager;
_ContextFactory = contextFactory; _eventAggregator = eventAggregator;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_currencies = currencies; _currencies = currencies;
_emailSenderFactory = emailSenderFactory;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_AppService = AppService; _appService = appService;
} }
private readonly UserManager<ApplicationUser> _UserManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContextFactory _ContextFactory; private readonly EventAggregator _eventAggregator;
private readonly EventAggregator _EventAggregator;
private readonly BTCPayNetworkProvider _NetworkProvider;
private readonly CurrencyNameTable _currencies; private readonly CurrencyNameTable _currencies;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly AppService _AppService; private readonly AppService _appService;
public string CreatedAppId { get; set; } public string CreatedAppId { get; set; }
[HttpGet("/stores/{storeId}/apps")]
public async Task<IActionResult> ListApps( public async Task<IActionResult> ListApps(
string storeId,
string sortOrder = null, string sortOrder = null,
string sortOrderColumn = null string sortOrderColumn = null
) )
{ {
var apps = await _AppService.GetAllApps(GetUserId()); var apps = await _appService.GetAllApps(GetUserId(), false, CurrentStore.Id);
if (sortOrder != null && sortOrderColumn != null) if (sortOrder != null && sortOrderColumn != null)
{ {
apps = apps.OrderByDescending(app => apps = apps.OrderByDescending(app =>
{
switch (sortOrderColumn)
{ {
switch (sortOrderColumn) case nameof(app.AppName):
{ return app.AppName;
case nameof(app.AppName): case nameof(app.StoreName):
return app.AppName; return app.StoreName;
case nameof(app.StoreName): case nameof(app.AppType):
return app.StoreName; return app.AppType;
case nameof(app.AppType): default:
return app.AppType; return app.Id;
default: }
return app.Id; }).ToArray();
}
}).ToArray();
switch (sortOrder) switch (sortOrder)
{ {
@@ -90,64 +80,27 @@ namespace BTCPayServer.Controllers
} }
} }
return View(new ListAppsViewModel() return View(new ListAppsViewModel
{ {
Apps = apps Apps = apps
}); });
} }
[HttpPost] [HttpGet("/stores/{storeId}/apps/create")]
[Route("{appId}/delete")] public IActionResult CreateApp(string storeId)
public async Task<IActionResult> DeleteAppPost(string appId)
{ {
var appData = await GetOwnedApp(appId); return View(new CreateAppViewModel
if (appData == null) {
return NotFound(); StoreId = CurrentStore.Id
if (await _AppService.DeleteApp(appData)) });
TempData[WellKnownTempData.SuccessMessage] = "App deleted successfully.";
return RedirectToAction(nameof(ListApps));
} }
[HttpGet] [HttpPost("/stores/{storeId}/apps/create")]
[Route("create")] public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
public async Task<IActionResult> CreateApp()
{ {
var stores = await _AppService.GetOwnedStores(GetUserId()); vm.StoreId = CurrentStore.Id;
if (stores.Length == 0)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
vm.SetStores(stores);
return View(vm);
}
[HttpPost] if (!Enum.TryParse(vm.SelectedAppType, out AppType appType))
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;
vm.SetStores(stores);
vm.SelectedStore = selectedStore;
if (!Enum.TryParse<AppType>(vm.SelectedAppType, out AppType appType))
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type"); ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
if (!ModelState.IsValid) if (!ModelState.IsValid)
@@ -155,14 +108,9 @@ namespace BTCPayServer.Controllers
return View(vm); return View(vm);
} }
if (!stores.Any(s => s.Id == selectedStore))
{
TempData[WellKnownTempData.ErrorMessage] = "You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
var appData = new AppData var appData = new AppData
{ {
StoreDataId = selectedStore, StoreDataId = CurrentStore.Id,
Name = vm.AppName, Name = vm.AppName,
AppType = appType.ToString() AppType = appType.ToString()
}; };
@@ -171,18 +119,16 @@ namespace BTCPayServer.Controllers
switch (appType) switch (appType)
{ {
case AppType.Crowdfund: case AppType.Crowdfund:
var emptyCrowdfund = new CrowdfundSettings(); var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
emptyCrowdfund.TargetCurrency = defaultCurrency;
appData.SetSettings(emptyCrowdfund); appData.SetSettings(emptyCrowdfund);
break; break;
case AppType.PointOfSale: case AppType.PointOfSale:
var empty = new PointOfSaleSettings(); var empty = new PointOfSaleSettings { Currency = defaultCurrency };
empty.Currency = defaultCurrency;
appData.SetSettings(empty); appData.SetSettings(empty);
break; break;
} }
await _AppService.UpdateOrCreateApp(appData); await _appService.UpdateOrCreateApp(appData);
TempData[WellKnownTempData.SuccessMessage] = "App successfully created"; TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
CreatedAppId = appData.Id; CreatedAppId = appData.Id;
@@ -193,19 +139,10 @@ namespace BTCPayServer.Controllers
case AppType.Crowdfund: case AppType.Crowdfund:
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = appData.Id }); return RedirectToAction(nameof(UpdateCrowdfund), new { appId = appData.Id });
default: default:
return RedirectToAction(nameof(ListApps)); return RedirectToAction(nameof(ListApps), new { storeId = appData.StoreDataId });
} }
} }
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
{
if (String.IsNullOrWhiteSpace(currency))
{
currency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
}
return currency.Trim().ToUpperInvariant();
}
[HttpGet("{appId}/delete")] [HttpGet("{appId}/delete")]
public async Task<IActionResult> DeleteApp(string appId) public async Task<IActionResult> DeleteApp(string appId)
{ {
@@ -215,14 +152,39 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{appData.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete")); return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{appData.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
} }
[HttpPost("{appId}/delete")]
public async Task<IActionResult> DeleteAppPost(string appId)
{
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await _appService.DeleteApp(appData))
TempData[WellKnownTempData.SuccessMessage] = "App deleted successfully.";
return RedirectToAction(nameof(ListApps), new { storeId = appData.StoreDataId });
}
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
{
if (string.IsNullOrWhiteSpace(currency))
{
currency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
}
return currency.Trim().ToUpperInvariant();
}
private Task<AppData> GetOwnedApp(string appId, AppType? type = null) private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{ {
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type); return _appService.GetAppDataIfOwner(GetUserId(), appId, type);
} }
private string GetUserId() private string GetUserId()
{ {
return _UserManager.GetUserId(User); return _userManager.GetUserId(User);
}
private StoreData CurrentStore
{
get => HttpContext.GetStoreData();
} }
} }
} }

View File

@@ -81,7 +81,7 @@ namespace BTCPayServer.Controllers
} }
[HttpGet("invoices/{invoiceId}")] [HttpGet("invoices/{invoiceId}")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Invoice(string invoiceId) public async Task<IActionResult> Invoice(string invoiceId)
{ {
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
@@ -97,12 +97,12 @@ namespace BTCPayServer.Controllers
var store = await _StoreRepository.FindStore(invoice.StoreId); var store = await _StoreRepository.FindStore(invoice.StoreId);
var invoiceState = invoice.GetInvoiceState(); var invoiceState = invoice.GetInvoiceState();
var model = new InvoiceDetailsModel() var model = new InvoiceDetailsModel
{ {
StoreId = store.Id, StoreId = store.Id,
StoreName = store.StoreName, StoreName = store.StoreName,
StoreLink = Url.Action(nameof(StoresController.PaymentMethods), "Stores", new { storeId = store.Id }), StoreLink = Url.Action(nameof(StoresController.PaymentMethods), "Stores", new { storeId = store.Id }),
PaymentRequestLink = Url.Action(nameof(PaymentRequestController.ViewPaymentRequest), "PaymentRequest", new { id = invoice.Metadata.PaymentRequestId }), PaymentRequestLink = Url.Action(nameof(PaymentRequestController.ViewPaymentRequest), "PaymentRequest", new { payReqId = invoice.Metadata.PaymentRequestId }),
Id = invoice.Id, Id = invoice.Id,
State = invoiceState.Status.ToModernStatus().ToString(), State = invoiceState.Status.ToModernStatus().ToString(),
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
@@ -183,6 +183,8 @@ namespace BTCPayServer.Controllers
new { pullPaymentId = ppId }); new { pullPaymentId = ppId });
} }
HttpContext.SetStoreData(invoice.StoreData);
var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods(); var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods();
var pmis = paymentMethods.Select(method => method.GetId()).ToList(); var pmis = paymentMethods.Select(method => method.GetId()).ToList();
var options = (await payoutHandlers.GetSupportedPaymentMethods(invoice.StoreData)).Where(id => pmis.Contains(id)).ToList(); var options = (await payoutHandlers.GetSupportedPaymentMethods(invoice.StoreData)).Where(id => pmis.Contains(id)).ToList();
@@ -216,18 +218,21 @@ namespace BTCPayServer.Controllers
} }
[HttpPost("invoices/{invoiceId}/refund")] [HttpPost("invoices/{invoiceId}/refund")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken) public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken)
{ {
using var ctx = _dbContextFactory.CreateContext(); using var ctx = _dbContextFactory.CreateContext();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId); var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice is null) if (invoice is null)
return NotFound(); return NotFound();
var store = await _StoreRepository.FindStore(invoice.StoreId, GetUserId()); var store = await _StoreRepository.FindStore(invoice.StoreId, GetUserId());
if (store is null) if (store is null)
return NotFound(); return NotFound();
if (!CanRefund(invoice.GetInvoiceState())) if (!CanRefund(invoice.GetInvoiceState()))
return NotFound(); return NotFound();
var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod); var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod);
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true); var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8; var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
@@ -421,7 +426,7 @@ namespace BTCPayServer.Controllers
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> MassAction(string command, string[] selectedItems) public async Task<IActionResult> MassAction(string command, string[] selectedItems, string? storeId = null)
{ {
if (selectedItems != null) if (selectedItems != null)
{ {
@@ -435,7 +440,7 @@ namespace BTCPayServer.Controllers
} }
} }
return RedirectToAction(nameof(ListInvoices)); return RedirectToAction(nameof(ListInvoices), new { storeId });
} }
[HttpGet("i/{invoiceId}")] [HttpGet("i/{invoiceId}")]
@@ -732,18 +737,30 @@ namespace BTCPayServer.Controllers
return Ok("{}"); return Ok("{}");
} }
[HttpGet("/stores/{storeId}/invoices")]
[HttpGet("invoices")] [HttpGet("invoices")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(InvoicesModel? model = null) public async Task<IActionResult> ListInvoices(InvoicesModel? model = null, string? storeId = null)
{ {
model = this.ParseListQuery(model ?? new InvoicesModel()); model = this.ParseListQuery(model ?? new InvoicesModel());
var fs = new SearchString(model.SearchTerm); var fs = new SearchString(model.SearchTerm);
var storeIds = fs.GetFilterArray("storeid") != null ? fs.GetFilterArray("storeid") : new List<string>().ToArray(); var storeIds = storeId == null
? fs.GetFilterArray("storeid") != null ? fs.GetFilterArray("storeid") : new List<string>().ToArray()
: new []{ storeId };
model.StoreIds = storeIds; model.StoreIds = storeIds;
if (storeId != null)
{
var store = await _StoreRepository.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
HttpContext.SetStoreData(store);
model.StoreId = store.Id;
}
InvoiceQuery invoiceQuery = GetInvoiceQuery(model.SearchTerm, model.TimezoneOffset ?? 0); InvoiceQuery invoiceQuery = GetInvoiceQuery(model.SearchTerm, model.TimezoneOffset ?? 0);
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery); var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Take = model.Count; invoiceQuery.Take = model.Count;
@@ -823,6 +840,7 @@ namespace BTCPayServer.Controllers
nameof(SelectListItem.Text)); nameof(SelectListItem.Text));
} }
[HttpGet("/stores/{storeId}/invoices/create")]
[HttpGet("invoices/create")] [HttpGet("invoices/create")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
@@ -840,15 +858,25 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
} }
if (model?.StoreId != null)
{
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store == null)
return NotFound();
HttpContext.SetStoreData(store);
}
var vm = new CreateInvoiceModel var vm = new CreateInvoiceModel
{ {
Stores = stores, Stores = stores,
StoreId = model?.StoreId,
AvailablePaymentMethods = GetPaymentMethodsSelectList() AvailablePaymentMethods = GetPaymentMethodsSelectList()
}; };
return View(vm); return View(vm);
} }
[HttpPost("/stores/{storeId}/invoices/create")]
[HttpPost("invoices/create")] [HttpPost("invoices/create")]
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
@@ -901,7 +929,7 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!"; TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
CreatedInvoiceId = result.Data.Id; CreatedInvoiceId = result.Data.Id;
return RedirectToAction(nameof(ListInvoices)); return RedirectToAction(nameof(ListInvoices), new { result.Data.StoreId });
} }
catch (BitpayHttpException ex) catch (BitpayHttpException ex)
{ {

View File

@@ -4,8 +4,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters; using BTCPayServer.Filters;
@@ -18,15 +17,14 @@ using BTCPayServer.Services.Stores;
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;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("payment-requests")] [Route("payment-requests")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class PaymentRequestController : Controller public class PaymentRequestController : Controller
{ {
private readonly InvoiceController _InvoiceController; private readonly InvoiceController _InvoiceController;
@@ -61,16 +59,17 @@ namespace BTCPayServer.Controllers
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
} }
[HttpGet("")]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> GetPaymentRequests(ListPaymentRequestsViewModel model = null) [HttpGet("/stores/{storeId}/payment-requests")]
public async Task<IActionResult> GetPaymentRequests(string storeId, ListPaymentRequestsViewModel model = null)
{ {
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel()); model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
var includeArchived = new SearchString(model.SearchTerm).GetFilterBool("includearchived") == true; var includeArchived = new SearchString(model.SearchTerm).GetFilterBool("includearchived") == true;
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery() var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
{ {
UserId = GetUserId(), UserId = GetUserId(),
StoreId = CurrentStore.Id,
Skip = model.Skip, Skip = model.Skip,
Count = model.Count, Count = model.Count,
IncludeArchived = includeArchived IncludeArchived = includeArchived
@@ -81,40 +80,30 @@ namespace BTCPayServer.Controllers
return View(model); return View(model);
} }
[HttpGet("edit/{id?}")] [HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
public async Task<IActionResult> EditPaymentRequest(string id) public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
{ {
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId()); var data = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (data == null && !string.IsNullOrEmpty(id)) if (data == null && !string.IsNullOrEmpty(payReqId))
{ {
return NotFound(); return NotFound();
} }
SelectList stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), return View(nameof(EditPaymentRequest), new UpdatePaymentRequestViewModel(data)
nameof(StoreData.StoreName), data?.StoreDataId);
if (!stores.Any())
{ {
TempData.SetStatusMessageModel(new StatusMessageModel StoreId = CurrentStore.Id
{ });
Html =
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction("GetPaymentRequests");
}
return View(nameof(EditPaymentRequest), new UpdatePaymentRequestViewModel(data) { Stores = stores });
} }
[HttpPost("edit/{id?}")] [HttpPost("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel) public async Task<IActionResult> EditPaymentRequest(string payReqId, UpdatePaymentRequestViewModel viewModel)
{ {
if (string.IsNullOrEmpty(viewModel.Currency) || if (string.IsNullOrEmpty(viewModel.Currency) ||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null) _Currencies.GetCurrencyData(viewModel.Currency, false) == null)
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency"); ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId()); var data = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (data == null && !string.IsNullOrEmpty(id)) if (data == null && !string.IsNullOrEmpty(payReqId))
{ {
return NotFound(); return NotFound();
} }
@@ -126,10 +115,6 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
return View(nameof(EditPaymentRequest), viewModel); return View(nameof(EditPaymentRequest), viewModel);
} }
@@ -153,7 +138,7 @@ namespace BTCPayServer.Controllers
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts; blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
data.SetBlob(blob); data.SetBlob(blob);
if (string.IsNullOrEmpty(id)) if (string.IsNullOrEmpty(payReqId))
{ {
data.Created = DateTimeOffset.UtcNow; data.Created = DateTimeOffset.UtcNow;
} }
@@ -162,14 +147,14 @@ namespace BTCPayServer.Controllers
_EventAggregator.Publish(new PaymentRequestUpdated { Data = data, PaymentRequestId = data.Id, }); _EventAggregator.Publish(new PaymentRequestUpdated { Data = data, PaymentRequestId = data.Id, });
TempData[WellKnownTempData.SuccessMessage] = "Saved"; TempData[WellKnownTempData.SuccessMessage] = "Saved";
return RedirectToAction(nameof(EditPaymentRequest), new { id = data.Id }); return RedirectToAction(nameof(EditPaymentRequest), new { storeId = CurrentStore.Id, payReqId = data.Id });
} }
[HttpGet("{id}")] [HttpGet("{payReqId}")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequest(string id) public async Task<IActionResult> ViewPaymentRequest(string payReqId)
{ {
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId()); var result = await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId());
if (result == null) if (result == null)
{ {
return NotFound(); return NotFound();
@@ -179,9 +164,9 @@ namespace BTCPayServer.Controllers
return View(result); return View(result);
} }
[HttpGet("{id}/pay")] [HttpGet("{payReqId}/pay")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true, public async Task<IActionResult> PayPaymentRequest(string payReqId, bool redirectToInvoice = true,
decimal? amount = null, CancellationToken cancellationToken = default) decimal? amount = null, CancellationToken cancellationToken = default)
{ {
if (amount.HasValue && amount.Value <= 0) if (amount.HasValue && amount.Value <= 0)
@@ -189,7 +174,7 @@ namespace BTCPayServer.Controllers
return BadRequest("Please provide an amount greater than 0"); return BadRequest("Please provide an amount greater than 0");
} }
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId()); var result = await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId());
if (result == null) if (result == null)
{ {
return NotFound(); return NotFound();
@@ -199,7 +184,7 @@ namespace BTCPayServer.Controllers
{ {
if (redirectToInvoice) if (redirectToInvoice)
{ {
return RedirectToAction("ViewPaymentRequest", new { Id = id }); return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
} }
return BadRequest("Payment Request cannot be paid as it has been archived"); return BadRequest("Payment Request cannot be paid as it has been archived");
@@ -210,7 +195,7 @@ namespace BTCPayServer.Controllers
{ {
if (redirectToInvoice) if (redirectToInvoice)
{ {
return RedirectToAction("ViewPaymentRequest", new { Id = id }); return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
} }
return BadRequest("Payment Request has already been settled."); return BadRequest("Payment Request has already been settled.");
@@ -220,7 +205,7 @@ namespace BTCPayServer.Controllers
{ {
if (redirectToInvoice) if (redirectToInvoice)
{ {
return RedirectToAction("ViewPaymentRequest", new { Id = id }); return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
} }
return BadRequest("Payment Request has expired"); return BadRequest("Payment Request has expired");
@@ -249,18 +234,18 @@ namespace BTCPayServer.Controllers
else else
amount = result.AmountDue; amount = result.AmountDue;
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null, cancellationToken); var pr = await _PaymentRequestRepository.FindPaymentRequest(payReqId, null, cancellationToken);
var blob = pr.GetBlob(); var blob = pr.GetBlob();
var store = pr.StoreData; var store = pr.StoreData;
try try
{ {
var redirectUrl = _linkGenerator.PaymentRequestLink(id, Request.Scheme, Request.Host, Request.PathBase); var redirectUrl = _linkGenerator.PaymentRequestLink(payReqId, Request.Scheme, Request.Host, Request.PathBase);
var invoiceMetadata = var invoiceMetadata =
new InvoiceMetadata new InvoiceMetadata
{ {
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(id), OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(payReqId),
PaymentRequestId = id, PaymentRequestId = payReqId,
BuyerEmail = result.Email BuyerEmail = result.Email
}; };
@@ -273,7 +258,7 @@ namespace BTCPayServer.Controllers
Checkout = {RedirectURL = redirectUrl} Checkout = {RedirectURL = redirectUrl}
}; };
var additionalTags = new List<string> {PaymentRequestRepository.GetInternalTag(id)}; var additionalTags = new List<string> {PaymentRequestRepository.GetInternalTag(payReqId)};
var newInvoice = await _InvoiceController.CreateInvoiceCoreRaw(invoiceRequest,store, Request.GetAbsoluteRoot(), additionalTags, cancellationToken); var newInvoice = await _InvoiceController.CreateInvoiceCoreRaw(invoiceRequest,store, Request.GetAbsoluteRoot(), additionalTags, cancellationToken);
if (redirectToInvoice) if (redirectToInvoice)
@@ -289,10 +274,10 @@ namespace BTCPayServer.Controllers
} }
} }
[HttpGet("{id}/cancel")] [HttpGet("{payReqId}/cancel")]
public async Task<IActionResult> CancelUnpaidPendingInvoice(string id, bool redirect = true) public async Task<IActionResult> CancelUnpaidPendingInvoice(string payReqId, bool redirect = true)
{ {
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId()); var result = await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId());
if (result == null) if (result == null)
{ {
return NotFound(); return NotFound();
@@ -318,21 +303,16 @@ namespace BTCPayServer.Controllers
if (redirect) if (redirect)
{ {
TempData[WellKnownTempData.SuccessMessage] = "Payment cancelled"; TempData[WellKnownTempData.SuccessMessage] = "Payment cancelled";
return RedirectToAction(nameof(ViewPaymentRequest), new { Id = id }); return RedirectToAction(nameof(ViewPaymentRequest), new { Id = payReqId });
} }
return Ok("Payment cancelled"); return Ok("Payment cancelled");
} }
private string GetUserId() [HttpGet("{payReqId}/clone")]
public async Task<IActionResult> ClonePaymentRequest(string payReqId)
{ {
return _UserManager.GetUserId(User); var result = await EditPaymentRequest(CurrentStore.Id, payReqId);
}
[HttpGet("{id}/clone")]
public async Task<IActionResult> ClonePaymentRequest(string id)
{
var result = await EditPaymentRequest(id);
if (result is ViewResult viewResult) if (result is ViewResult viewResult)
{ {
var model = (UpdatePaymentRequestViewModel)viewResult.Model; var model = (UpdatePaymentRequestViewModel)viewResult.Model;
@@ -346,15 +326,15 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
} }
[HttpGet("{id}/archive")] [HttpGet("{payReqId}/archive")]
public async Task<IActionResult> TogglePaymentRequestArchival(string id) public async Task<IActionResult> TogglePaymentRequestArchival(string payReqId)
{ {
var result = await EditPaymentRequest(id); var result = await EditPaymentRequest(CurrentStore.Id, payReqId);
if (result is ViewResult viewResult) if (result is ViewResult viewResult)
{ {
var model = (UpdatePaymentRequestViewModel)viewResult.Model; var model = (UpdatePaymentRequestViewModel)viewResult.Model;
model.Archived = !model.Archived; model.Archived = !model.Archived;
await EditPaymentRequest(id, model); await EditPaymentRequest(payReqId, model);
TempData[WellKnownTempData.SuccessMessage] = model.Archived TempData[WellKnownTempData.SuccessMessage] = model.Archived
? "The payment request has been archived and will no longer appear in the payment request list by default again." ? "The payment request has been archived and will no longer appear in the payment request list by default again."
: "The payment request has been unarchived and will appear in the payment request list by default."; : "The payment request has been unarchived and will appear in the payment request list by default.";
@@ -363,5 +343,15 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
} }
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
private StoreData CurrentStore
{
get => HttpContext.GetStoreData();
}
} }
} }

View File

@@ -2,12 +2,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -17,7 +14,6 @@ using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
@@ -25,18 +21,13 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using BundlerMinifier.TagHelpers;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
@@ -513,7 +504,7 @@ namespace BTCPayServer.Controllers
}); });
} }
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes) out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
{ {
var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var excludeFilters = storeBlob.GetExcludedPaymentMethods();
@@ -850,13 +841,6 @@ namespace BTCPayServer.Controllers
var userId = GetUserId(); var userId = GetUserId();
if (string.IsNullOrWhiteSpace(userId)) if (string.IsNullOrWhiteSpace(userId))
return Challenge(AuthenticationSchemes.Cookie); return Challenge(AuthenticationSchemes.Cookie);
var storeId = CurrentStore?.Id;
if (storeId != null)
{
var store = await _Repo.FindStore(storeId, userId);
if (store != null)
HttpContext.SetStoreData(store);
}
var model = new CreateTokenViewModel(); var model = new CreateTokenViewModel();
ViewBag.HidePublicKey = true; ViewBag.HidePublicKey = true;
ViewBag.ShowStores = true; ViewBag.ShowStores = true;

View File

@@ -26,18 +26,7 @@ namespace BTCPayServer.Models.AppViewModels
public string AppName { get; set; } public string AppName { get; set; }
[Display(Name = "Store")] [Display(Name = "Store")]
public string SelectedStore { get; set; } public string StoreId { get; set; }
public void SetStores(StoreData[] stores)
{
var defaultStore = stores[0].Id;
var choices = stores.Select(o => new Format() { Name = o.StoreName, Value = o.Id }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
Stores = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
SelectedStore = chosen.Value;
}
public SelectList Stores { get; set; }
[Display(Name = "App Type")] [Display(Name = "App Type")]
public string SelectedAppType { get; set; } public string SelectedAppType { get; set; }

View File

@@ -9,6 +9,7 @@ namespace BTCPayServer.Models.InvoicingModels
{ {
public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>(); public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>();
public string[] StoreIds { get; set; } public string[] StoreIds { get; set; }
public string StoreId { get; set; }
} }
public class InvoiceModel public class InvoiceModel

View File

@@ -13,7 +13,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public class ListPaymentRequestsViewModel : BasePagingViewModel public class ListPaymentRequestsViewModel : BasePagingViewModel
{ {
public List<ViewPaymentRequestViewModel> Items { get; set; } public List<ViewPaymentRequestViewModel> Items { get; set; }
} }
public class UpdatePaymentRequestViewModel public class UpdatePaymentRequestViewModel
@@ -82,6 +81,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public ViewPaymentRequestViewModel(PaymentRequestData data) public ViewPaymentRequestViewModel(PaymentRequestData data)
{ {
Id = data.Id; Id = data.Id;
StoreId = data.StoreDataId;
var blob = data.GetBlob(); var blob = data.GetBlob();
Archived = data.Archived; Archived = data.Archived;
Title = blob.Title; Title = blob.Title;
@@ -121,6 +121,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public string AmountDueFormatted { get; set; } public string AmountDueFormatted { get; set; }
public decimal Amount { get; set; } public decimal Amount { get; set; }
public string Id { get; set; } public string Id { get; set; }
public string StoreId { get; set; }
public string Currency { get; set; } public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; } public DateTime? ExpiryDate { get; set; }
public string Title { get; set; } public string Title { get; set; }

View File

@@ -2,10 +2,14 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
namespace BTCPayServer.Security namespace BTCPayServer.Security
{ {
@@ -14,14 +18,23 @@ namespace BTCPayServer.Security
private readonly HttpContext _HttpContext; private readonly HttpContext _HttpContext;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly PaymentRequestService _paymentRequestService;
private readonly InvoiceRepository _invoiceRepository;
public CookieAuthorizationHandler(IHttpContextAccessor httpContextAccessor, public CookieAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
StoreRepository storeRepository) StoreRepository storeRepository,
AppService appService,
InvoiceRepository invoiceRepository,
PaymentRequestService paymentRequestService)
{ {
_HttpContext = httpContextAccessor.HttpContext; _HttpContext = httpContextAccessor.HttpContext;
_userManager = userManager; _userManager = userManager;
_appService = appService;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_invoiceRepository = invoiceRepository;
_paymentRequestService = paymentRequestService;
} }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
{ {
@@ -37,15 +50,46 @@ namespace BTCPayServer.Security
return; return;
} }
string storeId = context.Resource is string s? s :_HttpContext.GetImplicitStoreId(); string storeId = context.Resource is string s ? s : _HttpContext.GetImplicitStoreId();
if (storeId == null) if (storeId == null)
return; {
var routeData = _HttpContext.GetRouteData();
if (routeData != null)
{
// resolve from app
if (routeData.Values.TryGetValue("appId", out var vAppId))
{
string appId = vAppId as string;
var app = await _appService.GetApp(appId, null);
storeId = app?.StoreDataId;
}
// resolve from payment request
else if (routeData.Values.TryGetValue("payReqId", out var vPayReqId))
{
string payReqId = vPayReqId as string;
var paymentRequest = await _paymentRequestService.GetPaymentRequest(payReqId);
storeId = paymentRequest?.StoreId;
}
// resolve from app
if (routeData.Values.TryGetValue("invoiceId", out var vInvoiceId))
{
string invoiceId = vInvoiceId as string;
var invoice = await _invoiceRepository.GetInvoice(invoiceId);
storeId = invoice?.StoreId;
}
}
// store could not be found
if (storeId == null)
{
return;
}
}
var userid = _userManager.GetUserId(context.User); var userid = _userManager.GetUserId(context.User);
if (string.IsNullOrEmpty(userid)) if (string.IsNullOrEmpty(userid))
return; return;
var store = await _storeRepository.FindStore(storeId, userid); var store = await _storeRepository.FindStore(storeId, userid);
bool success = false; bool success = false;

View File

@@ -2,14 +2,11 @@
ViewData["Title"] = "Access denied"; ViewData["Title"] = "Access denied";
} }
<section> <div class="container">
<div class="container"> <div class="row">
<div class="row"> <div class="col-md-4">
<div class="col-md-4"> <h4>@ViewData["Title"]</h4>
<h4>@ViewData["Title"]</h4> <p class="text-danger">You do not have access to this resource.</p>
<hr />
<p class="text-danger">You do not have access to this resource.</p>
</div>
</div> </div>
</div> </div>
</section> </div>

View File

@@ -4,36 +4,32 @@
Layout = "_LayoutSimple"; Layout = "_LayoutSimple";
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6 section-heading"> <div class="col-md-6 section-heading">
<h2>@ViewData["Title"]</h2> <h2>@ViewData["Title"]</h2>
<hr class="primary"> <hr class="primary">
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<form asp-action="ForgotPassword" method="post">
<h4>Start password reset</h4>
<p>
We all forget passwords every now and then. Just provide email address tied to
your account and we'll start the process of helping you recover your account.
</p>
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div> </div>
</section> </div>
<div class="row justify-content-center">
<div class="col-md-6">
<form asp-action="ForgotPassword" method="post">
<h4>Start password reset</h4>
<p>
We all forget passwords every now and then. Just provide email address tied to
your account and we'll start the process of helping you recover your account.
</p>
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />

View File

@@ -2,21 +2,11 @@
ViewData["Title"] = "Email sent!"; ViewData["Title"] = "Email sent!";
} }
<h2></h2> <div class="row">
<p> <div class="col-md-4">
</p> <h4>@ViewData["Title"]</h4>
<p>
Please check your email to reset your password.
<section> </p>
<div class="container">
<div class="row">
<div class="col-md-4">
<h4>@ViewData["Title"]</h4>
<hr />
<p>
Please check your email to reset your password.
</p>
</div>
</div>
</div> </div>
</section> </div>

View File

@@ -1,19 +1,13 @@
@{ @{
ViewData["Title"] = "Locked out"; ViewData["Title"] = "Locked out";
} }
<section>
<div class="container">
<div class="row"> <div class="row">
<div class="col-lg-12 section-heading"> <div class="col-lg-12 section-heading">
<h2>@ViewData["Title"]</h2> <h2>@ViewData["Title"]</h2>
<hr class="primary"> <hr class="primary">
</div>
<div class="col-lg-12 lead">
<p>This account has been locked out because of multiple invalid login attempts. Please try again later.</p>
</div>
</div>
</div> </div>
</section> <div class="col-lg-12 lead">
<p>This account has been locked out because of multiple invalid login attempts. Please try again later.</p>
</div>
</div>

View File

@@ -1,44 +1,39 @@
@model LoginWith2faViewModel @model LoginWith2faViewModel
<section class="pt-5"> <div class="row pt-5">
<div> <div class="col-lg-12 section-heading">
<div class="row"> <h2>Two-factor authentication</h2>
<div class="col-lg-12 section-heading"> <hr class="primary">
<h2>Two-factor authentication</h2>
<hr class="primary">
</div>
</div>
<div class="row mb-3">
<form class="col-12" method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-action="LoginWith2fa">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input asp-for="RememberMe" type="hidden"/>
<div class="form-group">
<label asp-for="TwoFactorCode" class="form-label"></label>
<input asp-for="TwoFactorCode" class="form-control" autocomplete="off" autofocus style="width:13em" />
<span asp-validation-for="TwoFactorCode" class="text-danger"></span>
</div>
<div class="form-group">
<div class="checkbox">
<label asp-for="RememberMachine" class="form-label">
<input asp-for="RememberMachine"/>
@Html.DisplayNameFor(m => m.RememberMachine)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Log in</button>
</div>
</form>
</div>
<div class="row">
<div class="col-12">
<p class="text-secondary">
Don't have access to your authenticator device?
<br>
You can <a asp-action="LoginWithRecoveryCode" asp-route-returnUrl="@ViewData["ReturnUrl"]">log in with a recovery code</a>.
</p>
</div>
</div>
</div> </div>
</section> </div>
<div class="row mb-3">
<form class="col-12" method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-action="LoginWith2fa">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input asp-for="RememberMe" type="hidden"/>
<div class="form-group">
<label asp-for="TwoFactorCode" class="form-label"></label>
<input asp-for="TwoFactorCode" class="form-control" autocomplete="off" autofocus style="width:13em" />
<span asp-validation-for="TwoFactorCode" class="text-danger"></span>
</div>
<div class="form-group">
<div class="checkbox">
<label asp-for="RememberMachine" class="form-label">
<input asp-for="RememberMachine"/>
@Html.DisplayNameFor(m => m.RememberMachine)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Log in</button>
</div>
</form>
</div>
<div class="row">
<div class="col-12">
<p class="text-secondary">
Don't have access to your authenticator device?
<br>
You can <a asp-action="LoginWithRecoveryCode" asp-route-returnUrl="@ViewData["ReturnUrl"]">log in with a recovery code</a>.
</p>
</div>
</div>

View File

@@ -7,22 +7,18 @@
<input type="hidden" asp-for="RememberMe"/> <input type="hidden" asp-for="RememberMe"/>
</form> </form>
<section class="pt-5"> <div class="row pt-5">
<div> <div class="col-lg-12 section-heading">
<div class="row"> <h2>FIDO2 Authentication</h2>
<div class="col-lg-12 section-heading"> <hr class="primary">
<h2>FIDO2 Authentication</h2> <div>
<hr class="primary"> <span id="spinner" class="fa fa-spinner fa-spin float-end ms-3 me-5 mt-1 fido-running" style="font-size:2.5em"></span>
<div> <p>Insert your security key into your computer's USB port. If it has a button, tap on it.</p>
<span id="spinner" class="fa fa-spinner fa-spin float-end ms-3 me-5 mt-1 fido-running" style="font-size:2.5em"></span>
<p>Insert your security key into your computer's USB port. If it has a button, tap on it.</p>
</div>
<p id="error-message" class="d-none alert alert-danger"></p>
<button id="btn-retry" class="btn btn-secondary d-none" type="button">Retry</button>
</div>
</div> </div>
<p id="error-message" class="d-none alert alert-danger"></p>
<button id="btn-retry" class="btn btn-secondary d-none" type="button">Retry</button>
</div> </div>
</section> </div>
<script> <script>
document.getElementById('btn-retry').addEventListener('click', () => window.location.reload()) document.getElementById('btn-retry').addEventListener('click', () => window.location.reload())

View File

@@ -3,33 +3,27 @@
ViewData["Title"] = "Recovery code verification"; ViewData["Title"] = "Recovery code verification";
} }
<div class="row">
<div class="col-lg-12 section-heading">
<section> <h2>@ViewData["Title"]</h2>
<div class="container"> <hr class="primary">
<div class="row"> <p>
<div class="col-lg-12 section-heading"> You have requested to login with a recovery code. This login will not be remembered until you provide
<h2>@ViewData["Title"]</h2> an authenticator app code at login or disable 2FA and login again.
<hr class="primary"> </p>
<p>
You have requested to login with a recovery code. This login will not be remembered until you provide
an authenticator app code at login or disable 2FA and login again.
</p>
</div>
</div>
<div class="row">
<form method="post" class="col-12">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="RecoveryCode" class="form-label"></label>
<input asp-for="RecoveryCode" class="form-control" autocomplete="off" />
<span asp-validation-for="RecoveryCode" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Log in</button>
</form>
</div>
</div> </div>
</section> </div>
<div class="row">
<form method="post" class="col-12">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="RecoveryCode" class="form-label"></label>
<input asp-for="RecoveryCode" class="form-control" autocomplete="off" />
<span asp-validation-for="RecoveryCode" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Log in</button>
</form>
</div>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />

View File

@@ -3,38 +3,34 @@
ViewData["Title"] = "Two-factor/U2F authentication"; ViewData["Title"] = "Two-factor/U2F authentication";
} }
<section> @if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null)
<div class="container"> {
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null) <div asp-validation-summary="ModelOnly" class="text-danger"></div>
{ }
<div asp-validation-summary="ModelOnly" class="text-danger"></div> else if (Model.LoginWith2FaViewModel == null && Model.LoginWithFido2ViewModel == null)
} {
else if (Model.LoginWith2FaViewModel == null && Model.LoginWithFido2ViewModel == null) <div class="row">
{ <div class="col-lg-12 section-heading">
<div class="row"> <h2 class="bg-danger">Both 2FA and U2F/FIDO2 Authentication Methods are not available. Please go to the https endpoint.</h2>
<div class="col-lg-12 section-heading"> <hr class="danger">
<h2 class="bg-danger">Both 2FA and U2F/FIDO2 Authentication Methods are not available. Please go to the https endpoint.</h2>
<hr class="danger">
</div>
</div>
}
<div class="row justify-content-center">
@if (Model.LoginWith2FaViewModel != null)
{
<div class="col-sm-12 col-md-6">
<partial name="LoginWith2fa" model="@Model.LoginWith2FaViewModel"/>
</div>
}
@if (Model.LoginWithFido2ViewModel != null)
{
<div class="col-sm-12 col-md-6">
<partial name="LoginWithFido2" model="@Model.LoginWithFido2ViewModel"/>
</div>
}
</div> </div>
</div> </div>
</section> }
<div class="row justify-content-center">
@if (Model.LoginWith2FaViewModel != null)
{
<div class="col-sm-12 col-md-6">
<partial name="LoginWith2fa" model="@Model.LoginWith2FaViewModel"/>
</div>
}
@if (Model.LoginWithFido2ViewModel != null)
{
<div class="col-sm-12 col-md-6">
<partial name="LoginWithFido2" model="@Model.LoginWithFido2ViewModel"/>
</div>
}
</div>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />

View File

@@ -7,35 +7,27 @@
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["Title"]</h2> <h2 class="mb-4">@ViewData["Title"]</h2>
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<form asp-action="CreateApp"> <form asp-action="CreateApp">
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group"> <div class="form-group">
<label asp-for="SelectedStore" class="form-label" data-required></label> <label asp-for="SelectedAppType" class="form-label" data-required></label>
<select asp-for="SelectedStore" asp-items="Model.Stores" class="form-select"></select> <select asp-for="SelectedAppType" asp-items="Model.AppTypes" class="form-select"></select>
</div>
<div class="form-group">
<label asp-for="SelectedAppType" class="form-label" data-required></label>
<select asp-for="SelectedAppType" asp-items="Model.AppTypes" class="form-select"></select>
</div>
<div class="form-group">
<label asp-for="AppName" class="form-label" data-required></label>
<input asp-for="AppName" class="form-control" required />
<span asp-validation-for="AppName" class="text-danger"></span>
</div>
<div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
<a asp-action="ListApps" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</form>
</div> </div>
</div> <div class="form-group">
<label asp-for="AppName" class="form-label" data-required></label>
<input asp-for="AppName" class="form-control" required />
<span asp-validation-for="AppName" class="text-danger"></span>
</div>
<div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
<a asp-action="ListApps" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</form>
</div> </div>
</section> </div>

View File

@@ -9,118 +9,117 @@
var sortByAsc = "Sort by ascending..."; var sortByAsc = "Sort by ascending...";
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-2"> <div class="d-sm-flex align-items-center justify-content-between mb-2">
<h2 class="mb-0"> <h2 class="mb-0">
@ViewData["PageTitle"] @ViewData["PageTitle"]
<small> <small>
<a href="https://docs.btcpayserver.org/Apps/" target="_blank" rel="noreferrer noopener"> <a href="https://docs.btcpayserver.org/Apps/" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span> <span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a> </a>
</small> </small>
</h2> </h2>
<a asp-action="CreateApp" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateNewApp"><span class="fa fa-plus"></span> Create a new app</a> <a asp-action="CreateApp" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateNewApp"><span class="fa fa-plus"></span> Create a new app</a>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
@if (Model.Apps.Any()) @if (Model.Apps.Any())
{
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName"
class="text-nowrap"
title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
Store
<span class="fa @(storeNameSortOrder == "asc" ? "fa-sort-alpha-desc" : storeNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(appNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppName"
class="text-nowrap"
title="@(appNameSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
Name
<span class="fa @(appNameSortOrder == "asc" ? "fa-sort-alpha-desc" : appNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(appTypeSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppType"
class="text-nowrap"
title="@(appTypeSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
App Type
<span class="fa @(appTypeSortOrder == "asc" ? "fa-sort-alpha-desc" : appTypeSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var app in Model.Apps)
{ {
<table class="table table-hover table-responsive-md"> <tr>
<thead> <td>
<tr> @if (app.IsOwner)
<th> {
<a <span><a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
asp-action="ListApps" }
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")" else
asp-route-sortOrderColumn="StoreName" {
class="text-nowrap" <span>@app.StoreName</span>
title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)" }
> </td>
Store <td>@app.AppName</td>
<span class="fa @(storeNameSortOrder == "asc" ? "fa-sort-alpha-desc" : storeNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" /> <td>
</a> @typeof(AppType).DisplayName(app.AppType)
</th> @if (app.AppType != AppType.Crowdfund.ToString())
<th> {
<a <span>-</span>
asp-action="ListApps" }
asp-route-sortOrder="@(appNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppName"
class="text-nowrap"
title="@(appNameSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
Name
<span class="fa @(appNameSortOrder == "asc" ? "fa-sort-alpha-desc" : appNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th>
<a
asp-action="ListApps"
asp-route-sortOrder="@(appTypeSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppType"
class="text-nowrap"
title="@(appTypeSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
App Type
<span class="fa @(appTypeSortOrder == "asc" ? "fa-sort-alpha-desc" : appTypeSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var app in Model.Apps)
{
<tr>
<td>
@if (app.IsOwner)
{
<span><a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
}
else
{
<span>@app.StoreName</span>
}
</td>
<td>@app.AppName</td>
<td>
@typeof(AppType).DisplayName(app.AppType)
@if (app.AppType != AppType.Crowdfund.ToString())
{
<span>-</span>
}
@app.ViewStyle @app.ViewStyle
</td> </td>
<td style="text-align:right"> <td class="text-end">
@if (app.IsOwner) @if (app.IsOwner)
{ {
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a> <a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
<span> - </span> <span> - </span>
} }
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id">View</a> <a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id">View</a>
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id" target="_blank" <a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id" target="_blank"
title="View in New Window"><span class="fa fa-external-link"></span></a> title="View in New Window"><span class="fa fa-external-link"></span></a>
<span> - </span> <span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@app.AppName</strong> and its settings will be permanently deleted from your store <strong>@app.StoreName</strong>." data-confirm-input="DELETE">Delete</a> <a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@app.AppName</strong> and its settings will be permanently deleted from your store <strong>@app.StoreName</strong>." data-confirm-input="DELETE">Delete</a>
</td> </td>
</tr> </tr>
}
</tbody>
</table>
} }
else </tbody>
{ </table>
<p class="text-secondary mt-3"> }
There are no apps yet. else
</p> {
} <p class="text-secondary mt-3">
</div> There are no apps yet.
</div> </p>
}
</div> </div>
</section> </div>
<partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" /> <partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" />

View File

@@ -3,6 +3,7 @@
@model UpdateCrowdfundViewModel @model UpdateCrowdfundViewModel
@{ @{
ViewData.SetActivePageAndTitle(AppsNavPages.Update, "Update Crowdfund", Model.StoreName); ViewData.SetActivePageAndTitle(AppsNavPages.Update, "Update Crowdfund", Model.StoreName);
ViewData.SetActiveId(Model.AppId);
} }
@section PageHeadContent { @section PageHeadContent {
@@ -15,242 +16,238 @@
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js" asp-append-version="true"></bundle> <bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js" asp-append-version="true"></bundle>
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2> <h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2>
<form method="post"> <form method="post">
<input type="hidden" asp-for="StoreId" /> <input type="hidden" asp-for="StoreId" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="form-group"> <div class="form-group">
<label asp-for="AppName" class="form-label" data-required></label> <label asp-for="AppName" class="form-label" data-required></label>
<input asp-for="AppName" class="form-control" required /> <input asp-for="AppName" class="form-control" required />
<span asp-validation-for="AppName" class="text-danger"></span> <span asp-validation-for="AppName" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="Title" class="form-label" data-required></label> <label asp-for="Title" class="form-label" data-required></label>
<input asp-for="Title" class="form-control" required /> <input asp-for="Title" class="form-control" required />
<span asp-validation-for="Title" class="text-danger"></span> <span asp-validation-for="Title" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="Tagline" class="form-label"></label> <label asp-for="Tagline" class="form-label"></label>
<input asp-for="Tagline" class="form-control" /> <input asp-for="Tagline" class="form-control" />
<span asp-validation-for="Tagline" class="text-danger"></span> <span asp-validation-for="Tagline" class="text-danger"></span>
</div> </div>
</div>
<div class="col-lg-9">
<div class="form-group">
<label asp-for="Description" class="form-label" data-required></label>
<textarea asp-for="Description" rows="20" cols="40" class="form-control richtext"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<label asp-for="TargetCurrency" class="form-label"></label>
<input asp-for="TargetCurrency" class="form-control" placeholder="Use store's default settings" />
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TargetAmount" class="form-label"></label>
<input asp-for="TargetAmount" class="form-control" />
<span asp-validation-for="TargetAmount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="form-label"></label>
<div class="input-group">
<input type="datetime-local" asp-for="StartDate"
value="@(Model.StartDate?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker"
placeholder="No start date has been set" />
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class="fa fa-times"></span>
</button>
</div> </div>
<div class="col-lg-9"> <span asp-validation-for="StartDate" class="text-danger"></span>
<div class="form-group"> </div>
<label asp-for="Description" class="form-label" data-required></label> <div class="form-group">
<textarea asp-for="Description" rows="20" cols="40" class="form-control richtext"></textarea> <label asp-for="EndDate" class="form-label"></label>
<span asp-validation-for="Description" class="text-danger"></span> <div class="input-group">
</div> <input type="datetime-local" asp-for="EndDate"
value="@(Model.EndDate?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker"
placeholder="No end date has been set" />
<button class="btn btn-secondary input-group-clear input-group-text" type="button" title="Clear">
<span class="fa fa-times"></span>
</button>
</div> </div>
<div class="col-lg-6"> <span asp-validation-for="EndDate" class="text-danger"></span>
<div class="form-group"> </div>
<label asp-for="TargetCurrency" class="form-label"></label> <div class="form-group mb-4">
<input asp-for="TargetCurrency" class="form-control" placeholder="Use store's default settings" /> <label asp-for="ResetEvery" class="form-label"></label>
<span asp-validation-for="TargetCurrency" class="text-danger"></span> <div class="input-group">
</div> <input type="number" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control">
<div class="form-group"> <select class="form-select" asp-for="ResetEvery">
<label asp-for="TargetAmount" class="form-label"></label> @foreach (var opt in Model.ResetEveryValues)
<input asp-for="TargetAmount" class="form-control" /> {
<span asp-validation-for="TargetAmount" class="text-danger"></span> <option value="@opt">@opt</option>
</div> }
<div class="form-group"> </select>
<label asp-for="StartDate" class="form-label"></label> </div>
<div class="input-group"> </div>
<input type="datetime-local" asp-for="StartDate" <div class="form-group mb-3">
value="@(Model.StartDate?.ToString("u", CultureInfo.InvariantCulture))" <div class="d-flex align-items-center mb-3">
class="form-control flatdtpicker" <input asp-for="Enabled" type="checkbox" class="btcpay-toggle me-2"/>
placeholder="No start date has been set" /> <label asp-for="Enabled" class="form-label mb-0"></label>
<button class="btn btn-secondary input-group-clear" type="button" title="Clear"> </div>
<span class="fa fa-times"></span> <span asp-validation-for="Enabled" class="text-danger"></span>
<div class="text-muted" hidden="@Model.Enabled">The crowdfund is only visible to you. To make it visible to anyone else, enable this.</div>
<div class="text-muted" hidden="@(!Model.Enabled)">The crowdfund is visible to anyone. To make it only visible to you, disable this.</div>
</div>
</div>
<div class="col-lg-12">
<partial name="TemplateEditor" model="@(nameof(Model.PerksTemplate), "Perks")" />
</div>
<div class="col-lg-6">
<div class="form-group">
<label asp-for="PerksTemplate" class="form-label"></label>
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Contributions</h4>
<div class="form-check mb-3">
<input asp-for="SortPerksByPopularity" type="checkbox" class="form-check-input" />
<label asp-for="SortPerksByPopularity" class="form-check-label"></label>
<span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="DisplayPerksRanking" type="checkbox" class="form-check-input" />
<label asp-for="DisplayPerksRanking" class="form-check-label"></label>
<span asp-validation-for="DisplayPerksRanking" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="DisplayPerksValue" type="checkbox" class="form-check-input" />
<label asp-for="DisplayPerksValue" class="form-check-label"></label>
<span asp-validation-for="DisplayPerksValue" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check-input" />
<label asp-for="EnforceTargetAmount" class="form-check-label"></label>
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Crowdfund Behavior</h4>
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-check">
<input asp-for="UseAllStoreInvoices" type="checkbox" class="form-check-input" />
<label asp-for="UseAllStoreInvoices" class="form-check-label"></label>
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Sound</h4>
<div class="form-group mb-3">
<div class="d-flex align-items-center mb-3">
<input asp-for="SoundsEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#SoundsEnabledSettings" aria-expanded="@Model.SoundsEnabled" aria-controls="SoundsEnabledSettings"/>
<label asp-for="SoundsEnabled" class="form-label mb-0"></label>
</div>
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
</div>
<div class="collapse @(Model.SoundsEnabled ? "show" : "")" id="SoundsEnabledSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="Sounds" class="form-label"></label>
<textarea asp-for="Sounds" class="form-control"></textarea>
<span asp-validation-for="Sounds" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-4">Animation</h4>
<div class="form-group mb-3">
<div class="d-flex align-items-center mb-3">
<input asp-for="AnimationsEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#AnimationsEnabledSettings" aria-expanded="@Model.AnimationsEnabled" aria-controls="AnimationsEnabledSettings"/>
<label asp-for="AnimationsEnabled" class="form-label mb-0"></label>
</div>
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
</div>
<div class="collapse @(Model.AnimationsEnabled ? "show" : "")" id="AnimationsEnabledSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="AnimationColors" class="form-label"></label>
<textarea asp-for="AnimationColors" class="form-control"></textarea>
<span asp-validation-for="AnimationColors" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-4">Discussion</h4>
<div class="form-group mb-3">
<div class="d-flex align-items-center mb-3">
<input asp-for="DisqusEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#DisqusEnabledSettings" aria-expanded="@Model.DisqusEnabled" aria-controls="DisqusEnabledSettings"/>
<label asp-for="DisqusEnabled" class="form-label mb-0"></label>
</div>
<span asp-validation-for="DisqusEnabled" class="text-danger"></span>
</div>
<div class="collapse @(Model.DisqusEnabled ? "show" : "")" id="DisqusEnabledSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="DisqusShortname" class="form-label"></label>
<input asp-for="DisqusShortname" class="form-control" />
<span asp-validation-for="DisqusShortname" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button> </button>
</div> </h2>
<span asp-validation-for="StartDate" class="text-danger"></span> <div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
</div> <div class="accordion-body">
<div class="form-group"> <div class="form-group">
<label asp-for="EndDate" class="form-label"></label> <label asp-for="MainImageUrl" class="form-label"></label>
<div class="input-group"> <input asp-for="MainImageUrl" class="form-control" />
<input type="datetime-local" asp-for="EndDate" <span asp-validation-for="MainImageUrl" class="text-danger"></span>
value="@(Model.EndDate?.ToString("u", CultureInfo.InvariantCulture))" </div>
class="form-control flatdtpicker" <div class="form-group">
placeholder="No end date has been set" /> <label asp-for="CustomCSSLink" class="form-label"></label>
<button class="btn btn-secondary input-group-clear input-group-text" type="button" title="Clear"> <a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-times"></span> <span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</button> </a>
</div> <input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="EndDate" class="text-danger"></span> <span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div> </div>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label asp-for="ResetEvery" class="form-label"></label> <label asp-for="EmbeddedCSS" class="form-label"></label>
<div class="input-group"> <textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<input type="number" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control"> <span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
<select class="form-select" asp-for="ResetEvery">
@foreach (var opt in Model.ResetEveryValues)
{
<option value="@opt">@opt</option>
}
</select>
</div>
</div>
<div class="form-group mb-3">
<div class="d-flex align-items-center mb-3">
<input asp-for="Enabled" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="Enabled" class="form-label mb-0"></label>
</div>
<span asp-validation-for="Enabled" class="text-danger"></span>
<div class="text-muted" hidden="@Model.Enabled">The crowdfund is only visible to you. To make it visible to anyone else, enable this.</div>
<div class="text-muted" hidden="@(!Model.Enabled)">The crowdfund is visible to anyone. To make it only visible to you, disable this.</div>
</div>
</div>
<div class="col-lg-12">
<partial name="TemplateEditor" model="@(nameof(Model.PerksTemplate), "Perks")" />
</div>
<div class="col-lg-6">
<div class="form-group">
<label asp-for="PerksTemplate" class="form-label"></label>
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Contributions</h4>
<div class="form-check mb-3">
<input asp-for="SortPerksByPopularity" type="checkbox" class="form-check-input" />
<label asp-for="SortPerksByPopularity" class="form-check-label"></label>
<span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="DisplayPerksRanking" type="checkbox" class="form-check-input" />
<label asp-for="DisplayPerksRanking" class="form-check-label"></label>
<span asp-validation-for="DisplayPerksRanking" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="DisplayPerksValue" type="checkbox" class="form-check-input" />
<label asp-for="DisplayPerksValue" class="form-check-label"></label>
<span asp-validation-for="DisplayPerksValue" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check-input" />
<label asp-for="EnforceTargetAmount" class="form-check-label"></label>
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Crowdfund Behavior</h4>
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-check">
<input asp-for="UseAllStoreInvoices" type="checkbox" class="form-check-input" />
<label asp-for="UseAllStoreInvoices" class="form-check-label"></label>
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Sound</h4>
<div class="form-group mb-3">
<div class="d-flex align-items-center mb-3">
<input asp-for="SoundsEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#SoundsEnabledSettings" aria-expanded="@Model.SoundsEnabled" aria-controls="SoundsEnabledSettings"/>
<label asp-for="SoundsEnabled" class="form-label mb-0"></label>
</div>
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
</div>
<div class="collapse @(Model.SoundsEnabled ? "show" : "")" id="SoundsEnabledSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="Sounds" class="form-label"></label>
<textarea asp-for="Sounds" class="form-control"></textarea>
<span asp-validation-for="Sounds" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-4">Animation</h4>
<div class="form-group mb-3">
<div class="d-flex align-items-center mb-3">
<input asp-for="AnimationsEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#AnimationsEnabledSettings" aria-expanded="@Model.AnimationsEnabled" aria-controls="AnimationsEnabledSettings"/>
<label asp-for="AnimationsEnabled" class="form-label mb-0"></label>
</div>
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
</div>
<div class="collapse @(Model.AnimationsEnabled ? "show" : "")" id="AnimationsEnabledSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="AnimationColors" class="form-label"></label>
<textarea asp-for="AnimationColors" class="form-control"></textarea>
<span asp-validation-for="AnimationColors" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-4">Discussion</h4>
<div class="form-group mb-3">
<div class="d-flex align-items-center mb-3">
<input asp-for="DisqusEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#DisqusEnabledSettings" aria-expanded="@Model.DisqusEnabled" aria-controls="DisqusEnabledSettings"/>
<label asp-for="DisqusEnabled" class="form-label mb-0"></label>
</div>
<span asp-validation-for="DisqusEnabled" class="text-danger"></span>
</div>
<div class="collapse @(Model.DisqusEnabled ? "show" : "")" id="DisqusEnabledSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="DisqusShortname" class="form-label"></label>
<input asp-for="DisqusShortname" class="form-control" />
<span asp-validation-for="DisqusShortname" class="text-danger"></span>
</div>
</div>
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="MainImageUrl" class="form-label"></label>
<input asp-for="MainImageUrl" class="form-control" />
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group mb-4">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-9 mt-2 mb-5">
<div class="d-grid gap-3 d-md-block">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button>
<div class="btn-group me-md-2">
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
@if (Model.ModelWithMinimumData)
{
<div class="btn-group">
<a class="btn btn-secondary flex-grow-1" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId" id="ViewApp">View Crowdfund</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId"
target="viewapp_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
}
</div>
</div>
</div> </div>
</form> </div>
<div class="col-lg-9 mt-2 mb-5">
<div class="d-grid gap-3 d-md-block">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button>
<div class="btn-group me-md-2">
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="Invoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="Invoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
@if (Model.ModelWithMinimumData)
{
<div class="btn-group">
<a class="btn btn-secondary flex-grow-1" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId" id="ViewApp">View Crowdfund</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId"
target="viewapp_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
}
</div>
</div>
</div> </div>
</section> </form>

View File

@@ -3,257 +3,255 @@
@model UpdatePointOfSaleViewModel @model UpdatePointOfSaleViewModel
@{ @{
ViewData.SetActivePageAndTitle(AppsNavPages.Update, "Update Point of Sale", Model.StoreName); ViewData.SetActivePageAndTitle(AppsNavPages.Update, "Update Point of Sale", Model.StoreName);
ViewData.SetActiveId(Model.Id);
} }
<section>
<div class="container">
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2> <partial name="_StatusMessage" />
<form method="post"> <h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2>
<input type="hidden" asp-for="StoreId" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row"> <form method="post">
<div class="col-lg-6"> <input type="hidden" asp-for="StoreId" />
<div class="form-group"> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<label asp-for="AppName" class="form-label" data-required></label>
<input asp-for="AppName" class="form-control" required /> <div class="row">
<span asp-validation-for="AppName" class="text-danger"></span> <div class="col-lg-6">
</div> <div class="form-group">
<div class="form-group"> <label asp-for="AppName" class="form-label" data-required></label>
<label asp-for="Title" class="form-label" data-required></label> <input asp-for="AppName" class="form-control" required />
<input asp-for="Title" class="form-control" required /> <span asp-validation-for="AppName" class="text-danger"></span>
<span asp-validation-for="Title" class="text-danger"></span> </div>
</div> <div class="form-group">
<div class="form-group"> <label asp-for="Title" class="form-label" data-required></label>
<label asp-for="Currency" class="form-label"></label> <input asp-for="Title" class="form-control" required />
<input asp-for="Currency" class="form-control" placeholder="Use store's default settings" /> <span asp-validation-for="Title" class="text-danger"></span>
<span asp-validation-for="Currency" class="text-danger"></span> </div>
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control" placeholder="Use store's default settings" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>
<div class="col-lg-9">
<div class="form-group">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" rows="10" cols="40" class="form-control richtext"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
<div class="col-lg-12">
<partial name="TemplateEditor" model="@(nameof(Model.Template), "Products")" />
<div class="form-group">
<label asp-for="Template" class="form-label"></label>
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
</div>
<div class="col-lg-6">
<h4 class="mt-5 mb-4">Appearance</h4>
<div class="form-group">
<label asp-for="DefaultView" class="form-label" data-required></label>
<select asp-for="DefaultView" asp-items="@Html.GetEnumSelectList<PosViewType>()" class="form-select" required></select>
<span asp-validation-for="DefaultView" class="text-danger"></span>
<div class="mt-2">
<span class="text-secondary">Choose the point of sale style for your customers.</span>
</div>
</div>
<div class="form-group" id="button-price-text">
<label asp-for="ButtonText" class="form-label" data-required></label>
<input asp-for="ButtonText" class="form-control" required />
<span asp-validation-for="ButtonText" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RequiresRefundEmail" class="form-label"></label>
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select" required></select>
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
</div>
<section id="discounts" class="p-0">
<h4 class="mt-5 mb-4">Discounts</h4>
<div class="form-check mb-4">
<input asp-for="ShowDiscount" type="checkbox" class="form-check-input" />
<label asp-for="ShowDiscount" class="form-check-label"></label>
<span asp-validation-for="ShowDiscount" class="text-danger"></span>
<div class="mt-2">
<span class="text-secondary">Not recommended for customer self-checkout.</span>
</div> </div>
</div> </div>
<div class="col-lg-9"> </section>
<div class="form-group"> <section id="custom-payments" class="p-0">
<label asp-for="Description" class="form-label"></label> <h4 class="mt-5 mb-4">Custom Payments</h4>
<textarea asp-for="Description" rows="10" cols="40" class="form-control richtext"></textarea> <div class="form-group mb-4 d-flex align-items-center">
<span asp-validation-for="Description" class="text-danger"></span> <input asp-for="ShowCustomAmount" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomAmountSettings" aria-expanded="@Model.ShowCustomAmount" aria-controls="CustomAmountSettings"/>
<label asp-for="ShowCustomAmount" class="form-label mb-0"></label>
<span asp-validation-for="ShowCustomAmount" class="text-danger"></span>
</div>
<div class="collapse @(Model.ShowCustomAmount ? "show" : "")" id="CustomAmountSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="CustomButtonText" class="form-label" data-required></label>
<input asp-for="CustomButtonText" class="form-control" required />
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
</div> </div>
</div> </div>
<div class="col-lg-12"> </section>
<partial name="TemplateEditor" model="@(nameof(Model.Template), "Products")" /> <section id="tips" class="p-0">
<h4 class="mt-5 mb-4">Tips</h4>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="EnableTips" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomTipsSettings" aria-expanded="@Model.EnableTips" aria-controls="CustomTipsSettings" />
<label asp-for="EnableTips" class="form-label mb-0"></label>
<span asp-validation-for="EnableTips" class="text-danger"></span>
</div>
<div class="collapse @(Model.EnableTips ? "show" : "")" id="CustomTipsSettings">
<div class="form-group"> <div class="form-group">
<label asp-for="Template" class="form-label"></label> <label asp-for="CustomTipText" class="form-label" data-required></label>
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea> <input asp-for="CustomTipText" class="form-control" required />
<span asp-validation-for="Template" class="text-danger"></span> <span asp-validation-for="CustomTipText" class="text-danger"></span>
</div>
<div class="form-group mb-0 pb-3">
<label asp-for="CustomTipPercentages" class="form-label"></label>
<input asp-for="CustomTipPercentages" class="form-control" />
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
</div> </div>
</div> </div>
<div class="col-lg-6"> </section>
<h4 class="mt-5 mb-4">Appearance</h4> </div>
<div class="form-group"> <div class="col-lg-9">
<label asp-for="DefaultView" class="form-label" data-required></label> <h4 class="mt-5 mb-2">Additional Options</h4>
<select asp-for="DefaultView" asp-items="@Html.GetEnumSelectList<PosViewType>()" class="form-select" required></select> <div class="form-group">
<span asp-validation-for="DefaultView" class="text-danger"></span> <div class="accordion" id="additional">
<div class="mt-2"> <div class="accordion-item">
<span class="text-secondary">Choose the point of sale style for your customers.</span> <h2 class="accordion-header" id="additional-embed-payment-button-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-payment-button" aria-expanded="false" aria-controls="additional-embed-payment-button">
Embed a payment button linking to POS item
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-embed-payment-button" class="accordion-collapse collapse" aria-labelledby="additional-embed-payment-button-header">
<div class="accordion-body">
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
</div>
</div> </div>
</div> </div>
<div class="form-group" id="button-price-text"> <div class="accordion-item">
<label asp-for="ButtonText" class="form-label" data-required></label> <h2 class="accordion-header" id="additional-embed-pos-iframe-header">
<input asp-for="ButtonText" class="form-control" required /> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-pos-iframe" aria-expanded="false" aria-controls="additional-embed-pos-iframe">
<span asp-validation-for="ButtonText" class="text-danger"></span> Embed Point of Sale via Iframe
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-embed-pos-iframe" class="accordion-collapse collapse" aria-labelledby="additional-embed-pos-iframe-header">
<div class="accordion-body">
You can embed this POS via an iframe.
@{
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "AppsPublic", new { appId = Model.Id }, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>";
}
<pre><code class="html">@iframe</code></pre>
</div>
</div>
</div> </div>
<div class="form-group"> <div class="accordion-item">
<label asp-for="RequiresRefundEmail" class="form-label"></label> <h2 class="accordion-header" id="additional-redirect-header">
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select" required></select> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-redirect" aria-expanded="false" aria-controls="additional-redirect">
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span> Redirects
</div> <vc:icon symbol="caret-down" />
<section id="discounts" class="p-0"> </button>
<h4 class="mt-5 mb-4">Discounts</h4> </h2>
<div class="form-check mb-4"> <div id="additional-redirect" class="accordion-collapse collapse" aria-labelledby="additional-redirect-header">
<input asp-for="ShowDiscount" type="checkbox" class="form-check-input" /> <div class="accordion-body">
<label asp-for="ShowDiscount" class="form-check-label"></label> <div class="form-group">
<span asp-validation-for="ShowDiscount" class="text-danger"></span> <label asp-for="RedirectUrl" class="form-label"></label>
<div class="mt-2"> <input asp-for="RedirectUrl" class="form-control" />
<span class="text-secondary">Not recommended for customer self-checkout.</span> <span asp-validation-for="RedirectUrl" class="text-danger"></span>
</div>
</div>
</section>
<section id="custom-payments" class="p-0">
<h4 class="mt-5 mb-4">Custom Payments</h4>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="ShowCustomAmount" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomAmountSettings" aria-expanded="@Model.ShowCustomAmount" aria-controls="CustomAmountSettings"/>
<label asp-for="ShowCustomAmount" class="form-label mb-0"></label>
<span asp-validation-for="ShowCustomAmount" class="text-danger"></span>
</div>
<div class="collapse @(Model.ShowCustomAmount ? "show" : "")" id="CustomAmountSettings">
<div class="form-group mb-0 pb-3">
<label asp-for="CustomButtonText" class="form-label" data-required></label>
<input asp-for="CustomButtonText" class="form-control" required />
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
</div>
</div>
</section>
<section id="tips" class="p-0">
<h4 class="mt-5 mb-4">Tips</h4>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="EnableTips" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#CustomTipsSettings" aria-expanded="@Model.EnableTips" aria-controls="CustomTipsSettings" />
<label asp-for="EnableTips" class="form-label mb-0"></label>
<span asp-validation-for="EnableTips" class="text-danger"></span>
</div>
<div class="collapse @(Model.EnableTips ? "show" : "")" id="CustomTipsSettings">
<div class="form-group">
<label asp-for="CustomTipText" class="form-label" data-required></label>
<input asp-for="CustomTipText" class="form-control" required />
<span asp-validation-for="CustomTipText" class="text-danger"></span>
</div>
<div class="form-group mb-0 pb-3">
<label asp-for="CustomTipPercentages" class="form-label"></label>
<input asp-for="CustomTipPercentages" class="form-control" />
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
</div>
</div>
</section>
</div>
<div class="col-lg-9">
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-embed-payment-button-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-payment-button" aria-expanded="false" aria-controls="additional-embed-payment-button">
Embed a payment button linking to POS item
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-embed-payment-button" class="accordion-collapse collapse" aria-labelledby="additional-embed-payment-button-header">
<div class="accordion-body">
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
</div>
</div> </div>
</div> <div class="form-group">
<div class="accordion-item"> <label asp-for="RedirectAutomatically" class="form-label"></label>
<h2 class="accordion-header" id="additional-embed-pos-iframe-header"> <select asp-for="RedirectAutomatically" asp-items="Model.RedirectAutomaticallySelectList" class="form-select"></select>
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-pos-iframe" aria-expanded="false" aria-controls="additional-embed-pos-iframe"> <span asp-validation-for="RedirectAutomatically" class="text-danger"></span>
Embed Point of Sale via Iframe
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-embed-pos-iframe" class="accordion-collapse collapse" aria-labelledby="additional-embed-pos-iframe-header">
<div class="accordion-body">
You can embed this POS via an iframe.
@{
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "AppsPublic", new { appId = Model.Id }, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>";
}
<pre><code class="html">@iframe</code></pre>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="additional-redirect-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-redirect" aria-expanded="false" aria-controls="additional-redirect">
Redirects
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-redirect" class="accordion-collapse collapse" aria-labelledby="additional-redirect-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="RedirectUrl" class="form-label"></label>
<input asp-for="RedirectUrl" class="form-control" />
<span asp-validation-for="RedirectUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RedirectAutomatically" class="form-label"></label>
<select asp-for="RedirectAutomatically" asp-items="Model.RedirectAutomaticallySelectList" class="form-select"></select>
<span asp-validation-for="RedirectAutomatically" class="text-danger"></span>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="additional-notification-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notification" aria-expanded="false" aria-controls="additional-notification">
Notification URL Callbacks
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-notification" class="accordion-collapse collapse" aria-labelledby="additional-notification-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
<li>You can then ship your order</li>
</ul>
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="accordion-item">
<div class="col-lg-9 mt-2 mb-5"> <h2 class="accordion-header" id="additional-notification-header">
<div class="d-grid gap-3 d-md-block"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notification" aria-expanded="false" aria-controls="additional-notification">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button> Notification URL Callbacks
<div class="btn-group me-md-2"> <vc:icon symbol="caret-down" />
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a> </button>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm" </h2>
target="viewinvoices_@Model.Id"><span class="fa fa-external-link"></span></a> <div id="additional-notification" class="accordion-collapse collapse" aria-labelledby="additional-notification-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
<li>You can then ship your order</li>
</ul>
</p>
</div>
</div> </div>
<div class="btn-group"> </div>
<a class="btn btn-secondary flex-grow-1" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id" id="ViewApp">View Point of Sale</a> <div class="accordion-item">
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id" <h2 class="accordion-header" id="additional-custom-css-header">
target="viewapp_@Model.Id"><span class="fa fa-external-link"></span></a> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </div>
<div class="col-lg-9 mt-2 mb-5">
<div class="d-grid gap-3 d-md-block">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button>
<div class="btn-group me-md-2">
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.Id"><span class="fa fa-external-link"></span></a>
</div>
<div class="btn-group">
<a class="btn btn-secondary flex-grow-1" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id" id="ViewApp">View Point of Sale</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ViewPointOfSale" asp-controller="AppsPublic" asp-route-appId="@Model.Id"
target="viewapp_@Model.Id"><span class="fa fa-external-link"></span></a>
</div>
</div>
</div>
</div> </div>
</section> </form>
@section PageHeadContent { @section PageHeadContent {
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css" asp-append-version="true"> <link rel="stylesheet" href="~/vendor/highlightjs/default.min.css" asp-append-version="true">

View File

@@ -261,7 +261,7 @@
<div class="me-3" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</div> <div class="me-3" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</div>
@if (!theme.CustomTheme) @if (!theme.CustomTheme)
{ {
<vc:theme-switch css-class="text-muted me-3" responsive="none" /> <vc:theme-switch css-class="text-muted me-3" />
} }
<div class="form-check me-3 my-0 only-for-js" v-if="srvModel.animationsEnabled || animation"> <div class="form-check me-3 my-0 only-for-js" v-if="srvModel.animationsEnabled || animation">
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation"> <input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">

View File

@@ -1,103 +1,84 @@
@{ @{
ViewData["Title"] = "Home Page"; ViewData["Title"] = "Home Page";
ViewBag.AlwaysShrinkNavBar = false;
} }
<header class="masthead"> <div class="container">
<div class="header-content"> <div class="row">
<div class="header-content-inner text-white"> <div class="col-lg-12 text-center">
<h1>Welcome to BTCPay Server</h1> <div>
<p class="fw-semibold">BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p> <h1>Welcome to BTCPay Server</h1>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">Official website</a> <p class="lead" style="max-width:30em;margin:1.5em auto">BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p>
<a href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener" class="btn btn-primary rounded-pill fs-5">Official website</a>
</div>
<hr class="primary my-5">
<h2 class="mb-4">A Payment Server for Bitcoin</h2>
</div> </div>
</div> </div>
</header> <div class="row">
<div class="col-lg-4 col-md-6 text-center">
<section id="services"> <div class="py-4 service-box">
<div class="container"> <img src="~/img/lock-logo.png" class="mb-2" alt="" asp-append-version="true" />
<div class="row"> <h3>Secure</h3>
<div class="col-lg-12 text-center"> <p class="text-muted mb-0">The payment server does not need to know your private keys, so your money can't be stolen.</p>
<h2>A Payment Server for Bitcoin</h2> </div>
<hr class="primary"> </div>
<div class="col-lg-4 col-md-6 text-center">
<div class="py-4 service-box">
<img src="~/img/qr-logo.png" class="mb-2" alt="" asp-append-version="true" />
<h3>Easy</h3>
<p class="text-muted mb-0">A user-friendly Bitcoin checkout page for your customers.</p>
</div>
</div>
<div class="col-lg-4 col-md-6 text-center">
<div class="py-4 service-box">
<img src="~/img/money-logo.png" class="mb-2" alt="" asp-append-version="true" />
<h3>Visibility</h3>
<p class="text-muted mb-0">Manage, generate reports, and search for your invoices easily.</p>
</div> </div>
</div> </div>
</div> </div>
<div class="container"> <div class="row text-center my-5">
<div class="row"> <h2 class="mb-4">Video tutorials</h2>
<div class="col-lg-4 col-md-6 text-center"> <div class="text-center">
<div class="service-box"> <a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank" rel="noreferrer noopener">
<img src="~/img/lock-logo.png" alt="" asp-append-version="true" /> <img src="~/img/youtube.png" alt="Video tutorials" class="img-fluid" asp-append-version="true" />
<h3>Secure</h3> </a>
<p class="text-muted">The payment server does not need to know your private keys, so your money can't be stolen.</p>
</div>
</div>
<div class="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/qr-logo.png" alt="" asp-append-version="true" />
<h3>Easy</h3>
<p class="text-muted">A user-friendly Bitcoin checkout page for your customers.</p>
</div>
</div>
<div class="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/money-logo.png" alt="" asp-append-version="true" />
<h3>Visibility</h3>
<p class="text-muted">Manage, generate reports, and search for your invoices easily.</p>
</div>
</div>
</div> </div>
</div> </div>
</section> <div class="row">
<div class="col-lg-8 mx-auto text-center">
<div class="call-to-action bg-tile"> <h2>Donate</h2>
<div class="container text-center"> <hr class="primary">
<h2>Video tutorials</h2> <p>
<div class="row"> BTCPay Server is proudly free and open-source, built and maintained<br /> by a world-wide community of passionate contributors.<br />
<div class="col-lg-12 text-center"> Support us by donating through BTCPay Server Foundation<br /> or by directly sending donation to a specific contributor.
<a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank" rel="noreferrer noopener"> </p>
<img src="~/img/youtube.png" class="img-fluid" asp-append-version="true" /> <p>
<a href="https://btcpayserver.org/donate/" rel="noreferrer noopener">
<svg viewBox="0 0 208 55" width="208" xmlns="http://www.w3.org/2000/svg">
<path d="m208 48c0 3.866-3.135 7-7 7h-194c-3.866 0-7-3.134-7-7v-41c0-3.866 3.134-7 7-7h194c3.865 0 7 3.134 7 7z" fill="var(--btcpay-bg-cta, #0f3723)"/>
<g fill="#fff">
<path d="m125.169 20.648c.519 0 1.017.051 1.495.153.479.103.898.276 1.261.522s.654.57.873.972c.22.402.329.903.329 1.503 0 .336-.052.664-.156.981-.104.318-.25.606-.439.864-.19.258-.415.477-.674.657-.26.18-.551.306-.873.378v.036c.793.108 1.428.447 1.899 1.017.474.57.708 1.275.708 2.115 0 .204-.017.436-.052.693s-.104.521-.207.792c-.104.271-.254.537-.45.801-.195.265-.458.495-.785.693-.328.198-.731.36-1.209.485-.479.127-1.052.189-1.72.189h-4.368v-12.851zm0 5.634c.472 0 .881-.057 1.228-.171.346-.114.633-.27.863-.468s.403-.429.518-.693.174-.546.174-.846c0-1.608-.928-2.412-2.782-2.412h-3.192v4.59zm0 6.174c.438 0 .853-.039 1.244-.117s.737-.219 1.037-.423c.299-.204.536-.477.708-.818.173-.343.259-.771.259-1.287 0-.828-.278-1.449-.838-1.863s-1.362-.621-2.41-.621h-3.192v5.13h3.192z"/>
<path d="m137.534 20.648v1.044h-3.167v11.808h-1.176v-11.808h-3.15v-1.044z"/>
<path d="m146.747 23.301c-.231-.381-.513-.7-.845-.958s-.697-.454-1.096-.589c-.4-.135-.82-.202-1.259-.202-.799 0-1.485.158-2.06.475-.574.316-1.045.736-1.41 1.257-.366.522-.637 1.111-.811 1.768-.176.657-.263 1.332-.263 2.023 0 .68.087 1.351.263 2.013.174.663.443 1.255.811 1.777.365.521.835.941 1.41 1.258.574.316 1.261.475 2.06.475.563 0 1.069-.104 1.521-.315.451-.212.838-.499 1.166-.862.326-.363.591-.789.793-1.275.203-.485.332-1.011.388-1.574h1.147c-.078.774-.252 1.473-.522 2.094-.271.622-.619 1.148-1.047 1.583-.427.435-.931.769-1.511 1.002s-1.225.353-1.934.353c-.945 0-1.775-.18-2.49-.537-.716-.358-1.31-.836-1.781-1.435-.473-.599-.828-1.29-1.064-2.075s-.354-1.613-.354-2.479c0-.868.117-1.694.354-2.48.236-.785.592-1.479 1.064-2.084.473-.604 1.065-1.084 1.781-1.442.715-.357 1.545-.537 2.49-.537.574 0 1.135.088 1.68.264s1.041.437 1.486.783c.444.346.816.771 1.114 1.275s.487 1.085.565 1.741h-1.147c-.101-.485-.267-.916-.499-1.297z"/>
<path d="m154.704 20.648c.553 0 1.054.081 1.503.243s.835.402 1.158.72c.322.318.57.705.742 1.161.174.456.26.972.26 1.548s-.086 1.943-.26 2.399c-.172.456-.42.843-.742 1.161s-.708.558-1.158.72-.95.243-1.503.243h-2.884v4.657h-1.176v-12.852zm-.259 7.151c.818 0 1.479-.216 1.986-.648.508-.432.762-1.943.762-2.831s-.254-1.548-.762-1.98c-.507-.432-1.168-.648-1.986-.648h-2.625v6.107z"/>
<path d="m164.361 20.648 4.84 12.852h-1.262l-1.504-3.996h-5.615l-1.486 3.996h-1.244l4.959-12.852zm1.677 7.812-2.384-6.588-2.473 6.588z"/>
<path d="m167.398 20.648h1.381l4.06 6.516 4.043-6.516h1.399l-4.872 7.56v5.292h-1.177v-5.292z"/>
<path d="m27.005 22.02c.782-.118 1.711-.207 2.729-.207 1.844 0 3.158.428 4.028 1.24.885.812 1.402 1.962 1.402 3.571 0 1.623-.502 2.951-1.432 3.866-.929.93-2.464 1.432-4.397 1.432-.915 0-1.682-.045-2.331-.118v-9.784zm1.284 8.793c.324.06.796.074 1.298.074 2.745 0 4.235-1.534 4.235-4.221.015-2.346-1.313-3.836-4.028-3.836-.664 0-1.166.059-1.505.133z"/>
<path d="m43.312 28.202c0 2.642-1.83 3.792-3.556 3.792-1.933 0-3.423-1.416-3.423-3.674 0-2.391 1.564-3.792 3.542-3.792 2.05 0 3.437 1.49 3.437 3.674zm-5.666.073c0 1.564.9 2.745 2.169 2.745 1.239 0 2.169-1.166 2.169-2.774 0-1.21-.605-2.745-2.14-2.745-1.533 0-2.198 1.417-2.198 2.774z"/>
<path d="m44.953 26.623c0-.738-.015-1.343-.059-1.933h1.151l.074 1.181h.03c.354-.679 1.18-1.343 2.361-1.343.988 0 2.523.59 2.523 3.04v4.265h-1.298v-4.117c0-1.151-.428-2.11-1.653-2.11-.855 0-1.52.605-1.741 1.328-.059.163-.089.384-.089.605v4.294h-1.298v-5.21z"/>
<path d="m57.099 31.832-.104-.9h-.044c-.398.561-1.166 1.062-2.184 1.062-1.446 0-2.184-1.018-2.184-2.051 0-1.727 1.534-2.671 4.293-2.656v-.148c0-.59-.162-1.652-1.623-1.652-.664 0-1.357.207-1.859.531l-.294-.856c.59-.384 1.446-.634 2.346-.634 2.184 0 2.715 1.49 2.715 2.921v2.671c0 .62.03 1.225.118 1.712zm-.192-3.644c-1.417-.029-3.025.221-3.025 1.608 0 .841.561 1.239 1.225 1.239.93 0 1.52-.59 1.727-1.195.044-.133.074-.28.074-.413v-1.239z"/>
<path d="m61.764 22.639v2.051h1.859v.989h-1.859v3.852c0 .886.251 1.387.974 1.387.339 0 .59-.044.752-.088l.059.974c-.251.104-.649.177-1.151.177-.605 0-1.092-.191-1.402-.546-.369-.384-.502-1.018-.502-1.859v-3.896h-1.106v-.99h1.106v-1.711z"/>
<path d="m65.72 28.497c.029 1.756 1.151 2.479 2.449 2.479.93 0 1.49-.163 1.977-.369l.222.93c-.458.206-1.24.442-2.376.442-2.198 0-3.512-1.446-3.512-3.601s1.269-3.851 3.35-3.851c2.332 0 2.951 2.051 2.951 3.364 0 .266-.03.472-.044.605h-5.017zm3.806-.93c.015-.826-.339-2.11-1.8-2.11-1.313 0-1.889 1.209-1.992 2.11z"/>
<path d="m77.084 22.639v2.051h1.859v.989h-1.859v3.852c0 .886.251 1.387.974 1.387.339 0 .59-.044.752-.088l.059.974c-.251.104-.649.177-1.151.177-.605 0-1.092-.191-1.402-.546-.369-.384-.502-1.018-.502-1.859v-3.896h-1.106v-.99h1.106v-1.711z"/>
<path d="m86.78 28.202c0 2.642-1.83 3.792-3.556 3.792-1.933 0-3.423-1.416-3.423-3.674 0-2.391 1.564-3.792 3.542-3.792 2.05 0 3.437 1.49 3.437 3.674zm-5.666.073c0 1.564.9 2.745 2.169 2.745 1.239 0 2.169-1.166 2.169-2.774 0-1.21-.605-2.745-2.14-2.745-1.534 0-2.198 1.417-2.198 2.774z"/>
</g>
<path d="m98.329 22.548v-8.32l10.112 4.812-7.447 5.435 3.511 2.537 8.67-6.263c.888-.685 1.47-1.238 1.421-2.137-.07-1.287-.923-1.772-1.695-2.081l-15.397-7.293s-1.7-.78-2.801.482c-.384.44-.453 1.765-.453 1.765v31.58h.029c-.036-1.295 1.166-1.9 1.166-1.9l2.884-1.372v-8.321z" fill="#cdd932"/>
<path d="m104.505 27.01-6.176-4.462v8.929z" fill="#1d7a44"/>
<path d="m113.176 33.271-8.67-6.262-3.511 2.536 7.446 5.435-10.111 4.813-2.884 1.372s-1.202.604-1.166 1.901c.009.324.093.688.294 1.101.745 1.521 2.93.615 2.93.615l15.398-7.292c.771-.309 1.625-.793 1.693-2.081.05-.899-.533-1.452-1.419-2.138z" fill="#51b13e"/>
</svg>
</a> </a>
</div> </p>
</div> </div>
</div> </div>
</div> </div>
<section class="mb-5">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<h2>Donate</h2>
<hr class="primary">
<p>
BTCPay Server is proudly free and open-source, built and maintained<br /> by a world-wide community of passionate contributors.<br />
Support us by donating through BTCPay Server Foundation<br /> or by directly sending donation to a specific contributor.
</p>
<p>
<a href="https://btcpayserver.org/donate/" rel="noreferrer noopener">
<svg viewBox="0 0 208 55" width="208" xmlns="http://www.w3.org/2000/svg">
<path d="m208 48c0 3.866-3.135 7-7 7h-194c-3.866 0-7-3.134-7-7v-41c0-3.866 3.134-7 7-7h194c3.865 0 7 3.134 7 7z" fill="var(--btcpay-bg-cta, #0f3723)"/>
<g fill="#fff">
<path d="m125.169 20.648c.519 0 1.017.051 1.495.153.479.103.898.276 1.261.522s.654.57.873.972c.22.402.329.903.329 1.503 0 .336-.052.664-.156.981-.104.318-.25.606-.439.864-.19.258-.415.477-.674.657-.26.18-.551.306-.873.378v.036c.793.108 1.428.447 1.899 1.017.474.57.708 1.275.708 2.115 0 .204-.017.436-.052.693s-.104.521-.207.792c-.104.271-.254.537-.45.801-.195.265-.458.495-.785.693-.328.198-.731.36-1.209.485-.479.127-1.052.189-1.72.189h-4.368v-12.851zm0 5.634c.472 0 .881-.057 1.228-.171.346-.114.633-.27.863-.468s.403-.429.518-.693.174-.546.174-.846c0-1.608-.928-2.412-2.782-2.412h-3.192v4.59zm0 6.174c.438 0 .853-.039 1.244-.117s.737-.219 1.037-.423c.299-.204.536-.477.708-.818.173-.343.259-.771.259-1.287 0-.828-.278-1.449-.838-1.863s-1.362-.621-2.41-.621h-3.192v5.13h3.192z"/>
<path d="m137.534 20.648v1.044h-3.167v11.808h-1.176v-11.808h-3.15v-1.044z"/>
<path d="m146.747 23.301c-.231-.381-.513-.7-.845-.958s-.697-.454-1.096-.589c-.4-.135-.82-.202-1.259-.202-.799 0-1.485.158-2.06.475-.574.316-1.045.736-1.41 1.257-.366.522-.637 1.111-.811 1.768-.176.657-.263 1.332-.263 2.023 0 .68.087 1.351.263 2.013.174.663.443 1.255.811 1.777.365.521.835.941 1.41 1.258.574.316 1.261.475 2.06.475.563 0 1.069-.104 1.521-.315.451-.212.838-.499 1.166-.862.326-.363.591-.789.793-1.275.203-.485.332-1.011.388-1.574h1.147c-.078.774-.252 1.473-.522 2.094-.271.622-.619 1.148-1.047 1.583-.427.435-.931.769-1.511 1.002s-1.225.353-1.934.353c-.945 0-1.775-.18-2.49-.537-.716-.358-1.31-.836-1.781-1.435-.473-.599-.828-1.29-1.064-2.075s-.354-1.613-.354-2.479c0-.868.117-1.694.354-2.48.236-.785.592-1.479 1.064-2.084.473-.604 1.065-1.084 1.781-1.442.715-.357 1.545-.537 2.49-.537.574 0 1.135.088 1.68.264s1.041.437 1.486.783c.444.346.816.771 1.114 1.275s.487 1.085.565 1.741h-1.147c-.101-.485-.267-.916-.499-1.297z"/>
<path d="m154.704 20.648c.553 0 1.054.081 1.503.243s.835.402 1.158.72c.322.318.57.705.742 1.161.174.456.26.972.26 1.548s-.086 1.943-.26 2.399c-.172.456-.42.843-.742 1.161s-.708.558-1.158.72-.95.243-1.503.243h-2.884v4.657h-1.176v-12.852zm-.259 7.151c.818 0 1.479-.216 1.986-.648.508-.432.762-1.943.762-2.831s-.254-1.548-.762-1.98c-.507-.432-1.168-.648-1.986-.648h-2.625v6.107z"/>
<path d="m164.361 20.648 4.84 12.852h-1.262l-1.504-3.996h-5.615l-1.486 3.996h-1.244l4.959-12.852zm1.677 7.812-2.384-6.588-2.473 6.588z"/>
<path d="m167.398 20.648h1.381l4.06 6.516 4.043-6.516h1.399l-4.872 7.56v5.292h-1.177v-5.292z"/>
<path d="m27.005 22.02c.782-.118 1.711-.207 2.729-.207 1.844 0 3.158.428 4.028 1.24.885.812 1.402 1.962 1.402 3.571 0 1.623-.502 2.951-1.432 3.866-.929.93-2.464 1.432-4.397 1.432-.915 0-1.682-.045-2.331-.118v-9.784zm1.284 8.793c.324.06.796.074 1.298.074 2.745 0 4.235-1.534 4.235-4.221.015-2.346-1.313-3.836-4.028-3.836-.664 0-1.166.059-1.505.133z"/>
<path d="m43.312 28.202c0 2.642-1.83 3.792-3.556 3.792-1.933 0-3.423-1.416-3.423-3.674 0-2.391 1.564-3.792 3.542-3.792 2.05 0 3.437 1.49 3.437 3.674zm-5.666.073c0 1.564.9 2.745 2.169 2.745 1.239 0 2.169-1.166 2.169-2.774 0-1.21-.605-2.745-2.14-2.745-1.533 0-2.198 1.417-2.198 2.774z"/>
<path d="m44.953 26.623c0-.738-.015-1.343-.059-1.933h1.151l.074 1.181h.03c.354-.679 1.18-1.343 2.361-1.343.988 0 2.523.59 2.523 3.04v4.265h-1.298v-4.117c0-1.151-.428-2.11-1.653-2.11-.855 0-1.52.605-1.741 1.328-.059.163-.089.384-.089.605v4.294h-1.298v-5.21z"/>
<path d="m57.099 31.832-.104-.9h-.044c-.398.561-1.166 1.062-2.184 1.062-1.446 0-2.184-1.018-2.184-2.051 0-1.727 1.534-2.671 4.293-2.656v-.148c0-.59-.162-1.652-1.623-1.652-.664 0-1.357.207-1.859.531l-.294-.856c.59-.384 1.446-.634 2.346-.634 2.184 0 2.715 1.49 2.715 2.921v2.671c0 .62.03 1.225.118 1.712zm-.192-3.644c-1.417-.029-3.025.221-3.025 1.608 0 .841.561 1.239 1.225 1.239.93 0 1.52-.59 1.727-1.195.044-.133.074-.28.074-.413v-1.239z"/>
<path d="m61.764 22.639v2.051h1.859v.989h-1.859v3.852c0 .886.251 1.387.974 1.387.339 0 .59-.044.752-.088l.059.974c-.251.104-.649.177-1.151.177-.605 0-1.092-.191-1.402-.546-.369-.384-.502-1.018-.502-1.859v-3.896h-1.106v-.99h1.106v-1.711z"/>
<path d="m65.72 28.497c.029 1.756 1.151 2.479 2.449 2.479.93 0 1.49-.163 1.977-.369l.222.93c-.458.206-1.24.442-2.376.442-2.198 0-3.512-1.446-3.512-3.601s1.269-3.851 3.35-3.851c2.332 0 2.951 2.051 2.951 3.364 0 .266-.03.472-.044.605h-5.017zm3.806-.93c.015-.826-.339-2.11-1.8-2.11-1.313 0-1.889 1.209-1.992 2.11z"/>
<path d="m77.084 22.639v2.051h1.859v.989h-1.859v3.852c0 .886.251 1.387.974 1.387.339 0 .59-.044.752-.088l.059.974c-.251.104-.649.177-1.151.177-.605 0-1.092-.191-1.402-.546-.369-.384-.502-1.018-.502-1.859v-3.896h-1.106v-.99h1.106v-1.711z"/>
<path d="m86.78 28.202c0 2.642-1.83 3.792-3.556 3.792-1.933 0-3.423-1.416-3.423-3.674 0-2.391 1.564-3.792 3.542-3.792 2.05 0 3.437 1.49 3.437 3.674zm-5.666.073c0 1.564.9 2.745 2.169 2.745 1.239 0 2.169-1.166 2.169-2.774 0-1.21-.605-2.745-2.14-2.745-1.534 0-2.198 1.417-2.198 2.774z"/>
</g>
<path d="m98.329 22.548v-8.32l10.112 4.812-7.447 5.435 3.511 2.537 8.67-6.263c.888-.685 1.47-1.238 1.421-2.137-.07-1.287-.923-1.772-1.695-2.081l-15.397-7.293s-1.7-.78-2.801.482c-.384.44-.453 1.765-.453 1.765v31.58h.029c-.036-1.295 1.166-1.9 1.166-1.9l2.884-1.372v-8.321z" fill="#cdd932"/>
<path d="m104.505 27.01-6.176-4.462v8.929z" fill="#1d7a44"/>
<path d="m113.176 33.271-8.67-6.262-3.511 2.536 7.446 5.435-10.111 4.813-2.884 1.372s-1.202.604-1.166 1.901c.009.324.093.688.294 1.101.745 1.521 2.93.615 2.93.615l15.398-7.292c.771-.309 1.625-.793 1.693-2.081.05-.899-.533-1.452-1.419-2.138z" fill="#51b13e"/>
</svg>
</a>
</p>
</div>
</div>
</div>
</section>

View File

@@ -25,127 +25,131 @@
</script> </script>
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["Title"]</h2> <h2 class="mb-4">@ViewData["Title"]</h2>
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<form asp-action="CreateInvoice" method="post" id="create-invoice-form"> <form asp-action="CreateInvoice" method="post" id="create-invoice-form">
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group"> @if (Model.StoreId != null)
<label asp-for="Stores" class="form-label"></label> {
<select asp-for="StoreId" asp-items="Model.Stores" class="form-select"></select> <input type="hidden" asp-for="StoreId" />
<span asp-validation-for="StoreId" class="text-danger"></span> }
else
{
<div class="form-group">
<label asp-for="Stores" class="form-label"></label>
<select asp-for="StoreId" asp-items="Model.Stores" class="form-select"></select>
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-4">Invoice Details</h4>
}
<div class="form-group">
<label asp-for="Amount" class="form-label"></label>
<input asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OrderId" class="form-label"></label>
<input asp-for="OrderId" class="form-control" />
<span asp-validation-for="OrderId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ItemDesc" class="form-label"></label>
<input asp-for="ItemDesc" class="form-control" />
<span asp-validation-for="ItemDesc" class="text-danger"></span>
</div>
<div class="form-group mb-4">
<label asp-for="SupportedTransactionCurrencies" class="form-label"></label>
@foreach (var item in Model.AvailablePaymentMethods)
{
<div class="form-check mb-2">
<label class="form-check-label">
<input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value">
@item.Text
</label>
</div> </div>
<h4 class="mt-5 mb-4">Invoice Details</h4> }
<div class="form-group"> <span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
<label asp-for="Amount" class="form-label"></label> </div>
<input asp-for="Amount" class="form-control" /> <div class="form-group">
<span asp-validation-for="Amount" class="text-danger"></span> <label asp-for="DefaultPaymentMethod" class="form-label"></label>
</div> <select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select">
<div class="form-group"> <option value="" selected>Use the stores default</option>
<label asp-for="Currency" class="form-label"></label> </select>
<input asp-for="Currency" class="form-control" /> <span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span>
<span asp-validation-for="Currency" class="text-danger"></span> </div>
</div> <h4 class="mt-5 mb-4">Customer Information</h4>
<div class="form-group"> <div class="form-group">
<label asp-for="OrderId" class="form-label"></label> <label asp-for="BuyerEmail" class="form-label"></label>
<input asp-for="OrderId" class="form-control" /> <input asp-for="BuyerEmail" class="form-control" />
<span asp-validation-for="OrderId" class="text-danger"></span> <span asp-validation-for="BuyerEmail" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="ItemDesc" class="form-label"></label> <label asp-for="RequiresRefundEmail" class="form-label"></label>
<input asp-for="ItemDesc" class="form-control" /> <select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select"></select>
<span asp-validation-for="ItemDesc" class="text-danger"></span> <span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
</div> </div>
<div class="form-group mb-4">
<label asp-for="SupportedTransactionCurrencies" class="form-label"></label> <h4 class="mt-5 mb-2">Additional Options</h4>
@foreach (var item in Model.AvailablePaymentMethods) <div class="form-group">
{ <div class="accordion" id="additional">
<div class="form-check mb-2"> <div class="accordion-item">
<label class="form-check-label"> <h2 class="accordion-header" id="additional-pos-data-header">
<input name="SupportedTransactionCurrencies" class="form-check-input" checked="checked" type="checkbox" value="@item.Value"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-pos-data" aria-expanded="false" aria-controls="additional-pos-data">
@item.Text Point Of Sale Data
</label> <vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-pos-data" class="accordion-collapse collapse" aria-labelledby="additional-pos-data-header">
<p>Custom data to correlate the invoice with an order. This data can be a simple text, number or JSON object, e.g. <code>{ "orderId": 615, "product": "Pizza" }</code></p>
<div class="form-group">
<label asp-for="PosData" class="form-label"></label>
<input asp-for="PosData" class="form-control"/>
<span asp-validation-for="PosData" class="text-danger"></span>
</div> </div>
} </div>
<span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="accordion-item">
<label asp-for="DefaultPaymentMethod" class="form-label"></label> <h2 class="accordion-header" id="additional-notifications-header">
<select asp-for="DefaultPaymentMethod" asp-items="Model.AvailablePaymentMethods" class="form-select"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notifications" aria-expanded="false" aria-controls="additional-notifications">
<option value="" selected>Use the stores default</option> Invoice Notifications
</select> <vc:icon symbol="caret-down"/>
<span asp-validation-for="DefaultPaymentMethod" class="text-danger"></span> </button>
</div> </h2>
<h4 class="mt-5 mb-4">Customer Information</h4> <div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header">
<div class="form-group"> <div class="accordion-body">
<label asp-for="BuyerEmail" class="form-label"></label> <div class="form-group">
<input asp-for="BuyerEmail" class="form-control" /> <label asp-for="NotificationUrl" class="form-label"></label>
<span asp-validation-for="BuyerEmail" class="text-danger"></span> <input asp-for="NotificationUrl" class="form-control"/>
</div> <span asp-validation-for="NotificationUrl" class="text-danger"></span>
<div class="form-group">
<label asp-for="RequiresRefundEmail" class="form-label"></label>
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select"></select>
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-pos-data-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-pos-data" aria-expanded="false" aria-controls="additional-pos-data">
Point Of Sale Data
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-pos-data" class="accordion-collapse collapse" aria-labelledby="additional-pos-data-header">
<p>Custom data to correlate the invoice with an order. This data can be a simple text, number or JSON object, e.g. <code>{ "orderId": 615, "product": "Pizza" }</code></p>
<div class="form-group">
<label asp-for="PosData" class="form-label"></label>
<input asp-for="PosData" class="form-control"/>
<span asp-validation-for="PosData" class="text-danger"></span>
</div>
</div> </div>
</div> <div class="form-group">
<div class="accordion-item"> <label asp-for="NotificationEmail" class="form-label"></label>
<h2 class="accordion-header" id="additional-notifications-header"> <input asp-for="NotificationEmail" class="form-control"/>
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notifications" aria-expanded="false" aria-controls="additional-notifications"> <span asp-validation-for="NotificationEmail" class="text-danger"></span>
Invoice Notifications <p id="InvoiceEmailHelpBlock" class="form-text text-muted">
<vc:icon symbol="caret-down"/> Receive updates for this invoice.
</button> </p>
</h2>
<div id="additional-notifications" class="accordion-collapse collapse" aria-labelledby="additional-notifications-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="NotificationUrl" class="form-label"></label>
<input asp-for="NotificationUrl" class="form-control"/>
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="form-label"></label>
<input asp-for="NotificationEmail" class="form-control"/>
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
Receive updates for this invoice.
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
<a asp-action="ListInvoices" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</form>
</div> </div>
</div>
<div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
<a asp-action="ListInvoices" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</form>
</div> </div>
</section> </div>

View File

@@ -44,353 +44,351 @@
</script> </script>
} }
<section class="invoice-details"> <div class="invoice-details">
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<div class="row mb-5"> <div class="row mb-5">
<h2 class="col-xs-12 col-lg-6 mb-4 mb-lg-0">@ViewData["Title"]</h2> <h2 class="col-xs-12 col-lg-6 mb-4 mb-lg-0">@ViewData["Title"]</h2>
<div class="col-xs-12 col-lg-6 mb-2 mb-lg-0 text-lg-end"> <div class="col-xs-12 col-lg-6 mb-2 mb-lg-0 text-lg-end">
<div class="d-inline-flex"> <div class="d-inline-flex">
@if (Model.ShowCheckout) @if (Model.ShowCheckout)
{
<a asp-action="Checkout" class="invoice-checkout-link btn btn-primary text-nowrap ms-2" asp-route-invoiceId="@Model.Id">
<i class="fa fa-qrcode"></i>
Checkout
</a>
}
@if (Model.CanRefund)
{
<a id="refundlink" class="btn btn-success text-nowrap" asp-action="Refund" asp-route-invoiceId="@Context.GetRouteValue("invoiceId")"><span class="fa fa-undo"></span> Issue Refund</a>
}
else
{
<button href="#" class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it." disabled><span class="fa fa-undo me-1"></span> Issue refund</button>
}
<form class="p-0 ms-3" asp-action="ToggleArchive" asp-route-invoiceId="@Model.Id" method="post">
<button type="submit" class="btn @(Model.Archived ? "btn-warning" : "btn btn-danger")" id="btn-archive-toggle">
@if (Model.Archived)
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Unarchive this invoice">Unarchive</span>
}
else
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this invoice so that it does not appear in the invoice list by default"><i class="fa fa-archive me-1"></i> Archive</span>
}
</button>
</form>
</div>
</div>
</div>
<div class="row justify-content-between">
<div class="col-md-5">
<h3 class="mb-3">Invoice Information</h3>
<table class="table table-responsive-md mb-5">
<tr>
<th class="fw-semibold">Store</th>
<td><a href="@Model.StoreLink" rel="noreferrer noopener">@Model.StoreName</a></td>
</tr>
<tr>
<th class="fw-semibold">Invoice Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th class="fw-semibold">Order Id</th>
<td>
@if (string.IsNullOrEmpty(Model.TypedMetadata.OrderUrl))
{
@Model.TypedMetadata.OrderId
}
else
{
<a href="@Model.TypedMetadata.OrderUrl" rel="noreferrer noopener" target="_blank">
@if (string.IsNullOrEmpty(Model.TypedMetadata.OrderId))
{
<span>View Order</span>
}
else
{
@Model.TypedMetadata.OrderId
}
</a>
}
</td>
</tr>
<tr>
<th class="fw-semibold">Payment Request Id</th>
<td><a href="@Model.PaymentRequestLink" rel="noreferrer noopener">@Model.TypedMetadata.PaymentRequestId</a></td>
</tr>
<tr>
<th class="fw-semibold">State</th>
<td>
@if (Model.CanMarkStatus)
{
<div class="dropdown">
<button class="btn btn-secondary btn-sm dropdown-toggle py-1 px-2" type="button" id="markStatusDropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@Model.State
@if (Model.StatusException != InvoiceExceptionStatus.None)
{
@String.Format("({0})", Model.StatusException.ToString());
}
</button>
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
@if (Model.CanMarkInvalid)
{
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
Mark as invalid <span class="fa fa-times"></span>
</a>
}
@if (Model.CanMarkSettled)
{
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
Mark as settled <span class="fa fa-check-circle"></span>
</a>
}
</div>
</div>
}
else
{
@Model.State
@if (Model.StatusException != InvoiceExceptionStatus.None)
{
@String.Format(" ({0})", Model.StatusException.ToString());
}
}
</td>
</tr>
<tr>
<th class="fw-semibold">Created Date</th>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th class="fw-semibold">Expiration Date</th>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th class="fw-semibold">Monitoring Date</th>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<th class="fw-semibold">Transaction Speed</th>
<td>@Model.TransactionSpeed</td>
</tr>
<tr>
<th class="fw-semibold">Total Fiat Due</th>
<td>@Model.Fiat</td>
</tr>
@if (!string.IsNullOrEmpty(Model.RefundEmail))
{
<tr>
<th class="fw-semibold">Refund Email</th>
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.NotificationUrl))
{
<tr>
<th class="fw-semibold">Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
{
<tr>
<th class="fw-semibold">Redirect Url</th>
<td><a href="@Model.RedirectUrl" rel="noreferrer noopener">@Model.RedirectUrl</a></td>
</tr>
}
</table>
@if (Model.PosData.Count == 0)
{ {
<h3 class="mb-3">Product Information</h3> <a asp-action="Checkout" class="invoice-checkout-link btn btn-primary text-nowrap ms-2" asp-route-invoiceId="@Model.Id">
<table class="table table-responsive-md mb-5"> <i class="fa fa-qrcode"></i>
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode)) Checkout
{ </a>
<tr>
<th class="fw-semibold">Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
{
<tr>
<th class="fw-semibold">Item Description</th>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
}
<tr>
<th class="fw-semibold">Price</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th class="fw-semibold">Tax Included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
} }
</div> @if (Model.CanRefund)
<div class="col-md-5">
<h3 class="mb-3">Buyer Information</h3>
<table class="table table-responsive-md mb-5">
<tr>
<th class="fw-semibold">Name</th>
<td>@Model.TypedMetadata.BuyerName</td>
</tr>
<tr>
<th class="fw-semibold">Email</th>
<td><a href="mailto:@Model.TypedMetadata.BuyerEmail">@Model.TypedMetadata.BuyerEmail</a></td>
</tr>
<tr>
<th class="fw-semibold">Phone</th>
<td>@Model.TypedMetadata.BuyerPhone</td>
</tr>
<tr>
<th class="fw-semibold">Address 1</th>
<td>@Model.TypedMetadata.BuyerAddress1</td>
</tr>
<tr>
<th class="fw-semibold">Address 2</th>
<td>@Model.TypedMetadata.BuyerAddress2</td>
</tr>
<tr>
<th class="fw-semibold">City</th>
<td>@Model.TypedMetadata.BuyerCity</td>
</tr>
<tr>
<th class="fw-semibold">State</th>
<td>@Model.TypedMetadata.BuyerState</td>
</tr>
<tr>
<th class="fw-semibold">Country</th>
<td>@Model.TypedMetadata.BuyerCountry</td>
</tr>
<tr>
<th class="fw-semibold">Zip</th>
<td>@Model.TypedMetadata.BuyerZip</td>
</tr>
</table>
</div>
</div>
@if (Model.PosData.Count != 0)
{
<div class="row">
<div class="col-md-6">
<h3 class="mb-3">Product information</h3>
<table class="table table-responsive-md mb-5">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
{
<tr>
<th>Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
{
<tr>
<th>Item Description</th>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
}
<tr>
<th>Price</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
</div>
<div class="col-md-6 mb-4" id="posData">
<h3 class="mb-3">Point of Sale Data</h3>
<partial name="PosData" model="(Model.PosData, 1)" />
</div>
</div>
}
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)" />
@if (Model.Deliveries.Count != 0)
{
<h3 class="mb-3">Webhook deliveries</h3>
<ul class="list-group mb-5">
@foreach (var delivery in Model.Deliveries)
{ {
<li class="list-group-item "> <a id="refundlink" class="btn btn-success text-nowrap" asp-action="Refund" asp-route-invoiceId="@Context.GetRouteValue("invoiceId")"><span class="fa fa-undo"></span> Issue Refund</a>
<form
asp-action="RedeliverWebhook"
asp-route-storeId="@Model.StoreId"
asp-route-invoiceId="@Model.Id"
asp-route-deliveryId="@delivery.Id"
method="post">
<div class="d-flex align-items-center">
<span class="d-flex align-items-center flex-fill me-3">
@if (delivery.Success)
{
<span class="d-flex align-items-center fa fa-check text-success" title="Success"></span>
}
else
{
<span class="d-flex align-items-center fa fa-times text-danger" title="@delivery.ErrorMessage"></span>
}
<span class="ms-3">
<a
asp-action="WebhookDelivery"
asp-route-invoiceId="@Model.Id"
asp-route-deliveryId="@delivery.Id"
class="btn btn-link delivery-content" target="_blank">
@delivery.Id
</a>
<span class="text-light mx-2">|</span>
<span class="small text-muted">@delivery.Type</span>
<span class="text-light mx-2">|</span>
</span>
</span>
<span class="d-flex align-items-center">
<span class="text-nowrap" data-bs-toggle="tooltip" title="@delivery.PayloadUrl" style="cursor: pointer;">
<span class="small text-truncate d-inline-block" style="max-width: 250px;">@delivery.PayloadUrl</span>
</span>
<span class="text-light mx-2">|</span>
<strong class="d-flex align-items-center text-muted small">
@delivery.Time.ToBrowserDate()
</strong>
<button id="#redeliver-@delivery.Id"
type="submit"
class="btn btn-info py-1 ms-3 redeliver">
Redeliver
</button>
</span>
</div>
</form>
</li>
} }
</ul> else
} {
<div class="row"> <button href="#" class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it." disabled><span class="fa fa-undo me-1"></span> Issue refund</button>
<div class="col-md-12"> }
<h3 class="mb-0">Events</h3> <form class="p-0 ms-3" asp-action="ToggleArchive" asp-route-invoiceId="@Model.Id" method="post">
<table class="table table-hover table-responsive-md"> <button type="submit" class="btn @(Model.Archived ? "btn-warning" : "btn btn-danger")" id="btn-archive-toggle">
<thead class="thead-inverse"> @if (Model.Archived)
<tr>
<th>Date</th>
<th>Message</th>
</tr>
</thead>
<tbody>
@foreach (var evt in Model.Events)
{ {
<tr class="text-@evt.GetCssClass()"> <span class="text-nowrap" data-bs-toggle="tooltip" title="Unarchive this invoice">Unarchive</span>
<td>@evt.Timestamp.ToBrowserDate()</td>
<td>@evt.Message</td>
</tr>
} }
</tbody> else
</table> {
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this invoice so that it does not appear in the invoice list by default"><i class="fa fa-archive me-1"></i> Archive</span>
}
</button>
</form>
</div> </div>
</div> </div>
</div> </div>
</section>
<div class="row justify-content-between">
<div class="col-md-5">
<h3 class="mb-3">Invoice Information</h3>
<table class="table table-responsive-md mb-5">
<tr>
<th class="fw-semibold">Store</th>
<td><a href="@Model.StoreLink" rel="noreferrer noopener">@Model.StoreName</a></td>
</tr>
<tr>
<th class="fw-semibold">Invoice Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th class="fw-semibold">Order Id</th>
<td>
@if (string.IsNullOrEmpty(Model.TypedMetadata.OrderUrl))
{
@Model.TypedMetadata.OrderId
}
else
{
<a href="@Model.TypedMetadata.OrderUrl" rel="noreferrer noopener" target="_blank">
@if (string.IsNullOrEmpty(Model.TypedMetadata.OrderId))
{
<span>View Order</span>
}
else
{
@Model.TypedMetadata.OrderId
}
</a>
}
</td>
</tr>
<tr>
<th class="fw-semibold">Payment Request Id</th>
<td><a href="@Model.PaymentRequestLink" rel="noreferrer noopener">@Model.TypedMetadata.PaymentRequestId</a></td>
</tr>
<tr>
<th class="fw-semibold">State</th>
<td>
@if (Model.CanMarkStatus)
{
<div class="dropdown">
<button class="btn btn-secondary btn-sm dropdown-toggle py-1 px-2" type="button" id="markStatusDropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@Model.State
@if (Model.StatusException != InvoiceExceptionStatus.None)
{
@String.Format("({0})", Model.StatusException.ToString());
}
</button>
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
@if (Model.CanMarkInvalid)
{
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
Mark as invalid <span class="fa fa-times"></span>
</a>
}
@if (Model.CanMarkSettled)
{
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
Mark as settled <span class="fa fa-check-circle"></span>
</a>
}
</div>
</div>
}
else
{
@Model.State
@if (Model.StatusException != InvoiceExceptionStatus.None)
{
@String.Format(" ({0})", Model.StatusException.ToString());
}
}
</td>
</tr>
<tr>
<th class="fw-semibold">Created Date</th>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th class="fw-semibold">Expiration Date</th>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th class="fw-semibold">Monitoring Date</th>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<th class="fw-semibold">Transaction Speed</th>
<td>@Model.TransactionSpeed</td>
</tr>
<tr>
<th class="fw-semibold">Total Fiat Due</th>
<td>@Model.Fiat</td>
</tr>
@if (!string.IsNullOrEmpty(Model.RefundEmail))
{
<tr>
<th class="fw-semibold">Refund Email</th>
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.NotificationUrl))
{
<tr>
<th class="fw-semibold">Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
{
<tr>
<th class="fw-semibold">Redirect Url</th>
<td><a href="@Model.RedirectUrl" rel="noreferrer noopener">@Model.RedirectUrl</a></td>
</tr>
}
</table>
@if (Model.PosData.Count == 0)
{
<h3 class="mb-3">Product Information</h3>
<table class="table table-responsive-md mb-5">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
{
<tr>
<th class="fw-semibold">Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
{
<tr>
<th class="fw-semibold">Item Description</th>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
}
<tr>
<th class="fw-semibold">Price</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th class="fw-semibold">Tax Included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
}
</div>
<div class="col-md-5">
<h3 class="mb-3">Buyer Information</h3>
<table class="table table-responsive-md mb-5">
<tr>
<th class="fw-semibold">Name</th>
<td>@Model.TypedMetadata.BuyerName</td>
</tr>
<tr>
<th class="fw-semibold">Email</th>
<td><a href="mailto:@Model.TypedMetadata.BuyerEmail">@Model.TypedMetadata.BuyerEmail</a></td>
</tr>
<tr>
<th class="fw-semibold">Phone</th>
<td>@Model.TypedMetadata.BuyerPhone</td>
</tr>
<tr>
<th class="fw-semibold">Address 1</th>
<td>@Model.TypedMetadata.BuyerAddress1</td>
</tr>
<tr>
<th class="fw-semibold">Address 2</th>
<td>@Model.TypedMetadata.BuyerAddress2</td>
</tr>
<tr>
<th class="fw-semibold">City</th>
<td>@Model.TypedMetadata.BuyerCity</td>
</tr>
<tr>
<th class="fw-semibold">State</th>
<td>@Model.TypedMetadata.BuyerState</td>
</tr>
<tr>
<th class="fw-semibold">Country</th>
<td>@Model.TypedMetadata.BuyerCountry</td>
</tr>
<tr>
<th class="fw-semibold">Zip</th>
<td>@Model.TypedMetadata.BuyerZip</td>
</tr>
</table>
</div>
</div>
@if (Model.PosData.Count != 0)
{
<div class="row">
<div class="col-md-6">
<h3 class="mb-3">Product information</h3>
<table class="table table-responsive-md mb-5">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
{
<tr>
<th>Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
{
<tr>
<th>Item Description</th>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
}
<tr>
<th>Price</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
</div>
<div class="col-md-6 mb-4" id="posData">
<h3 class="mb-3">Point of Sale Data</h3>
<partial name="PosData" model="(Model.PosData, 1)" />
</div>
</div>
}
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)" />
@if (Model.Deliveries.Count != 0)
{
<h3 class="mb-3">Webhook deliveries</h3>
<ul class="list-group mb-5">
@foreach (var delivery in Model.Deliveries)
{
<li class="list-group-item ">
<form
asp-action="RedeliverWebhook"
asp-route-storeId="@Model.StoreId"
asp-route-invoiceId="@Model.Id"
asp-route-deliveryId="@delivery.Id"
method="post">
<div class="d-flex align-items-center">
<span class="d-flex align-items-center flex-fill me-3">
@if (delivery.Success)
{
<span class="d-flex align-items-center fa fa-check text-success" title="Success"></span>
}
else
{
<span class="d-flex align-items-center fa fa-times text-danger" title="@delivery.ErrorMessage"></span>
}
<span class="ms-3">
<a
asp-action="WebhookDelivery"
asp-route-invoiceId="@Model.Id"
asp-route-deliveryId="@delivery.Id"
class="btn btn-link delivery-content" target="_blank">
@delivery.Id
</a>
<span class="text-light mx-2">|</span>
<span class="small text-muted">@delivery.Type</span>
<span class="text-light mx-2">|</span>
</span>
</span>
<span class="d-flex align-items-center">
<span class="text-nowrap" data-bs-toggle="tooltip" title="@delivery.PayloadUrl" style="cursor: pointer;">
<span class="small text-truncate d-inline-block" style="max-width: 250px;">@delivery.PayloadUrl</span>
</span>
<span class="text-light mx-2">|</span>
<strong class="d-flex align-items-center text-muted small">
@delivery.Time.ToBrowserDate()
</strong>
<button id="#redeliver-@delivery.Id"
type="submit"
class="btn btn-info py-1 ms-3 redeliver">
Redeliver
</button>
</span>
</div>
</form>
</li>
}
</ul>
}
<div class="row">
<div class="col-md-12">
<h3 class="mb-0">Events</h3>
<table class="table table-hover table-responsive-md">
<thead class="thead-inverse">
<tr>
<th>Date</th>
<th>Message</th>
</tr>
</thead>
<tbody>
@foreach (var evt in Model.Events)
{
<tr class="text-@evt.GetCssClass()">
<td>@evt.Timestamp.ToBrowserDate()</td>
<td>@evt.Message</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -178,286 +178,287 @@
} }
@Html.HiddenFor(a => a.Count) @Html.HiddenFor(a => a.Count)
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-4"> <partial name="_StatusMessage" />
<h2 class="mb-0">
@ViewData["Title"] <div class="d-sm-flex align-items-center justify-content-between mb-4">
<small> <h2 class="mb-0">
<a href="https://docs.btcpayserver.org/PaymentRequests/" class="ms-1" target="_blank" rel="noreferrer noopener"> @ViewData["Title"]
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span> <small>
<a href="https://docs.btcpayserver.org/PaymentRequests/" class="ms-1" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</small>
</h2>
<a id="CreateNewInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchTerm="@Model.SearchTerm" class="btn btn-primary mt-3 mt-sm-0">
<span class="fa fa-plus"></span>
Create an invoice
</a>
</div>
<partial name="InvoiceStatusChangePartial"/>
<div class="row">
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto">
<form asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
<input type="hidden" asp-for="Count"/>
<input asp-for="TimezoneOffset" type="hidden"/>
<div class="input-group">
<a href="#help" class="input-group-text text-secondary text-decoration-none" data-bs-toggle="collapse">
<span class="fa fa-filter"></span>
</a> </a>
</small> <input asp-for="SearchTerm" class="form-control"/>
</h2> <button type="submit" class="btn btn-secondary" title="Search invoice">
<a id="CreateNewInvoice" asp-action="CreateInvoice" asp-route-searchTerm="@Model.SearchTerm" class="btn btn-primary mt-3 mt-sm-0"> <span class="fa fa-search"></span> Search
<span class="fa fa-plus"></span> </button>
Create an invoice <button type="button" id="SearchOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</a> <span class="visually-hidden">Toggle Dropdown</span>
</div> </button>
<partial name="InvoiceStatusChangePartial"/>
<div class="row"> <div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle">
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto"> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:invalid@{@storeIds}">Invalid Invoices</a>
<form asp-action="ListInvoices" method="get"> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="status:processing,status:settled@{@storeIds}">Paid Invoices</a>
<input type="hidden" asp-for="Count"/> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidLate@{@storeIds}">Paid Late Invoices</a>
<input asp-for="TimezoneOffset" type="hidden"/> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidPartial@{@storeIds}">Paid Partial Invoices</a>
<div class="input-group"> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidOver@{@storeIds}">Paid Over Invoices</a>
<a href="#help" class="input-group-text text-secondary text-decoration-none" data-bs-toggle="collapse"> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="unusual:true@{@storeIds}">Unusual Invoices</a>
<span class="fa fa-filter"></span> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true@{@storeIds}">Archived Invoices</a>
</a> <div role="separator" class="dropdown-divider"></div>
<input asp-for="SearchTerm" class="form-control"/> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-24h@{@storeIds}">Last 24 hours</a>
<button type="submit" class="btn btn-secondary" title="Search invoice"> <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-3d@{@storeIds}">Last 3 days</a>
<span class="fa fa-search"></span> Search <a class="dropdown-item" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-7d@{@storeIds}">Last 7 days</a>
</button> <button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button>
<button type="button" id="SearchOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <div role="separator" class="dropdown-divider"></div>
<span class="visually-hidden">Toggle Dropdown</span> <a class="dropdown-item" href="?searchTerm=">Unfiltered</a>
</button> </div>
</div>
<span asp-validation-for="SearchTerm" class="text-danger"></span>
</form>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle"> @* Custom Range Modal *@
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="status:invalid@{@storeIds}">Invalid Invoices</a> <div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="status:processing,status:settled@{@storeIds}">Paid Invoices</a> <div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidLate@{@storeIds}">Paid Late Invoices</a> <div class="modal-content">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidPartial@{@storeIds}">Paid Partial Invoices</a> <div class="modal-header">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidOver@{@storeIds}">Paid Over Invoices</a> <h5 class="modal-title" id="customRangeModalTitle">Filter invoices by Custom Range</h5>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="unusual:true@{@storeIds}">Unusual Invoices</a> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true@{@storeIds}">Archived Invoices</a> <vc:icon symbol="close" />
<div role="separator" class="dropdown-divider"></div> </button>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-24h@{@storeIds}">Last 24 hours</a> </div>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-3d@{@storeIds}">Last 3 days</a> <div class="modal-body">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-timezoneoffset="0" asp-route-searchTerm="startDate:-7d@{@storeIds}">Last 7 days</a> <div class="form-group row">
<button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button> <label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label>
<div role="separator" class="dropdown-divider"></div> <div class="col-sm-9">
<a class="dropdown-item" href="?searchTerm=">Unfiltered</a> <div class="input-group">
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="Start Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">End Date</label>
<div class="col-sm-9">
<div class="input-group">
<input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="End Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div> </div>
</div> </div>
<span asp-validation-for="SearchTerm" class="text-danger"></span> <div class="modal-footer">
</form> <button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
@* Custom Range Modal *@
<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customRangeModalTitle">Filter invoices by Custom Range</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<vc:icon symbol="close" />
</button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="dtpStartDate" class="col-sm-3 col-form-label">Start Date</label>
<div class="col-sm-9">
<div class="input-group">
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="Start Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">End Date</label>
<div class="col-sm-9">
<div class="input-group">
<input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="End Date" />
<button type="button" class="btn btn-primary input-group-clear" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btnCustomRangeDate" type="button" class="btn btn-primary">Filter</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row collapse" id="help">
<div class="col @(Model.Total > 0 ? "pt-3 pb-lg-5" : "")">
<p>
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.
Be sure to split your search parameters with comma, for example:<br />
<code>startdate:2019-04-25 13:00:00, status:paid</code>
</p>
<p class="mb-2">
You can also apply filters to your search by searching for <code>filtername:value</code>, supported filters are:
</p>
<ul>
<li><code>storeid:id</code> for filtering a specific store</li>
<li><code>orderid:id</code> for filtering a specific order</li>
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
<li><code>status:(expired|invalid|settled|processing|new)</code> for filtering a specific status</li>
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
<li><code>startdate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created after certain date</li>
<li><code>enddate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created before certain date</li>
</ul>
</div>
</div>
@if (Model.Total > 0)
{
<form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5">
<span class="me-2">
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle">
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive"><i class="fa fa-archive"></i> Archive</button>
</div>
</span>
<span>
<a class="btn btn-secondary dropdown-toggle mb-1" href="#" role="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</a>
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</a>
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
</div>
</span>
<a href="https://docs.btcpayserver.org/Accounting/" class="ms-1" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<table id="invoices" class="table table-hover table-responsive-md mt-4">
<thead>
<tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
</th>
<th style="min-width:90px;" class="col-md-auto">
Date
<a id="switchTimeFormat" href="#">
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th style="max-width: 180px;">OrderId</th>
<th>InvoiceId</th>
<th style="min-width: 150px;">Status</th>
<th style="text-align:right">Amount</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr id="invoice_@invoice.InvoiceId" class="invoice-row">
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId"/>
</td>
<td>
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
@invoice.Date.ToBrowserDate()
</span>
</td>
<td style="max-width: 180px;">
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl" class="wraptext200" rel="noreferrer noopener">@invoice.OrderId</a>
}
else
{
<span>@invoice.OrderId</span>
}
</td>
<td>@invoice.InvoiceId</td>
<td>
@if (invoice.Details.Archived)
{
<span class="badge bg-warning">archived</span>
}
@if (invoice.CanMarkStatus)
{
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString());
}
</span>
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
Mark as invalid <span class="fa fa-times"></span>
</button>
}
@if (invoice.CanMarkSettled)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
Mark as settled <span class="fa fa-check-circle"></span>
</button>
}
</div>
</div>
}
else
{
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString());
}
</span>
}
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
{
<span class="badge">@paymentType.GetBadge()</span>
}
</td>
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if (invoice.ShowCheckout)
{
<span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId">Checkout</a>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
@if (!invoice.CanMarkStatus)
{
<span>-</span>
}
</span>
}
&nbsp;
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
<a class="only-for-js invoice-details-toggle" href="#">
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
</a>
</td>
</tr>
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;">
<td colspan="99" class="border-top-0">
<div style="margin-left: 15px; margin-bottom: 0;">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)"/>
</div>
</td>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model" />
</form>
}
else
{
<p class="text-secondary mt-3">
There are no invoices matching your criteria.
</p>
}
</div> </div>
</section> </div>
<div class="row collapse" id="help">
<div class="col @(Model.Total > 0 ? "pt-3 pb-lg-5" : "")">
<p>
You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.
Be sure to split your search parameters with comma, for example:<br />
<code>startdate:2019-04-25 13:00:00, status:paid</code>
</p>
<p class="mb-2">
You can also apply filters to your search by searching for <code>filtername:value</code>, supported filters are:
</p>
<ul>
<li><code>storeid:id</code> for filtering a specific store</li>
<li><code>orderid:id</code> for filtering a specific order</li>
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
<li><code>startdate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created after certain date</li>
<li><code>enddate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created before certain date</li>
</ul>
</div>
</div>
@if (Model.Total > 0)
{
<form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5">
<input type="hidden" name="storeId" value="@Model.StoreId" />
<span class="me-2">
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle">
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive"><i class="fa fa-archive"></i> Archive</button>
</div>
</span>
<span>
<a class="btn btn-secondary dropdown-toggle mb-1" href="#" role="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</a>
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</a>
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
</div>
</span>
<a href="https://docs.btcpayserver.org/Accounting/" class="ms-1" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<table id="invoices" class="table table-hover table-responsive-md mt-4">
<thead>
<tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input"/>
</th>
<th style="min-width:90px;" class="col-md-auto">
Date
<a id="switchTimeFormat" href="#">
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th style="max-width: 180px;">OrderId</th>
<th>InvoiceId</th>
<th style="min-width: 150px;">Status</th>
<th style="text-align:right">Amount</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr id="invoice_@invoice.InvoiceId" class="invoice-row">
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId"/>
</td>
<td>
<span class="switchTimeFormat" data-switch="@invoice.Date.ToTimeAgo()">
@invoice.Date.ToBrowserDate()
</span>
</td>
<td style="max-width: 180px;">
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl" class="wraptext200" rel="noreferrer noopener">@invoice.OrderId</a>
}
else
{
<span>@invoice.OrderId</span>
}
</td>
<td>@invoice.InvoiceId</td>
<td>
@if (invoice.Details.Archived)
{
<span class="badge bg-warning">archived</span>
}
@if (invoice.CanMarkStatus)
{
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
</span>
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
Mark as invalid <span class="fa fa-times"></span>
</button>
}
@if (invoice.CanMarkSettled)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
Mark as settled <span class="fa fa-check-circle"></span>
</button>
}
</div>
</div>
}
else
{
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
</span>
}
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
{
<span class="badge">@paymentType.GetBadge()</span>
}
</td>
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if (invoice.ShowCheckout)
{
<span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId">Checkout</a>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
@if (!invoice.CanMarkStatus)
{
<span>-</span>
}
</span>
}
&nbsp;
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
<a class="only-for-js invoice-details-toggle" href="#">
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
</a>
</td>
</tr>
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;">
<td colspan="99" class="border-top-0">
<div style="margin-left: 15px; margin-bottom: 0;">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)"/>
</div>
</td>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model"/>
</form>
}
else
{
<p class="text-secondary mt-3">
There are no invoices matching your criteria.
</p>
}

View File

@@ -3,92 +3,90 @@
ViewData["Title"] = "Refund"; ViewData["Title"] = "Refund";
} }
<section> <div class="row">
<div class="row"> <div class="modal-dialog">
<div class="modal-dialog"> <div class="modal-content">
<div class="modal-content"> <div class="modal-header">
<div class="modal-header"> <h4 class="modal-title">@Model.Title</h4>
<h4 class="modal-title">@Model.Title</h4> </div>
</div> <div class="modal-body">
<div class="modal-body"> <form method="post">
<form method="post"> <input type="hidden" asp-for="RefundStep" value="@Model.RefundStep"/>
<input type="hidden" asp-for="RefundStep" value="@Model.RefundStep"/> <input type="hidden" asp-for="Title" value="@Model.Title"/>
<input type="hidden" asp-for="Title" value="@Model.Title"/> @switch (Model.RefundStep)
@switch (Model.RefundStep) {
{ case RefundSteps.SelectPaymentMethod:
case RefundSteps.SelectPaymentMethod: <div class="form-group">
<div class="form-group"> <label asp-for="SelectedPaymentMethod" class="form-label"></label>
<label asp-for="SelectedPaymentMethod" class="form-label"></label> <select asp-items="Model.AvailablePaymentMethods" asp-for="SelectedPaymentMethod" class="form-select"></select>
<select asp-items="Model.AvailablePaymentMethods" asp-for="SelectedPaymentMethod" class="form-select"></select> <span asp-validation-for="SelectedPaymentMethod" class="text-danger"></span>
<span asp-validation-for="SelectedPaymentMethod" class="text-danger"></span> </div>
<div class="form-group">
<button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Next</button>
</div>
break;
case RefundSteps.SelectRate:
<input type="hidden" asp-for="SelectedPaymentMethod"/>
<input type="hidden" asp-for="CryptoAmountThen"/>
<input type="hidden" asp-for="FiatAmount"/>
<input type="hidden" asp-for="CryptoAmountNow"/>
<div class="form-group">
<div class="form-check">
<input id="RateThenText" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
<label for="RateThenText" class="form-check-label">@Model.RateThenText</label>
</div> </div>
<div class="form-group"> <small class="form-text text-muted">The crypto currency price, at the rate the invoice got paid.</small>
<button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Next</button> </div>
<div class="form-group">
<div class="form-check">
<input id="CurrentRateText" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
<label for="CurrentRateText" class="form-check-label">@Model.CurrentRateText</label>
</div> </div>
break; <small class="form-text text-muted">The crypto currency price, at the current rate.</small>
case RefundSteps.SelectRate: </div>
<input type="hidden" asp-for="SelectedPaymentMethod"/> <div class="form-group">
<input type="hidden" asp-for="CryptoAmountThen"/> <div class="form-check">
<input type="hidden" asp-for="FiatAmount"/> <input id="FiatText" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
<input type="hidden" asp-for="CryptoAmountNow"/> <label for="FiatText" class="form-check-label">@Model.FiatText</label>
<div class="form-group">
<div class="form-check">
<input id="RateThenText" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
<label for="RateThenText" class="form-check-label">@Model.RateThenText</label>
</div>
<small class="form-text text-muted">The crypto currency price, at the rate the invoice got paid.</small>
</div>
<div class="form-group">
<div class="form-check">
<input id="CurrentRateText" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
<label for="CurrentRateText" class="form-check-label">@Model.CurrentRateText</label>
</div>
<small class="form-text text-muted">The crypto currency price, at the current rate.</small>
</div>
<div class="form-group">
<div class="form-check">
<input id="FiatText" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
<label for="FiatText" class="form-check-label">@Model.FiatText</label>
</div>
<small class="form-text text-muted">The invoice currency, at the rate when the refund will be sent.</small>
</div> </div>
<small class="form-text text-muted">The invoice currency, at the rate when the refund will be sent.</small>
</div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input id="CustomText" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/> <input id="CustomText" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>
<label for="CustomText" class="form-check-label">Custom</label> <label for="CustomText" class="form-check-label">Custom</label>
</div>
<small class="form-text text-muted">The specified amount with the specified currency, at the rate when the refund will be sent. </small>
</div> </div>
<div class="form-group"> <small class="form-text text-muted">The specified amount with the specified currency, at the rate when the refund will be sent. </small>
<button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Create refund</button> </div>
</div> <div class="form-group">
break; <button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Create refund</button>
case RefundSteps.SelectCustomAmount: </div>
break;
case RefundSteps.SelectCustomAmount:
<input type="hidden" asp-for="SelectedPaymentMethod"/> <input type="hidden" asp-for="SelectedPaymentMethod"/>
<input type="hidden" asp-for="CryptoAmountThen"/> <input type="hidden" asp-for="CryptoAmountThen"/>
<input type="hidden" asp-for="FiatAmount"/> <input type="hidden" asp-for="FiatAmount"/>
<input type="hidden" asp-for="CryptoAmountNow"/> <input type="hidden" asp-for="CryptoAmountNow"/>
<input type="hidden" asp-for="SelectedRefundOption"/> <input type="hidden" asp-for="SelectedRefundOption"/>
<div class="form-group"> <div class="form-group">
<label asp-for="CustomAmount" class="form-label"></label> <label asp-for="CustomAmount" class="form-label"></label>
<div class="input-group"> <div class="input-group">
<input asp-for="CustomAmount" type="number" step="any" asp-format="{0}" class="form-control"/> <input asp-for="CustomAmount" type="number" step="any" asp-format="{0}" class="form-control"/>
<input asp-for="CustomCurrency" type="text" class="form-control"/> <input asp-for="CustomCurrency" type="text" class="form-control"/>
</div>
<span asp-validation-for="CustomAmount" class="text-danger w-100"></span>
<span asp-validation-for="CustomCurrency" class="text-danger w-100"></span>
</div> </div>
<div class="form-group"> <span asp-validation-for="CustomAmount" class="text-danger w-100"></span>
<button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Next</button> <span asp-validation-for="CustomCurrency" class="text-danger w-100"></span>
</div> </div>
break; <div class="form-group">
} <button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Next</button>
</form> </div>
</div> break;
}
</form>
</div> </div>
</div> </div>
</div> </div>
</section> </div>

View File

@@ -4,31 +4,29 @@
ViewData["Title"] = "Confirm Lightning Payout"; ViewData["Title"] = "Confirm Lightning Payout";
var cryptoCode = Context.GetRouteValue("cryptoCode"); var cryptoCode = Context.GetRouteValue("cryptoCode");
} }
<section>
<div class="container">
<h2 class="mb-4">@ViewData["Title"]</h2> <h2 class="mb-4">@ViewData["Title"]</h2>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<ul class="list-group"> <ul class="list-group">
@foreach (var item in Model) @foreach (var item in Model)
{ {
<li class="list-group-item d-flex justify-content-between align-items-center"> <li class="list-group-item d-flex justify-content-between align-items-center">
<div data-bs-toggle="tooltip" class="text-break" title="@item.Destination">@item.Destination</div> <div data-bs-toggle="tooltip" class="text-break" title="@item.Destination">@item.Destination</div>
<span class="text-capitalize badge bg-secondary">@item.Amount @cryptoCode</span> <span class="text-capitalize badge bg-secondary">@item.Amount @cryptoCode</span>
</li> </li>
<form method="post" class="list-group-item justify-content-center" id="pay-invoices-form"> <form method="post" class="list-group-item justify-content-center" id="pay-invoices-form">
<button type="submit" class="btn btn-primary xmx-2" style="min-width:25%;" id="Pay">Pay</button> <button type="submit" class="btn btn-primary xmx-2" style="min-width:25%;" id="Pay">Pay</button>
<button type="button" class="btn btn-secondary mx-2" onclick="history.back(); return false;" style="min-width:25%;">Go back</button> <button type="button" class="btn btn-secondary mx-2" onclick="history.back(); return false;" style="min-width:25%;">Go back</button>
</form> </form>
} }
</ul> </ul>
</div>
</div>
</div> </div>
</div>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
<script> <script>

View File

@@ -4,22 +4,19 @@
Layout = "_Layout"; Layout = "_Layout";
ViewData["Title"] = "Lightning Payout Result"; ViewData["Title"] = "Lightning Payout Result";
} }
<section>
<div class="container"> <h2 class="mb-4">@ViewData["Title"]</h2>
<h2 class="mb-4">@ViewData["Title"]</h2> @foreach (var item in Model)
@foreach (var item in Model) {
<div class="alert alert-@(item.Result == PayResult.Ok ? "success" : "danger") d-flex justify-content-between align-items-center mb-3" role="alert">
@if (item.Result == PayResult.Ok)
{ {
<div class="alert alert-@(item.Result == PayResult.Ok ? "success" : "danger") d-flex justify-content-between align-items-center mb-3" role="alert"> <div class="text-break me-3" title="@item.Destination">@item.Destination</div>
@if (item.Result == PayResult.Ok)
{
<div class="text-break me-3" title="@item.Destination">@item.Destination</div>
}
else
{
<div class="text-break me-3" title="@item.Destination">@item.Destination: @item.Message (@item.Result)</div>
}
<span class="badge fs-5">@(item.Result == PayResult.Ok ? "Sent" : "Failed")</span>
</div>
} }
else
{
<div class="text-break me-3" title="@item.Destination">@item.Destination: @item.Message (@item.Result)</div>
}
<span class="badge fs-5">@(item.Result == PayResult.Ok ? "Sent" : "Failed")</span>
</div> </div>
</section> }

View File

@@ -18,168 +18,165 @@
<input type="hidden" asp-for="ApplicationName" value="@Model.ApplicationName"/> <input type="hidden" asp-for="ApplicationName" value="@Model.ApplicationName"/>
<input type="hidden" asp-for="SelectiveStores" value="@Model.SelectiveStores"/> <input type="hidden" asp-for="SelectiveStores" value="@Model.SelectiveStores"/>
<input type="hidden" asp-for="ApplicationIdentifier" value="@Model.ApplicationIdentifier"/> <input type="hidden" asp-for="ApplicationIdentifier" value="@Model.ApplicationIdentifier"/>
<section> <
<div class="container"> <div class="row">
<div class="row"> <div class="col-lg-12 section-heading">
<div class="col-lg-12 section-heading"> <h2>Authorization Request</h2>
<h2>Authorization Request</h2> <p class="my-3">@(Model.ApplicationName ?? "An application") is requesting access to your account.</p>
<p class="my-3">@(Model.ApplicationName ?? "An application") is requesting access to your account.</p> @if (Model.RedirectUrl != null)
@if (Model.RedirectUrl != null) {
{ <p class="mb-1 alert alert-info">
<p class="mb-1 alert alert-info"> If authorized, the generated API key will be provided to <strong>@Model.RedirectUrl.AbsoluteUri</strong>
If authorized, the generated API key will be provided to <strong>@Model.RedirectUrl.AbsoluteUri</strong> </p>
</p> }
} </div>
</div>
<div class="row">
<div class="col-lg-12 list-group px-2">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="list-group-item ">
<div class="form-group">
<label asp-for="Label" class="form-label"></label>
<input asp-for="Label" class="form-control"/>
<span asp-validation-for="Label" class="text-danger"></span>
</div> </div>
</div> </div>
<div class="row"> @if (!permissions.Any())
<div class="col-lg-12 list-group px-2"> {
<div asp-validation-summary="All" class="text-danger"></div> <div class="list-group-item form-group">
<div class="list-group-item "> <p class="text-center">There are no associated permissions to the API key being requested by the application.<br/>The application cannot do anything with your BTCPay account other than validating your account exists.</p>
<div class="form-group"> </div>
<label asp-for="Label" class="form-label"></label> }
<input asp-for="Label" class="form-control"/>
<span asp-validation-for="Label" class="text-danger"></span>
</div> @for (int i = 0; i < Model.PermissionValues.Count; i++)
</div> {
@if (!permissions.Any()) <input type="hidden" asp-for="PermissionValues[i].Forbidden"/>
<input type="hidden" asp-for="PermissionValues[i].Permission"/>
<input type="hidden" asp-for="PermissionValues[i].StoreMode" value="@Model.PermissionValues[i].StoreMode"/>
@if (Model.PermissionValues[i].Forbidden && !Model.Strict)
{
continue;
}
@if (Policies.IsStorePolicy(Model.PermissionValues[i].Permission))
{
@if (Model.PermissionValues[i].StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
{ {
<div class="list-group-item form-group"> <div class="list-group-item form-group">
<p class="text-center">There are no associated permissions to the API key being requested by the application.<br/>The application cannot do anything with your BTCPay account other than validating your account exists.</p> <div class="form-check d-flex">
@if (Model.Strict || Model.PermissionValues[i].Forbidden)
{
<input id="@Model.PermissionValues[i].Permission" type="hidden" asp-for="PermissionValues[i].Value"/>
<input type="checkbox" checked="@Model.PermissionValues[i].Value" disabled class="form-check-input"/>
}
else
{
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input"/>
}
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label m-0 me-4 ms-2">@Model.PermissionValues[i].Title</label>
@if (Model.SelectiveStores)
{
<button type="submit" class="btn btn-link p-0 me-4" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">select specific stores...</button>
}
@if (Model.PermissionValues[i].Forbidden)
{
<br/>
<span class="text-danger">
This permission is not available for your account.
</span>
}
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
<span class="form-text text-muted">@Model.PermissionValues[i].Description</span>
</div>
</div> </div>
} }
else if (Model.SelectiveStores)
@for (int i = 0; i < Model.PermissionValues.Count; i++)
{ {
<input type="hidden" asp-for="PermissionValues[i].Forbidden"/> <div class="list-group-item ">
<input type="hidden" asp-for="PermissionValues[i].Permission"/> <h5 class="mb-1">@Model.PermissionValues[i].Title</h5>
<input type="hidden" asp-for="PermissionValues[i].StoreMode" value="@Model.PermissionValues[i].StoreMode"/> <span class="form-text text-muted">@Model.PermissionValues[i].Description</span>
@if (Model.PermissionValues[i].Forbidden && !Model.Strict) <button type="submit" class="btn btn-link" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">Give permission to all stores instead</button>
</div>
@if (!Model.Stores.Any())
{ {
continue; <div class="list-group-item alert-warning">
You currently have no stores configured.
</div>
} }
@if (Policies.IsStorePolicy(Model.PermissionValues[i].Permission)) @for (var index = 0; index < Model.PermissionValues[i].SpecificStores.Count; index++)
{ {
@if (Model.PermissionValues[i].StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) <div class="list-group-item transaction-output-form p-0 ps-lg-2">
{ <div class="row">
<div class="list-group-item form-group"> <div class="col-sm-12 col-md-12 col-lg-10 py-2">
<div class="form-check d-flex"> <div class="form-group my-0">
@if (Model.Strict || Model.PermissionValues[i].Forbidden) @if (Model.PermissionValues[i].SpecificStores[index] == null)
{ {
<input id="@Model.PermissionValues[i].Permission" type="hidden" asp-for="PermissionValues[i].Value"/> <select asp-for="PermissionValues[i].SpecificStores[index]" class="form-select" asp-items="@(new SelectList(Model.Stores.Where(data => !Model.PermissionValues[i].SpecificStores.Contains(data.Id)), nameof(StoreData.Id), nameof(StoreData.StoreName)))"></select>
<input type="checkbox" checked="@Model.PermissionValues[i].Value" disabled class="form-check-input"/> }
} else
else {
{ var store = Model.Stores.SingleOrDefault(data => data.Id == Model.PermissionValues[i].SpecificStores[index]);
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input"/> <select asp-for="PermissionValues[i].SpecificStores[index]" class="form-select" asp-items="@(new SelectList(new[] {store}, nameof(StoreData.Id), nameof(StoreData.StoreName), store.Id))"></select>
} }
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label m-0 me-4 ms-2">@Model.PermissionValues[i].Title</label>
@if (Model.SelectiveStores)
{
<button type="submit" class="btn btn-link p-0 me-4" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">select specific stores...</button>
}
@if (Model.PermissionValues[i].Forbidden) <span asp-validation-for="PermissionValues[i].SpecificStores[index]" class="text-danger"></span>
{
<br/>
<span class="text-danger">
This permission is not available for your account.
</span>
}
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
<span class="form-text text-muted">@Model.PermissionValues[i].Description</span>
</div>
</div>
}
else if (Model.SelectiveStores)
{
<div class="list-group-item ">
<h5 class="mb-1">@Model.PermissionValues[i].Title</h5>
<span class="form-text text-muted">@Model.PermissionValues[i].Description</span>
<button type="submit" class="btn btn-link" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">Give permission to all stores instead</button>
</div>
@if (!Model.Stores.Any())
{
<div class="list-group-item alert-warning">
You currently have no stores configured.
</div>
}
@for (var index = 0; index < Model.PermissionValues[i].SpecificStores.Count; index++)
{
<div class="list-group-item transaction-output-form p-0 ps-lg-2">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-10 py-2">
<div class="form-group my-0">
@if (Model.PermissionValues[i].SpecificStores[index] == null)
{
<select asp-for="PermissionValues[i].SpecificStores[index]" class="form-select" asp-items="@(new SelectList(Model.Stores.Where(data => !Model.PermissionValues[i].SpecificStores.Contains(data.Id)), nameof(StoreData.Id), nameof(StoreData.StoreName)))"></select>
}
else
{
var store = Model.Stores.SingleOrDefault(data => data.Id == Model.PermissionValues[i].SpecificStores[index]);
<select asp-for="PermissionValues[i].SpecificStores[index]" class="form-select" asp-items="@(new SelectList(new[] {store}, nameof(StoreData.Id), nameof(StoreData.StoreName), store.Id))"></select>
}
<span asp-validation-for="PermissionValues[i].SpecificStores[index]" class="text-danger"></span>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
<button type="submit" title="Remove Store Permission" name="command" value="@($"{Model.PermissionValues[i].Permission}:remove-store:{index}")"
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ms-2">
Remove
</button>
<button type="submit" title="Remove Store Permission" name="command" value="@($"{Model.PermissionValues[i].Permission}:remove-store:{index}")"
class="d-none d-lg-block remove-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
</button>
</div>
</div> </div>
</div> </div>
} <div class="col-sm-12 col-md-12 col-lg-2 pull-right">
@if (Model.PermissionValues[i].SpecificStores.Count < Model.Stores.Length) <button type="submit" title="Remove Store Permission" name="command" value="@($"{Model.PermissionValues[i].Permission}:remove-store:{index}")"
{ class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ms-2">
<div class="list-group-item"> Remove
<button type="submit" name="command" value="@($"{Model.PermissionValues[i].Permission}:add-store")" class="ms-1 btn btn-secondary">Add another store </button> </button>
<button type="submit" title="Remove Store Permission" name="command" value="@($"{Model.PermissionValues[i].Permission}:remove-store:{index}")"
class="d-none d-lg-block remove-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
</button>
</div> </div>
}
}
}
else
{
<div class="list-group-item form-group">
<div class="form-check">
@if (Model.Strict || Model.PermissionValues[i].Forbidden)
{
<input id="@Model.PermissionValues[i].Permission" type="hidden" asp-for="PermissionValues[i].Value"/>
<input type="checkbox" checked="@Model.PermissionValues[i].Value" disabled class="form-check-input"/>
}
else
{
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input"/>
}
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label">@Model.PermissionValues[i].Title</label>
@if (Model.PermissionValues[i].Forbidden)
{
<br/>
<span class="text-danger">
This permission is not available for your account.
</span>
}
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
<span class="form-text text-muted">@Model.PermissionValues[i].Description</span>
</div> </div>
</div> </div>
} }
@if (Model.PermissionValues[i].SpecificStores.Count < Model.Stores.Length)
{
<div class="list-group-item">
<button type="submit" name="command" value="@($"{Model.PermissionValues[i].Permission}:add-store")" class="ms-1 btn btn-secondary">Add another store </button>
</div>
}
} }
</div> }
</div> else
<div class="row my-4"> {
<div class="col-lg-12 text-center"> <div class="list-group-item form-group">
<button class="btn btn-primary mx-2" name="command" id="consent-yes" type="submit" value="Authorize">Authorize app</button> <div class="form-check">
<button class="btn btn-secondary mx-2" name="command" id="consent-no" type="submit" value="Cancel">Cancel</button> @if (Model.Strict || Model.PermissionValues[i].Forbidden)
</div> {
</div> <input id="@Model.PermissionValues[i].Permission" type="hidden" asp-for="PermissionValues[i].Value"/>
<input type="checkbox" checked="@Model.PermissionValues[i].Value" disabled class="form-check-input"/>
}
else
{
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input"/>
}
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label">@Model.PermissionValues[i].Title</label>
@if (Model.PermissionValues[i].Forbidden)
{
<br/>
<span class="text-danger">
This permission is not available for your account.
</span>
}
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
<span class="form-text text-muted">@Model.PermissionValues[i].Description</span>
</div>
</div>
}
}
</div> </div>
</section> </div>
<div class="row my-4">
<div class="col-lg-12 text-center">
<button class="btn btn-primary mx-2" name="command" id="consent-yes" type="submit" value="Authorize">Authorize app</button>
<button class="btn btn-secondary mx-2" name="command" id="consent-no" type="submit" value="Cancel">Cancel</button>
</div>
</div>
</form> </form>

View File

@@ -1,9 +1,9 @@
@inject SignInManager<ApplicationUser> SignInManager @inject SignInManager<ApplicationUser> SignInManager
<nav id="sideNav" class="nav flex-column mb-4"> <nav id="SectionNav" class="nav">
<a id="@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-controller="Manage" asp-action="Index">Profile</a> <a id="SectionNav-@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-controller="Manage" asp-action="Index">Profile</a>
<a id="@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-controller="Manage" asp-action="ChangePassword">Password</a> <a id="SectionNav-@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-controller="Manage" asp-action="ChangePassword">Password</a>
<a id="@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="Manage" asp-action="TwoFactorAuthentication">Two-Factor Authentication</a> <a id="SectionNav-@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="Manage" asp-action="TwoFactorAuthentication">Two-Factor Authentication</a>
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="Manage" asp-action="APIKeys">API Keys</a> <a id="SectionNav-@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="Manage" asp-action="APIKeys">API Keys</a>
<a id="@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="Manage" asp-action="NotificationSettings">Notifications</a> <a id="SectionNav-@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="Manage" asp-action="NotificationSettings">Notifications</a>
<vc:ui-extension-point location="user-nav" model="@Model"/> <vc:ui-extension-point location="user-nav" model="@Model"/>
</nav> </nav>

View File

@@ -3,99 +3,95 @@
ViewData["Title"] = "Notifications"; ViewData["Title"] = "Notifications";
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2"> <div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
<h2>@ViewData["Title"]</h2> <h2>@ViewData["Title"]</h2>
<a id="NotificationSettings" asp-controller="Manage" asp-action="NotificationSettings" class="btn btn-secondary"> <a id="NotificationSettings" asp-controller="Manage" asp-action="NotificationSettings" class="btn btn-secondary">
<span class="fa fa-cog"></span> <span class="fa fa-cog"></span>
Settings Settings
</a> </a>
</div>
@if (Model.Total > 0)
{
<form method="post" asp-action="MassAction">
<div class="row button-row">
<div class="col-lg-6">
<span class="dropdown" style="display:none;" id="MassAction">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu">
<button type="submit" class="dropdown-item" name="command" value="mark-seen"><i class="fa fa-eye"></i> Mark seen</button>
<button type="submit" class="dropdown-item" name="command" value="mark-unseen"><i class="fa fa-eye-slash"></i> Mark unseen</button>
<button type="submit" class="dropdown-item" name="command" value="delete"><i class="fa fa-trash-o"></i> Delete</button>
</div>
</span>
</div>
</div> </div>
@if (Model.Total > 0) <div class="row">
{ <div class="col-lg-12">
<form method="post" asp-action="MassAction"> <table class="table table-hover table-responsive-md">
<div class="row button-row"> <thead>
<div class="col-lg-6"> <tr>
<span class="dropdown" style="display:none;" id="MassAction"> <th width="30px" class="only-for-js">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> @if (Model.Total > 0)
Actions
</button>
<div class="dropdown-menu">
<button type="submit" class="dropdown-item" name="command" value="mark-seen"><i class="fa fa-eye"></i> Mark seen</button>
<button type="submit" class="dropdown-item" name="command" value="mark-unseen"><i class="fa fa-eye-slash"></i> Mark unseen</button>
<button type="submit" class="dropdown-item" name="command" value="delete"><i class="fa fa-trash-o"></i> Delete</button>
</div>
</span>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th width="30px" class="only-for-js">
@if (Model.Total > 0)
{
<input name="selectedItems" id="selectAllCheckbox" type="checkbox" class="form-check-input" />
}
</th>
<th width="190px">
Date
<a href="#" id="switchTimeFormat">
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th>Message</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{ {
<tr data-guid="@item.Id" class="notification-row @(item.Seen ? "seen" : "")"> <input name="selectedItems" id="selectAllCheckbox" type="checkbox" class="form-check-input" />
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@item.Id" />
</td>
<td class="toggleRowCheckbox">
<span class="switchTimeFormat" data-switch="@item.Created.ToTimeAgo()">
@item.Created.ToBrowserDate()
</span>
</td>
<td class="toggleRowCheckbox">
@item.Body
</td>
<td class="text-end fw-normal">
@if (!String.IsNullOrEmpty(item.ActionLink))
{
<a href="@item.ActionLink" class="btn btn-link p-0" rel="noreferrer noopener">Details</a>
<span class="d-none d-md-inline-block"> - </span>
}
<button class="btn btn-link p-0 btn-toggle-seen" type="submit" name="command" value="flip-individual:@(item.Id)">
<span>Mark&nbsp;</span><span class="seen-text"></span>
</button>
</td>
</tr>
} }
</tbody> </th>
</table> <th width="190px">
Date
<a href="#" id="switchTimeFormat">
<span class="fa fa-clock-o" title="Switch date format"></span>
</a>
</th>
<th>Message</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr data-guid="@item.Id" class="notification-row @(item.Seen ? "seen" : "")">
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@item.Id" />
</td>
<td class="toggleRowCheckbox">
<span class="switchTimeFormat" data-switch="@item.Created.ToTimeAgo()">
@item.Created.ToBrowserDate()
</span>
</td>
<td class="toggleRowCheckbox">
@item.Body
</td>
<td class="text-end fw-normal">
@if (!String.IsNullOrEmpty(item.ActionLink))
{
<a href="@item.ActionLink" class="btn btn-link p-0" rel="noreferrer noopener">Details</a>
<span class="d-none d-md-inline-block"> - </span>
}
<button class="btn btn-link p-0 btn-toggle-seen" type="submit" name="command" value="flip-individual:@(item.Id)">
<span>Mark&nbsp;</span><span class="seen-text"></span>
</button>
</td>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model" /> <vc:pager view-model="Model" />
</div> </div>
</div> </div>
</form> </form>
} }
else else
{ {
<p class="text-secondary mt-3"> <p class="text-secondary mt-3">
There are no notifications. There are no notifications.
</p> </p>
} }
</div>
</section>
<style> <style>
.notification-row.loading { .notification-row.loading {

View File

@@ -15,134 +15,114 @@
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js" asp-append-version="true"></bundle> <bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js" asp-append-version="true"></bundle>
} }
<section> <partial name="_StatusMessage" />
<div class="container"> <h2 class="mb-4">@ViewData["Title"]</h2>
<partial name="_StatusMessage" /> <form method="post" action="@Url.Action("EditPaymentRequest", "PaymentRequest", new { storeId = Model.StoreId, payReqId = Model.Id }, Context.Request.Scheme)">
<h2 class="mb-4">@ViewData["Title"]</h2> <div class="row">
<form method="post" action="@Url.Action("EditPaymentRequest", "PaymentRequest", new { id = Model.Id}, Context.Request.Scheme)"> <div class="col-lg-6">
<div class="row"> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="col-lg-6"> <div class="form-group">
<input type="hidden" name="Id" value="@Model.Id" /> <label asp-for="Title" class="form-label" data-required></label>
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Title" class="form-control" required />
<div class="form-group"> <span asp-validation-for="Title" class="text-danger"></span>
<label asp-for="Stores" class="form-label"></label> </div>
@if (string.IsNullOrEmpty(Model.Id)) <div class="form-group">
{ <label asp-for="Amount" class="form-label" data-required></label>
<select asp-for="StoreId" asp-items="Model.Stores" class="form-select"></select> <input type="number" step="any" asp-for="Amount" class="form-control" required />
} <span asp-validation-for="Amount" class="text-danger"></span>
else </div>
{ <div class="form-group">
<input type="hidden" asp-for="StoreId" value="@Model.StoreId" /> <label asp-for="Currency" class="form-label" data-required></label>
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly /> <input asp-for="Currency" class="form-control" required />
} <span asp-validation-for="Currency" class="text-danger"></span>
<span asp-validation-for="StoreId" class="text-danger"></span> </div>
</div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input type="email" asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
<p id="PaymentRequestEmailHelpBlock" class="form-text text-muted">
Receive updates for this payment request.
</p>
</div>
<h4 class="mt-5 mb-4">Request Details</h4>
<div class="form-group">
<label asp-for="Title" class="form-label" data-required></label>
<input asp-for="Title" class="form-control" required />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group"> <div class="form-group form-check">
<label asp-for="Amount" class="form-label" data-required></label> <input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check-input" />
<input type="number" step="any" asp-for="Amount" class="form-control" required /> <label asp-for="AllowCustomPaymentAmounts" class="form-check-label"></label>
<span asp-validation-for="Amount" class="text-danger"></span> <span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="Currency" class="form-label" data-required></label> <label asp-for="ExpiryDate" class="form-label"></label>
<input asp-for="Currency" class="form-control" required /> <div class="input-group ">
<span asp-validation-for="Currency" class="text-danger"></span> <input asp-for="ExpiryDate"
</div> value="@(Model.ExpiryDate?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker" min="today" placeholder="No expiry date has been set for this payment request" />
<div class="form-group form-check"> <button id="ClearExpiryDate" class="btn btn-secondary input-group-clear" type="button" title="Clear">
<input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check-input" /> <span class="fa fa-times"></span>
<label asp-for="AllowCustomPaymentAmounts" class="form-check-label"></label> </button>
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ExpiryDate" class="form-label"></label>
<div class="input-group ">
<input asp-for="ExpiryDate"
value="@(Model.ExpiryDate?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker" min="today" placeholder="No expiry date has been set for this payment request" />
<button id="ClearExpiryDate" class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class="fa fa-times"></span>
</button>
</div>
<span asp-validation-for="ExpiryDate" class="text-danger"></span>
</div>
</div> </div>
<div class="col-lg-9"> <span asp-validation-for="ExpiryDate" class="text-danger"></span>
<div class="form-group"> </div>
<label asp-for="Description" class="form-label"></label> <div class="form-group">
<textarea asp-for="Description" class="form-control richtext"></textarea> <label asp-for="Email" class="form-label"></label>
<span asp-validation-for="Description" class="text-danger"></span> <input type="email" asp-for="Email" class="form-control" />
</div> <span asp-validation-for="Email" class="text-danger"></span>
<p id="PaymentRequestEmailHelpBlock" class="form-text text-muted">
Receive updates for this payment request.
</p>
</div>
</div>
<div class="col-lg-9">
<div class="form-group">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control richtext"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<h4 class="mt-5 mb-2">Additional Options</h4> <h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group"> <div class="form-group">
<div class="accordion" id="additional"> <div class="accordion" id="additional">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header"> <h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS Custom CSS
<vc:icon symbol="caret-down" /> <vc:icon symbol="caret-down" />
</button> </button>
</h2> </h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header"> <div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body"> <div class="accordion-body">
<div class="form-group"> <div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label> <label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener"> <a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span> <span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a> </a>
<input asp-for="CustomCSSLink" class="form-control" /> <input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span> <span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="EmbeddedCSS" class="form-label"></label> <label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea> <textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span> <span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group mt-4">
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
@if (!string.IsNullOrEmpty(Model.Id))
{
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" asp-route-id="@Context.GetRouteValue("id")" id="@Model.Id" name="ViewAppButton">View</a>
<a class="btn btn-secondary"
target="_blank"
asp-action="ListInvoices"
asp-controller="Invoice"
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
<a class="btn btn-secondary" asp-route-id="@Context.GetRouteValue("id")" asp-action="ClonePaymentRequest" id="@Model.Id">Clone</a>
@if (!Model.Archived)
{
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Archive this payment request so that it does not appear in the payment request list by default" asp-route-id="@Context.GetRouteValue("id")" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival">Archive</a>
}
else
{
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Unarchive this payment request" asp-route-id="@Context.GetRouteValue("id")" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival">Unarchive</a>
}
}
<a asp-action="GetPaymentRequests" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</div> </div>
</div> </div>
</form>
<div class="form-group mt-4">
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
@if (!string.IsNullOrEmpty(Model.Id))
{
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" asp-route-payReqId="@Model.Id" id="ViewAppButton">View</a>
<a class="btn btn-secondary"
target="_blank"
asp-action="ListInvoices"
asp-controller="Invoice"
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
<a class="btn btn-secondary" asp-route-payReqId="@Model.Id" asp-action="ClonePaymentRequest" id="@Model.Id">Clone</a>
@if (!Model.Archived)
{
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Archive this payment request so that it does not appear in the payment request list by default" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id">Archive</a>
}
else
{
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Unarchive this payment request" asp-controller="PaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id">Unarchive</a>
}
}
<a asp-action="GetPaymentRequests" asp-route-storeId="@Model.StoreId" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</div>
</div> </div>
</section> </form>

View File

@@ -4,98 +4,95 @@
Layout = "_Layout"; Layout = "_Layout";
ViewData["Title"] = "Payment Requests"; ViewData["Title"] = "Payment Requests";
} }
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-4"> <partial name="_StatusMessage" />
<h2 class="mb-0">
@ViewData["Title"] <div class="d-sm-flex align-items-center justify-content-between mb-4">
<small> <h2 class="mb-0">
<a href="https://docs.btcpayserver.org/PaymentRequests/" class="ms-1" target="_blank" rel="noreferrer noopener"> @ViewData["Title"]
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span> <small>
</a> <a href="https://docs.btcpayserver.org/PaymentRequests/" class="ms-1" target="_blank" rel="noreferrer noopener">
</small> <span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</h2>
<a asp-action="EditPaymentRequest" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreatePaymentRequest">
<span class="fa fa-plus"></span>
Create a payment request
</a> </a>
</div> </small>
</h2>
<a asp-action="EditPaymentRequest" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreatePaymentRequest">
<span class="fa fa-plus"></span>
Create a payment request
</a>
</div>
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 mb-3 ms-auto"> <div class="col-12 col-lg-6 mb-3 ms-auto">
<form asp-action="GetPaymentRequests" method="get"> <form asp-action="GetPaymentRequests" method="get">
<input type="hidden" asp-for="Count"/> <input type="hidden" asp-for="Count"/>
<input type="hidden" asp-for="TimezoneOffset" /> <input type="hidden" asp-for="TimezoneOffset" />
<div class="input-group"> <div class="input-group">
<input asp-for="SearchTerm" class="form-control" style="width:300px;"/> <input asp-for="SearchTerm" class="form-control" style="width:300px;"/>
<button type="submit" class="btn btn-secondary" title="Search invoice"> <button type="submit" class="btn btn-secondary" title="Search invoice">
<span class="fa fa-search"></span> Search <span class="fa fa-search"></span> Search
</button> </button>
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
<div class="dropdown-menu dropdown-menu-end"> <div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" asp-action="GetPaymentRequests" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true">Include Archived Payment Reqs</a> <a class="dropdown-item" asp-action="GetPaymentRequests" asp-route-storeId="@Context.GetStoreData().Id" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true">Include Archived Payment Reqs</a>
<div role="separator" class="dropdown-divider"></div> <div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="?searchTerm=">Unfiltered</a> <a class="dropdown-item" href="?searchTerm=">Unfiltered</a>
</div> </div>
</div>
<span asp-validation-for="SearchTerm" class="text-danger"></span>
</form>
</div> </div>
</div> <span asp-validation-for="SearchTerm" class="text-danger"></span>
</form>
<div class="row">
<div class="col-lg-12">
@if (Model.Total > 0)
{
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th>Title</th>
<th>Expiry</th>
<th class="text-end">Price</th>
<th class="text-end">Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
<td class="text-end">@item.Amount @item.Currency</td>
<td class="text-end">@item.Status</td>
<td class="text-end">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
<span> - </span>
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-id="@item.Id">Clone</a>
<span> - </span>
<a asp-action="TogglePaymentRequestArchival" asp-route-id="@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a>
</td>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model"></vc:pager>
}
else
{
<p class="text-secondary mt-3">
There are no payment requests matching your criteria.
</p>
}
</div>
</div>
</div> </div>
</section> </div>
<div class="row">
<div class="col-lg-12">
@if (Model.Total > 0)
{
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th>Title</th>
<th>Expiry</th>
<th class="text-end">Price</th>
<th class="text-end">Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
<td class="text-end">@item.Amount @item.Currency</td>
<td class="text-end">@item.Status</td>
<td class="text-end">
<a asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-payReqId="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-controller="Invoice" asp-action="ListInvoices" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-payReqId="@item.Id">Pay</a>
<span> - </span>
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id">Clone</a>
<span> - </span>
<a asp-action="TogglePaymentRequestArchival" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a>
</td>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model" />
}
else
{
<p class="text-secondary mt-3">
There are no payment requests matching your criteria.
</p>
}
</div>
</div>

View File

@@ -70,7 +70,7 @@
</head> </head>
<body> <body>
<div id="app" class="min-vh-100 d-flex flex-column"> <div id="app" class="min-vh-100 d-flex flex-column">
<nav id="mainNav" class="navbar sticky-top py-3 py-lg-4 d-print-block"> <nav class="btcpay-header navbar sticky-top py-3 py-lg-4 d-print-block">
<div class="container"> <div class="container">
<div class="row align-items-center" style="width:calc(100% + 30px)"> <div class="row align-items-center" style="width:calc(100% + 30px)">
<div class="col-12 col-md-8 col-lg-9"> <div class="col-12 col-md-8 col-lg-9">
@@ -107,7 +107,7 @@
{ {
@if (Model.AllowCustomPaymentAmounts && !Model.AnyPendingInvoice) @if (Model.AllowCustomPaymentAmounts && !Model.AnyPendingInvoice)
{ {
<form method="get" asp-action="PayPaymentRequest" asp-route-id="@Model.Id" class="d-print-none"> <form method="get" asp-action="PayPaymentRequest" asp-route-payReqId="@Model.Id" class="d-print-none">
<div class="row"> <div class="row">
<div class="col col-12 col-sm-6 col-md-12"> <div class="col col-12 col-sm-6 col-md-12">
<div class="input-group"> <div class="input-group">
@@ -123,12 +123,12 @@
} }
else else
{ {
<a class="btn btn-primary d-inline-block d-print-none w-100 text-nowrap @if (!(Model.AnyPendingInvoice && !Model.PendingInvoiceHasPayments)) { @("btn-lg") }" asp-action="PayPaymentRequest" asp-route-id="@Model.Id" data-test="pay-button"> <a class="btn btn-primary d-inline-block d-print-none w-100 text-nowrap @if (!(Model.AnyPendingInvoice && !Model.PendingInvoiceHasPayments)) { @("btn-lg") }" asp-action="PayPaymentRequest" asp-route-payReqId="@Model.Id" data-test="pay-button">
Pay Invoice Pay Invoice
</a> </a>
if (Model.AnyPendingInvoice && !Model.PendingInvoiceHasPayments && Model.AllowCustomPaymentAmounts) if (Model.AnyPendingInvoice && !Model.PendingInvoiceHasPayments && Model.AllowCustomPaymentAmounts)
{ {
<form method="get" asp-action="CancelUnpaidPendingInvoice" asp-route-id="@Model.Id" class="mt-2 d-print-none"> <form method="get" asp-action="CancelUnpaidPendingInvoice" asp-route-payReqId="@Model.Id" class="mt-2 d-print-none">
<button class="btn btn-outline-secondary w-100 text-nowrap" type="submit">Cancel Invoice</button> <button class="btn btn-outline-secondary w-100 text-nowrap" type="submit">Cancel Invoice</button>
</form> </form>
} }

View File

@@ -2,19 +2,16 @@
var allErrors = ViewData.ModelState.Values.SelectMany(v => v.Errors.Select(b => b.ErrorMessage)); var allErrors = ViewData.ModelState.Values.SelectMany(v => v.Errors.Select(b => b.ErrorMessage));
} }
<section>
<div class="container"> <div class="row">
<div class="row"> <div class="col-lg-12">
<div class="col-lg-12"> <h2>Pay Button request failed</h2>
<h2>Pay Button request failed</h2> Please fix following errors:
Please fix following errors: <ul>
<ul> @foreach (var error in allErrors)
@foreach (var error in allErrors) {
{ <li>@error</li>
<li>@error</li> }
} </ul>
</ul>
</div>
</div>
</div> </div>
</section> </div>

View File

@@ -50,7 +50,7 @@
<div class="min-vh-100 d-flex flex-column"> <div class="min-vh-100 d-flex flex-column">
@if (Model.IsPending) @if (Model.IsPending)
{ {
<nav id="mainNav" class="navbar sticky-top py-3 py-lg-4 d-print-none"> <nav class="btcpay-header navbar sticky-top py-3 py-lg-4 d-print-none">
<div class="container"> <div class="container">
<form asp-action="ClaimPullPayment" asp-route-pullPaymentId="@Model.Id" class="w-100"> <form asp-action="ClaimPullPayment" asp-route-pullPaymentId="@Model.Id" class="w-100">
<div class="row align-items-center" style="width:calc(100% + 30px)"> <div class="row align-items-center" style="width:calc(100% + 30px)">

View File

@@ -1,17 +1,18 @@
@using BTCPayServer.Configuration @using BTCPayServer.Configuration
@inject BTCPayServerOptions BTCPayServerOptions @inject BTCPayServerOptions BTCPayServerOptions
<nav id="sideNav" class="nav flex-column mb-4">
<a asp-controller="Server" id="Server-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a> <nav id="SectionNav" class="nav">
<a asp-controller="Server" id="Server-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email Server</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
<a asp-controller="Server" id="Server-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email Server</a>
<a asp-controller="Server" id="Server-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
<a asp-controller="Server" id="Server-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a asp-controller="Server" id="SectionNav-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
@if (BTCPayServerOptions.DockerDeployment) @if (BTCPayServerOptions.DockerDeployment)
{ {
<a asp-controller="Server" id="Server-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
} }
<a asp-controller="Server" id="Server-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
<a asp-controller="Server" id="Server-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
<a asp-controller="Server" id="Server-@ServerNavPages.Plugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" asp-action="ListPlugins">Plugins (experimental)</a> <a asp-controller="Server" id="SectionNav-@ServerNavPages.Plugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" asp-action="ListPlugins">Plugins (experimental)</a>
<vc:ui-extension-point location="server-nav" model="@Model"/> <vc:ui-extension-point location="server-nav" model="@Model"/>
</nav> </nav>

View File

@@ -1,224 +1,129 @@
@using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.Invoice
@using BTCPayServer.Views.Manage
@using BTCPayServer.Views.PaymentRequest
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Contracts @using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Extensions @inject BTCPayServer.Services.BTCPayServerEnvironment _env
@inject SignInManager<ApplicationUser> SignInManager @inject UserManager<ApplicationUser> _userManager
@inject UserManager<ApplicationUser> UserManager @inject ISettingsRepository _settingsRepository
@inject RoleManager<IdentityRole> RoleManager @inject LinkGenerator _linkGenerator
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@inject ISettingsRepository SettingsRepository
@inject LinkGenerator linkGenerator
@{ @{
var theme = await SettingsRepository.GetTheme(); var logoSrc = $"{ViewContext.HttpContext.Request.PathBase}/img/logo.svg";
} var notificationDisabled = (await _settingsRepository.GetPolicies()).DisableInstantNotifications;
if (!notificationDisabled)
@functions {
// The .NET function for inserting classes requires this to be async
// ReSharper disable once CSharpWarnings::CS1998
#pragma warning disable CS1998
private async Task Logo(string classes = "")
{ {
<a href="~/" class="navbar-brand py-2 js-scroll-trigger @classes"> var user = await _userManager.GetUserAsync(User);
<svg class="logo" viewBox="0 0 192 84" xmlns="http://www.w3.org/2000/svg"><g><path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/><path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/><path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/><path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/><path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/><path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" class="logo-brand-text"/></g></svg> notificationDisabled = user?.DisabledNotifications == "all";
@if (Env.NetworkType != NBitcoin.ChainName.Mainnet)
{
<span class="badge bg-warning" style="font-size:10px;">@Env.NetworkType.ToString()</span>
}
</a>
} }
#pragma warning restore CS1998
} }
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"@(Env.IsDeveloping ? " data-devenv" : "")> <html lang="en"@(_env.IsDeveloping ? " data-devenv" : "")>
<head> <head>
<partial name="LayoutHead" /> <partial name="LayoutHead" />
@await RenderSectionAsync("PageHeadContent", false) @await RenderSectionAsync("PageHeadContent", false)
</head> </head>
<body id="page-top"> <body class="d-flex flex-column flex-lg-row min-vh-100">
@{ <header id="mainMenu" class="btcpay-header d-flex flex-column">
if (ViewBag.AlwaysShrinkNavBar == null) <div id="mainMenuHead" class="d-flex flex-lg-wrap align-items-center justify-content-between py-2 px-3 py-lg-3 px-lg-4">
{ <a href="~/" class="navbar-brand py-2 js-scroll-trigger">
ViewBag.AlwaysShrinkNavBar = true; <svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="logo"><use href="@logoSrc#small" class="logo-small" /><use href="@logoSrc#large" class="logo-large" /></svg>
} @if (_env.NetworkType != NBitcoin.ChainName.Mainnet)
var additionalStyle = ViewBag.AlwaysShrinkNavBar ? "navbar-shrink always-shrinked" : "";
}
<!-- Navigation -->
<nav class="navbar navbar-expand-lg fixed-top py-2 @additionalStyle" id="mainNav">
<div class="container">
@* Logo on Mobile *@
@{ await Logo("d-lg-none"); }
<button class="navbar-toggler navbar-toggler-right border-0 shadow-none p-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<svg class="navbar-toggler-icon" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-miterlimit="10" d="M4 7h22M4 15h22M4 23h22"/></svg>
</button>
<div id="navbarResponsive" class="offcanvas offcanvas-fade border-0" data-bs-scroll="true" tabindex="-1">
<div class="container">
<div class="offcanvas-header">
<div class="offcanvas-title" id="offcanvasLabel">
@* Logo in Offcanvas *@
@{ await Logo(); }
</div>
<button type="button" class="btn-close shadow-none m-0" data-bs-dismiss="offcanvas" aria-label="Close" style="padding:.8125rem;">
<vc:icon symbol="close"/>
</button>
</div>
<div class="offcanvas-body d-lg-flex w-100 align-items-center justify-content-between">
@* Logo on Desktop *@
@{ await Logo("d-none d-lg-inline-block"); }
@if (SignInManager.IsSignedIn(User))
{
<ul class="navbar-nav">
@if (User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(ServerNavPages))" id="ServerSettings">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(StoreNavPages))" id="Stores">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(AppsNavPages))" id="Apps">Apps</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(WalletsNavPages))" id="Wallets">Wallets</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(InvoiceNavPages))" id="Invoices">Invoices</a></li>
<li class="nav-item"><a asp-area="" asp-controller="PaymentRequest" asp-action="GetPaymentRequests" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="PaymentRequests">Payment Requests</a></li>
<vc:ui-extension-point location="header-nav" model="@Model"/>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="My settings" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="MySettings"><span class="d-lg-none d-sm-block">Account</span><i class="fa fa-user d-lg-inline-block d-none"></i></a>
</li>
<vc:notifications-dropdown/>
@if (!theme.CustomTheme)
{
<li class="nav-item">
<vc:theme-switch responsive="lg" css-class="nav-link" />
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Logout" class="nav-link js-scroll-trigger" id="Logout"><span class="d-lg-none d-sm-block">Logout</span><i class="fa fa-sign-out d-lg-inline-block d-none"></i></a>
</li>
</ul>
}
else if (Env.IsSecure)
{
<ul class="navbar-nav">
@if (!(await SettingsRepository.GetPolicies()).LockSubscription)
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger" id="Register">Register</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger" id="Login">Log in</a></li>
<vc:ui-extension-point location="header-nav" model="@Model"/>
</ul>
}
</div>
</div>
</div>
<div id="badUrl" class="alert alert-danger alert-dismissible" style="display:none; position:absolute; top:75px;" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close" />
</button>
<span>BTCPay is expecting you to access this website from <b>@(Env.ExpectedProtocol)://@(Env.ExpectedHost)/</b>. If you use a reverse proxy, please set the <b>X-Forwarded-Proto</b> header to <b id="browserScheme">@(Env.ExpectedProtocol)</b> (<a href="https://docs.btcpayserver.org/FAQ/Deployment/#cause-3-btcpay-is-expecting-you-to-access-this-website-from" target="_blank" class="alert-link" rel="noreferrer noopener">More information</a>)</span>
</div>
@if (!Env.IsSecure)
{ {
<div id="insecureEnv" class="alert alert-danger alert-dismissible" style="position:absolute; top:75px;" role="alert"> <span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@_env.NetworkType.ToString()</span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close" />
</button>
<span>
Your access to BTCPay Server is over an unsecured network. If you are using the docker deployment method with NGINX and HTTPS is not available, you probably did not configure your DNS settings correctly. <br/>
We disabled the register and login link so you don't leak your credentials.
</span>
</div>
} }
</a>
<vc:store-selector />
<vc:notifications-dropdown />
<button id="mainMenuToggle" class="mainMenuButton" type="button" data-bs-toggle="offcanvas" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span>Menu</span>
</button>
</div>
<vc:main-nav />
</header>
<main id="mainContent">
@if (_env.Context.Request.Host.ToString() != _env.ExpectedHost || _env.Context.Request.Scheme != _env.ExpectedProtocol)
{
<div id="badUrl" class="alert alert-danger alert-dismissible m-3" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close"/>
</button>
<span>
BTCPay is expecting you to access this website from <strong>@(_env.ExpectedProtocol)://@(_env.ExpectedHost)/</strong>.
If you use a reverse proxy, please set the <strong>X-Forwarded-Proto</strong> header to <strong id="browserScheme">@(_env.ExpectedProtocol)</strong>
(<a href="https://docs.btcpayserver.org/FAQ/Deployment/#cause-3-btcpay-is-expecting-you-to-access-this-website-from" target="_blank" class="alert-link" rel="noreferrer noopener">More information</a>)
</span>
</div> </div>
</nav> }
@if (!_env.IsSecure)
@RenderBody() {
<div id="insecureEnv" class="alert alert-danger alert-dismissible" style="position:absolute; top:75px;" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close"/>
</button>
<span>
Your access to BTCPay Server is over an unsecured network. If you are using the docker deployment method with NGINX and HTTPS is not available, you probably did not configure your DNS settings correctly. <br/>
We disabled the register and login link so you don't leak your credentials.
</span>
</div>
}
<section>
<div class="container">
@RenderBody()
</div>
</section>
@if (User.Identity.IsAuthenticated) @if (User.Identity.IsAuthenticated)
{ {
<footer class="btcpay-footer"> <footer class="btcpay-footer">
<div class="container"> <div class="container">
<div class="d-flex flex-column justify-content-between flex-lg-row py-1"> <div class="d-flex flex-column justify-content-between flex-xl-row py-1">
<div class="d-flex justify-content-center justify-content-lg-start mb-2 mb-lg-0"> <div class="d-flex justify-content-center justify-content-xl-start mb-2 mb-xl-0">
<a href="https://github.com/btcpayserver/btcpayserver" class="d-flex align-items-center me-4" target="_blank" rel="noreferrer noopener"> <a href="https://github.com/btcpayserver/btcpayserver" class="d-flex align-items-center me-4" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="github" /> <vc:icon symbol="github"/>
<span style="margin-left:.4rem">Github</span> <span style="margin-left:.4rem">Github</span>
</a> </a>
<a href="https://chat.btcpayserver.org/" class="d-flex align-items-center me-4" target="_blank" rel="noreferrer noopener"> <a href="https://chat.btcpayserver.org/" class="d-flex align-items-center me-4" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="mattermost" /> <vc:icon symbol="mattermost"/>
<span style="margin-left:.4rem">Mattermost</span> <span style="margin-left:.4rem">Mattermost</span>
</a> </a>
<a href="https://twitter.com/BtcpayServer" class="d-flex align-items-center" target="_blank" rel="noreferrer noopener"> <a href="https://twitter.com/BtcpayServer" class="d-flex align-items-center" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="twitter" /> <vc:icon symbol="twitter"/>
<span style="margin-left:.4rem">Twitter</span> <span style="margin-left:.4rem">Twitter</span>
</a> </a>
</div> </div>
<div class="text-center text-lg-start">@Env.ToString()</div> <div class="text-center text-xl-start">@_env.ToString()</div>
</div> </div>
</div> </div>
</footer> </footer>
} }
<partial name="LayoutFoot" /> <partial name="LayoutFoot"/>
@await RenderSectionAsync("PageFootContent", false) @await RenderSectionAsync("PageFootContent", false)
<partial name="LayoutPartials/SyncModal" /> <partial name="LayoutPartials/SyncModal"/>
@{
var notificationDisabled = (await SettingsRepository.GetPolicies()).DisableInstantNotifications;
if (!notificationDisabled)
{
var user = await UserManager.GetUserAsync(User);
notificationDisabled = user?.DisabledNotifications == "all";
}
}
<script type="text/javascript">
const expectedDomain = @Safe.Json(Env.ExpectedHost);
const expectedProtocol = @Safe.Json(Env.ExpectedProtocol);
if (window.location.host !== expectedDomain || window.location.protocol !== expectedProtocol + ":") {
document.getElementById("badUrl").style.display = "block";
document.getElementById("browserScheme").innerText = window.location.protocol.substr(0, window.location.protocol.length -1);
}
</script>
@if (!notificationDisabled) @if (!notificationDisabled)
{ {
<script> <script>
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2; if ('WebSocket' in window && window.WebSocket.CLOSING === 2) {
if (supportsWebSockets) { const { host, protocol } = window.location;
const wsUri = `${protocol === "https:" ? "wss:" : "ws:"}//${host}@_linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")`;
var loc = window.location, ws_uri; const newDataEndpoint = "@_linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
if (loc.protocol === "https:") { try {
ws_uri = "wss:"; socket = new WebSocket(wsUri);
} else { socket.onmessage = e => {
ws_uri = "ws:"; if (e.data === "ping") return;
$.get(newDataEndpoint, data => {
$("#Notifications").replaceWith($(data));
});
};
socket.onerror = e => {
console.error("Error while connecting to websocket for notifications (callback)", e);
};
}
catch (e) {
console.error("Error while connecting to websocket for notifications", e);
}
} }
ws_uri += "//" + loc.host; </script>
ws_uri += "@linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")";
var newDataEndpoint = "@linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
try {
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
if (e.data === "ping")
return;
$.get(newDataEndpoint, function (data) {
$("#notifications-nav-item").replaceWith($(data));
});
};
socket.onerror = function (e) {
console.error("Error while connecting to websocket for notifications (callback)", e);
};
}
catch (e) {
console.error("Error while connecting to websocket for notifications", e);
}
}
</script>
} }
</main>
</body> </body>
</html> </html>

View File

@@ -2,7 +2,6 @@
Layout = "/Views/Shared/_Layout.cshtml"; Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true; ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
ViewBag.ShowMainTitle = ViewBag.ShowMainTitle ?? true; ViewBag.ShowMainTitle = ViewBag.ShowMainTitle ?? true;
ViewBag.ShowBreadcrumb = ViewBag.ShowBreadcrumb ?? false;
if (!ViewData.ContainsKey("NavPartialName")) if (!ViewData.ContainsKey("NavPartialName"))
{ {
ViewData["NavPartialName"] = "_Nav"; ViewData["NavPartialName"] = "_Nav";
@@ -17,42 +16,17 @@
@await RenderSectionAsync("PageFootContent", false) @await RenderSectionAsync("PageFootContent", false)
} }
<section> @if (!string.IsNullOrEmpty(ViewData["MainTitle"] as string))
<div class="container"> {
@if (ViewBag.ShowBreadcrumb) <h2 class="mb-5">@ViewData["MainTitle"]</h2>
{ }
<nav aria-label="breadcrumb" class="mt-n3 mb-4"> @if (ViewBag.ShowMenu)
<ol class="breadcrumb px-0"> {
@if (!string.IsNullOrEmpty(ViewData["CategoryTitle"] as string)) <nav class="nav">
{ <partial name="@ViewData["NavPartialName"].ToString()" />
<li class="breadcrumb-item" aria-current="page">@ViewData["CategoryTitle"]</li> </nav>
} }
@if (!string.IsNullOrEmpty(ViewData["MainTitle"] as string))
{ <partial name="_StatusMessage" />
<li class="breadcrumb-item" aria-current="page">@ViewData["MainTitle"]</li>
} @RenderBody()
@if (!string.IsNullOrEmpty(ViewData["PageTitle"] as string))
{
<li class="breadcrumb-item" aria-current="page">@ViewData["PageTitle"]</li>
}
</ol>
</nav>
}
else if (!string.IsNullOrEmpty(ViewData["MainTitle"] as string))
{
<h2 class="mb-5">@ViewData["MainTitle"]</h2>
}
<div class="row">
@if (ViewBag.ShowMenu)
{
<div class="col-md-3 ms-n3 ms-md-0">
<partial name="@ViewData["NavPartialName"].ToString()" />
</div>
}
<div class="col-md-9">
<partial name="_StatusMessage" />
@RenderBody()
</div>
</div>
</div>
</section>

View File

@@ -58,7 +58,7 @@
{ {
<h4 class="mt-5 mb-3">Other actions</h4> <h4 class="mt-5 mb-3">Other actions</h4>
<div id="danger-zone"> <div id="danger-zone">
<a id="delete-store" class="btn btn-outline-danger mb-5" asp-action="DeleteStore" asp-route-storeId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@Model.StoreName</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.">Delete this store</a> <a id="DeleteStore" class="btn btn-outline-danger mb-5" asp-action="DeleteStore" asp-route-storeId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@Model.StoreName</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store." data-confirm-input="DELETE">Delete this store</a>
</div> </div>
} }
</div> </div>

View File

@@ -1,15 +1,13 @@
@using BTCPayServer.Client @using BTCPayServer.Client
<nav id="sideNav" class="nav flex-column mb-4"> <nav id="SectionNav" class="nav">
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.PaymentMethods))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PaymentMethods)" asp-controller="Stores" asp-action="PaymentMethods" asp-route-storeId="@Context.GetRouteValue("storeId")">Payment Methods</a> <a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.PaymentMethods))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PaymentMethods)" asp-controller="Stores" asp-action="PaymentMethods" asp-route-storeId="@Context.GetRouteValue("storeId")">Payment Methods</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="Stores" 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="Stores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="Stores" 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="Stores" asp-action="CheckoutAppearance" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout Appearance</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.GeneralSettings))" class="nav-link @ViewData.IsActivePage(StoreNavPages.GeneralSettings)" asp-controller="Stores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General Settings</a> <a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.GeneralSettings))" class="nav-link @ViewData.IsActivePage(StoreNavPages.GeneralSettings)" asp-controller="Stores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General Settings</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="Stores" 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="Stores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@Context.GetRouteValue("storeId")">Users</a> <a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@Context.GetRouteValue("storeId")">Users</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@Context.GetRouteValue("storeId")">Pay Button</a> <a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@Context.GetRouteValue("storeId")">Pay Button</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</a> <a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</a>
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@Context.GetRouteValue("storeId")">Webhooks</a> <a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@Context.GetRouteValue("storeId")">Webhooks</a>
<a id="Nav-@(nameof(StoreNavPages.PullPayments))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Pull Payments</a> <vc:ui-extension-point location="store-nav" model="@Model" />
<a id="Nav-@(nameof(StoreNavPages.Payouts))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" asp-action="Payouts" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Payouts</a>
<vc:ui-extension-point location="store-nav" model="@Model"/>
</nav> </nav>

View File

@@ -7,26 +7,22 @@
<partial name="_ValidationScriptsPartial"/> <partial name="_ValidationScriptsPartial"/>
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"]</h2> <h2 class="mb-4">@ViewData["PageTitle"]</h2>
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<form asp-action="CreateStore"> <form asp-action="CreateStore">
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group"> <div class="form-group">
<label asp-for="Name" class="form-label" data-required></label> <label asp-for="Name" class="form-label" data-required></label>
<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 class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
<a asp-action="ListStores" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</form>
</div> </div>
</div> <div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
<a asp-action="ListStores" class="btn btn-link px-0 ms-3">Back to list</a>
</div>
</form>
</div> </div>
</section> </div>

View File

@@ -7,92 +7,89 @@
var sortByAsc = "Sort by ascending..."; var sortByAsc = "Sort by ascending...";
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-2"> <div class="d-sm-flex align-items-center justify-content-between mb-2">
<h2 class="mb-0">@ViewData["PageTitle"]</h2> <h2 class="mb-0">@ViewData["PageTitle"]</h2>
<a asp-action="CreateStore" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateStore"><span class="fa fa-plus"></span> Create a new store</a> <a asp-action="CreateStore" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateStore"><span class="fa fa-plus"></span> Create a new store</a>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
@if (Model.Stores.Any()) @if (Model.Stores.Any())
{ {
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th> <th>
<a asp-action="ListStores" <a asp-action="ListStores"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")" asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName" asp-route-sortOrderColumn="StoreName"
class="text-nowrap" class="text-nowrap"
title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)"> title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)">
Name Name
<span class="fa @(storeNameSortOrder == "asc" ? "fa-sort-alpha-desc" : storeNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" /> <span class="fa @(storeNameSortOrder == "asc" ? "fa-sort-alpha-desc" : storeNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a> </a>
</th> </th>
<th> <th>
<a asp-action="ListStores" <a asp-action="ListStores"
asp-route-sortOrder="@(storeWebsiteSortOrder ?? "asc")" asp-route-sortOrder="@(storeWebsiteSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreWebsite" asp-route-sortOrderColumn="StoreWebsite"
class="text-nowrap" class="text-nowrap"
title="@(storeWebsiteSortOrder == "desc" ? sortByDesc : sortByAsc)"> title="@(storeWebsiteSortOrder == "desc" ? sortByDesc : sortByAsc)">
Website Website
<span class="fa @(storeWebsiteSortOrder == "asc" ? "fa-sort-alpha-desc" : storeWebsiteSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" /> <span class="fa @(storeWebsiteSortOrder == "asc" ? "fa-sort-alpha-desc" : storeWebsiteSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a> </a>
</th> </th>
<th style="text-align:right">Actions</th> <th style="text-align:right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var store in Model.Stores) @foreach (var store in Model.Stores)
{ {
<tr id="store-@store.Id"> <tr id="store-@store.Id">
<td> <td>
@if (store.IsOwner) @if (store.IsOwner)
{ {
<a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@store.Id">@store.Name</a> <a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@store.Id">@store.Name</a>
} }
else else
{ {
@store.Name @store.Name
} }
@if (store.HintWalletWarning) @if (store.HintWalletWarning)
{ {
<span class="fa fa-warning text-warning" title="Wallet not setup for this store" id="warninghint_@store.Id"></span> <span class="fa fa-warning text-warning" title="Wallet not setup for this store" id="warninghint_@store.Id"></span>
} }
</td> </td>
<td> <td>
@if (!string.IsNullOrEmpty(store.WebSite)) @if (!string.IsNullOrEmpty(store.WebSite))
{ {
<a href="@store.WebSite" rel="noreferrer noopener">@store.WebSite</a> <a href="@store.WebSite" rel="noreferrer noopener">@store.WebSite</a>
} }
</td> </td>
<td style="text-align:right"> <td style="text-align:right">
<a asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchTerm="storeid:@store.Id">Invoices</a><span> - </span> <a asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchTerm="storeid:@store.Id">Invoices</a><span> - </span>
@if (store.IsOwner) <a asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@store.Id">Pull Payments</a>
{ @if (store.IsOwner)
<a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@store.Id" id="update-store-@store.Id">Settings</a><span> - </span> {
<a asp-action="DeleteStore" asp-controller="Stores" asp-route-storeId="@store.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@store.Name</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store." data-confirm-input="DELETE">Delete</a><span> - </span> <span> - </span>
} <a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@store.Id" id="update-store-@store.Id">Settings</a><span> - </span>
<a asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@store.Id" >Pull Payments</a> <a asp-action="DeleteStore" asp-controller="Stores" asp-route-storeId="@store.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@store.Name</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store." data-confirm-input="DELETE">Delete</a><span> - </span>
</td> }
</tr> </td>
} </tr>
</tbody> }
</table> </tbody>
} </table>
else }
{ else
<p class="text-secondary mt-3"> {
There are no stores yet. <p class="text-secondary mt-3">
</p> There are no stores yet.
} </p>
</div> }
</div>
</div> </div>
</section> </div>
<partial name="_Confirm" model="@(new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.", "Delete"))" /> <partial name="_Confirm" model="@(new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.", "Delete"))" />

View File

@@ -3,72 +3,68 @@
ViewData.SetActivePageAndTitle(WalletsNavPages.Index, "Wallets"); ViewData.SetActivePageAndTitle(WalletsNavPages.Index, "Wallets");
} }
<section> <partial name="_StatusMessage" />
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex justify-content-between mb-2"> <div class="d-sm-flex justify-content-between mb-2">
<h2 class="mb-0"> <h2 class="mb-0">
@ViewData["Title"] @ViewData["Title"]
<small> <small>
<a href="https://docs.btcpayserver.org/Wallet/" target="_blank" rel="noreferrer noopener"> <a href="https://docs.btcpayserver.org/Wallet/" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span> <span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a> </a>
</small> </small>
</h2> </h2>
@if (Model.Wallets.Any()) @if (Model.Wallets.Any())
{
<div class="mt-2 mt-sm-1">
@foreach (var balance in Model.BalanceForCryptoCode)
{ {
<div class="mt-2 mt-sm-1"> <div class="fs-5 fw-semibold">@($"{balance.Value.ShowMoney(balance.Key)} {balance.Key.CryptoCode}")</div>
@foreach (var balance in Model.BalanceForCryptoCode)
{
<div class="fs-5 fw-semibold">@($"{balance.Value.ShowMoney(balance.Key)} {balance.Key.CryptoCode}")</div>
}
<div class="text-muted text-sm-end fs-6">Total Balance</div>
</div>
} }
<div class="text-muted text-sm-end fs-6">Total Balance</div>
</div> </div>
<div class="row"> }
<div class="col-lg-12"> </div>
@if (Model.Wallets.Any()) <div class="row">
{ <div class="col-lg-12">
<table class="table table-hover table-responsive-md"> @if (Model.Wallets.Any())
<thead> {
<tr> <table class="table table-hover table-responsive-md">
<th>Store Name</th> <thead>
<th>Crypto Code</th> <tr>
<th>Balance</th> <th>Store Name</th>
<th class="text-end">Actions</th> <th>Crypto Code</th>
</tr> <th>Balance</th>
</thead> <th class="text-end">Actions</th>
<tbody> </tr>
@foreach (var wallet in Model.Wallets) </thead>
<tbody>
@foreach (var wallet in Model.Wallets)
{
<tr>
@if (wallet.IsOwner)
{ {
<tr> <td><a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
@if (wallet.IsOwner)
{
<td><a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
}
else
{
<td>@wallet.StoreName</td>
}
<td>@wallet.CryptoCode</td>
<td>@wallet.Balance</td>
<td class="text-end">
<a asp-action="WalletTransactions" asp-route-walletId="@wallet.Id">Manage</a>
</td>
</tr>
} }
</tbody> else
</table> {
} <td>@wallet.StoreName</td>
else }
{ <td>@wallet.CryptoCode</td>
<p class="text-secondary mt-3"> <td>@wallet.Balance</td>
There are no wallets yet. You can add wallets in the store setup. <td class="text-end">
</p> <a asp-action="WalletTransactions" asp-route-walletId="@wallet.Id">Manage</a>
} </td>
</div> </tr>
</div> }
</tbody>
</table>
}
else
{
<p class="text-secondary mt-3">
There are no wallets yet. You can add wallets in the store setup.
</p>
}
</div> </div>
</section> </div>

View File

@@ -4,18 +4,17 @@
var wallet = WalletId.Parse(Context.GetRouteValue("walletId").ToString()); var wallet = WalletId.Parse(Context.GetRouteValue("walletId").ToString());
var network = BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(wallet.CryptoCode); var network = BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(wallet.CryptoCode);
} }
<nav id="sideNav" class="nav flex-column mb-4"> <nav id="SectionNav" class="nav">
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions" asp-route-walletId="@Context.GetRouteValue("walletId")" id="WalletTransactions">Transactions</a> <a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions" asp-route-walletId="@Context.GetRouteValue("walletId")" id="SectionNav-Transactions">Transactions</a>
@if (!network.ReadonlyWallet) @if (!network.ReadonlyWallet)
{ {
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend" asp-route-walletId="@Context.GetRouteValue("walletId")" id="WalletSend">Send</a> <a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend" asp-route-walletId="@Context.GetRouteValue("walletId")" id="SectionNav-Send">Send</a>
} }
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Receive)" asp-action="WalletReceive" asp-route-walletId="@Context.GetRouteValue("walletId")" id="WalletReceive">Receive</a> <a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Receive)" asp-action="WalletReceive" asp-route-walletId="@Context.GetRouteValue("walletId")" id="SectionNav-Receive">Receive</a>
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan" asp-route-walletId="@Context.GetRouteValue("walletId")" id="WalletRescan">Rescan</a> <a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan" asp-route-walletId="@Context.GetRouteValue("walletId")" id="SectionNav-Rescan">Rescan</a>
@if (!network.ReadonlyWallet) @if (!network.ReadonlyWallet)
{ {
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT" asp-route-walletId="@Context.GetRouteValue("walletId")">PSBT</a> <a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT" asp-route-walletId="@Context.GetRouteValue("walletId")" id="SectionNav-PSBT">PSBT</a>
} }
<a class="nav-link" asp-controller="Stores" asp-action="WalletSettings" asp-route-storeId="@wallet.StoreId" asp-route-cryptoCode="@wallet.CryptoCode" id="WalletSettings">Settings</a>
<vc:ui-extension-point location="wallet-nav" model="@Model" /> <vc:ui-extension-point location="wallet-nav" model="@Model" />
</nav> </nav>

View File

@@ -6,6 +6,7 @@
"wwwroot/vendor/font-awesome/css/font-awesome.css", "wwwroot/vendor/font-awesome/css/font-awesome.css",
"wwwroot/vendor/flatpickr/flatpickr.css", "wwwroot/vendor/flatpickr/flatpickr.css",
"wwwroot/main/fonts/OpenSans.css", "wwwroot/main/fonts/OpenSans.css",
"wwwroot/main/layout.css",
"wwwroot/main/site.css" "wwwroot/main/site.css"
] ]
}, },

View File

@@ -1,4 +1,5 @@
delegate('click', '.payment-method', function(e) { delegate('click', '.payment-method', e => {
closePaymentMethodDialog(e.target.dataset.paymentMethod); const el = e.target.closest('.payment-method')
closePaymentMethodDialog(el.dataset.paymentMethod);
return false; return false;
}) })

View File

@@ -14,7 +14,19 @@
<symbol id="scan-qr" viewBox="0 0 32 32"><path d="M20 .875h10c.621 0 1.125.504 1.125 1.125v10m0 8v10c0 .621-.504 1.125-1.125 1.125H20m-8 0H2A1.125 1.125 0 01.875 30V20m0-8V2C.875 1.379 1.379.875 2 .875h10" stroke="currentColor" stroke-width="1.75" fill="none" fill-rule="evenodd"/></symbol> <symbol id="scan-qr" viewBox="0 0 32 32"><path d="M20 .875h10c.621 0 1.125.504 1.125 1.125v10m0 8v10c0 .621-.504 1.125-1.125 1.125H20m-8 0H2A1.125 1.125 0 01.875 30V20m0-8V2C.875 1.379 1.379.875 2 .875h10" stroke="currentColor" stroke-width="1.75" fill="none" fill-rule="evenodd"/></symbol>
<symbol id="seed" viewBox="0 0 32 32"><rect x="0.875" y="2.875" width="30.25" height="26.25" rx="1.125" fill="none" stroke="currentColor" stroke-width="1.75"/><rect x="5" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="21" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="21" width="9" height="4" rx="0.5" fill="currentColor"/></symbol> <symbol id="seed" viewBox="0 0 32 32"><rect x="0.875" y="2.875" width="30.25" height="26.25" rx="1.125" fill="none" stroke="currentColor" stroke-width="1.75"/><rect x="5" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="21" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="21" width="9" height="4" rx="0.5" fill="currentColor"/></symbol>
<symbol id="warning" viewBox="0 0 24 24"><path d="M12.337 3.101a.383.383 0 00-.674 0l-9.32 17.434a.383.383 0 00.338.564h18.638a.384.384 0 00.337-.564L12.337 3.101zM9.636 2.018c1.01-1.89 3.719-1.89 4.728 0l9.32 17.434a2.681 2.681 0 01-2.365 3.945H2.681a2.68 2.68 0 01-2.364-3.945L9.636 2.018zm3.896 15.25a1.532 1.532 0 11-3.064 0 1.532 1.532 0 013.064 0zm-.383-8.044a1.15 1.15 0 00-2.298 0v3.83a1.15 1.15 0 002.298 0v-3.83z" fill="currentColor"/></symbol> <symbol id="warning" viewBox="0 0 24 24"><path d="M12.337 3.101a.383.383 0 00-.674 0l-9.32 17.434a.383.383 0 00.338.564h18.638a.384.384 0 00.337-.564L12.337 3.101zM9.636 2.018c1.01-1.89 3.719-1.89 4.728 0l9.32 17.434a2.681 2.681 0 01-2.365 3.945H2.681a2.68 2.68 0 01-2.364-3.945L9.636 2.018zm3.896 15.25a1.532 1.532 0 11-3.064 0 1.532 1.532 0 013.064 0zm-.383-8.044a1.15 1.15 0 00-2.298 0v3.83a1.15 1.15 0 002.298 0v-3.83z" fill="currentColor"/></symbol>
<symbol id="github" viewBox="0 0 25 24"><path clip-rule="evenodd" d="M12.75.3c-6.6 0-12 5.4-12 12 0 5.325 3.45 9.825 8.175 11.4.6.075.825-.225.825-.6v-2.025C6.375 21.825 5.7 19.5 5.7 19.5c-.525-1.35-1.35-1.725-1.35-1.725-1.125-.75.075-.75.075-.75 1.2.075 1.875 1.2 1.875 1.2 1.05 1.8 2.775 1.275 3.525.975a2.59 2.59 0 0 1 .75-1.575c-2.7-.3-5.475-1.35-5.475-5.925 0-1.275.45-2.4 1.2-3.225-.15-.3-.525-1.5.15-3.15 0 0 .975-.3 3.3 1.2.975-.3 1.95-.375 3-.375s2.025.15 3 .375c2.325-1.575 3.3-1.275 3.3-1.275.675 1.65.225 2.85.15 3.15.75.825 1.2 1.875 1.2 3.225 0 4.575-2.775 5.625-5.475 5.925.45.375.825 1.125.825 2.25v3.3c0 .3.225.675.825.6a12.015 12.015 0 0 0 8.175-11.4c0-6.6-5.4-12-12-12z" fill="currentColor" fill-rule="evenodd"></path></symbol> <symbol id="github" viewBox="0 0 25 24"><path clip-rule="evenodd" d="M12.75.3c-6.6 0-12 5.4-12 12 0 5.325 3.45 9.825 8.175 11.4.6.075.825-.225.825-.6v-2.025C6.375 21.825 5.7 19.5 5.7 19.5c-.525-1.35-1.35-1.725-1.35-1.725-1.125-.75.075-.75.075-.75 1.2.075 1.875 1.2 1.875 1.2 1.05 1.8 2.775 1.275 3.525.975a2.59 2.59 0 0 1 .75-1.575c-2.7-.3-5.475-1.35-5.475-5.925 0-1.275.45-2.4 1.2-3.225-.15-.3-.525-1.5.15-3.15 0 0 .975-.3 3.3 1.2.975-.3 1.95-.375 3-.375s2.025.15 3 .375c2.325-1.575 3.3-1.275 3.3-1.275.675 1.65.225 2.85.15 3.15.75.825 1.2 1.875 1.2 3.225 0 4.575-2.775 5.625-5.475 5.925.45.375.825 1.125.825 2.25v3.3c0 .3.225.675.825.6a12.015 12.015 0 0 0 8.175-11.4c0-6.6-5.4-12-12-12z" fill="currentColor" fill-rule="evenodd"/></symbol>
<symbol id="twitter" viewBox="0 0 37 37"><path d="M36 18c0 9.945-8.055 18-18 18S0 27.945 0 18 8.055 0 18 0s18 8.055 18 18zm-21.294 9.495c7.983 0 12.348-6.615 12.348-12.348 0-.189 0-.378-.009-.558a8.891 8.891 0 0 0 2.169-2.25 8.808 8.808 0 0 1-2.493.684 4.337 4.337 0 0 0 1.908-2.403 8.788 8.788 0 0 1-2.754 1.053 4.319 4.319 0 0 0-3.168-1.368 4.34 4.34 0 0 0-4.338 4.338c0 .342.036.675.117.99a12.311 12.311 0 0 1-8.946-4.536 4.353 4.353 0 0 0-.585 2.178 4.32 4.32 0 0 0 1.935 3.609 4.263 4.263 0 0 1-1.962-.54v.054a4.345 4.345 0 0 0 3.483 4.257 4.326 4.326 0 0 1-1.962.072 4.333 4.333 0 0 0 4.05 3.015 8.724 8.724 0 0 1-6.426 1.791 12.091 12.091 0 0 0 6.633 1.962z" fill="currentColor"></path></symbol> <symbol id="twitter" viewBox="0 0 37 37"><path d="M36 18c0 9.945-8.055 18-18 18S0 27.945 0 18 8.055 0 18 0s18 8.055 18 18zm-21.294 9.495c7.983 0 12.348-6.615 12.348-12.348 0-.189 0-.378-.009-.558a8.891 8.891 0 0 0 2.169-2.25 8.808 8.808 0 0 1-2.493.684 4.337 4.337 0 0 0 1.908-2.403 8.788 8.788 0 0 1-2.754 1.053 4.319 4.319 0 0 0-3.168-1.368 4.34 4.34 0 0 0-4.338 4.338c0 .342.036.675.117.99a12.311 12.311 0 0 1-8.946-4.536 4.353 4.353 0 0 0-.585 2.178 4.32 4.32 0 0 0 1.935 3.609 4.263 4.263 0 0 1-1.962-.54v.054a4.345 4.345 0 0 0 3.483 4.257 4.326 4.326 0 0 1-1.962.072 4.333 4.333 0 0 0 4.05 3.015 8.724 8.724 0 0 1-6.426 1.791 12.091 12.091 0 0 0 6.633 1.962z" fill="currentColor"/></symbol>
<symbol id="mattermost" viewBox="0 0 206 206"><path fill="currentColor" d="m163.012 19.596 1.082 21.794c17.667 19.519 24.641 47.161 15.846 73.14-13.129 38.782-56.419 59.169-96.693 45.535-40.272-13.633-62.278-56.124-49.15-94.905 8.825-26.066 31.275-43.822 57.276-48.524L105.422.038C61.592-1.15 20.242 26.056 5.448 69.76c-18.178 53.697 10.616 111.963 64.314 130.142 53.698 18.178 111.964-10.617 130.143-64.315 14.77-43.633-1.474-90.283-36.893-115.99"/><path fill="currentColor" d="m137.097 53.436-.596-17.531-.404-15.189s.084-7.322-.17-9.043a2.776 2.776 0 0 0-.305-.914l-.05-.109-.06-.094a2.378 2.378 0 0 0-1.293-1.07 2.382 2.382 0 0 0-1.714.078l-.033.014-.18.092a2.821 2.821 0 0 0-.75.518c-1.25 1.212-5.63 7.08-5.63 7.08l-9.547 11.82-11.123 13.563-19.098 23.75s-8.763 10.938-6.827 24.4c1.937 13.464 11.946 20.022 19.71 22.65 7.765 2.63 19.7 3.5 29.417-6.019 9.716-9.518 9.397-23.53 9.397-23.53l-.744-30.466z"/></symbol> <symbol id="mattermost" viewBox="0 0 206 206"><path fill="currentColor" d="m163.012 19.596 1.082 21.794c17.667 19.519 24.641 47.161 15.846 73.14-13.129 38.782-56.419 59.169-96.693 45.535-40.272-13.633-62.278-56.124-49.15-94.905 8.825-26.066 31.275-43.822 57.276-48.524L105.422.038C61.592-1.15 20.242 26.056 5.448 69.76c-18.178 53.697 10.616 111.963 64.314 130.142 53.698 18.178 111.964-10.617 130.143-64.315 14.77-43.633-1.474-90.283-36.893-115.99"/><path fill="currentColor" d="m137.097 53.436-.596-17.531-.404-15.189s.084-7.322-.17-9.043a2.776 2.776 0 0 0-.305-.914l-.05-.109-.06-.094a2.378 2.378 0 0 0-1.293-1.07 2.382 2.382 0 0 0-1.714.078l-.033.014-.18.092a2.821 2.821 0 0 0-.75.518c-1.25 1.212-5.63 7.08-5.63 7.08l-9.547 11.82-11.123 13.563-19.098 23.75s-8.763 10.938-6.827 24.4c1.937 13.464 11.946 20.022 19.71 22.65 7.765 2.63 19.7 3.5 29.417-6.019 9.716-9.518 9.397-23.53 9.397-23.53l-.744-30.466z"/></symbol>
<symbol id="notifications" viewBox="0 0 20 20"><path d="M2.78 17.046h-.578c-.867 0-1.618-.455-1.964-1.137-.462-.852-.231-1.932.52-2.557a3.126 3.126 0 0 0 1.155-2.216V7.67C1.913 3.466 5.553 0 10 0c4.448 0 8.087 3.466 8.087 7.67v3.694c.116.795.52 1.477 1.155 1.988.751.625.982 1.648.52 2.557-.346.682-1.155 1.136-1.964 1.136H2.78Zm.057-1.705H17.74c.174 0 .405-.114.463-.227.115-.228 0-.398-.116-.455-.924-.795-1.56-1.875-1.733-3.125V7.67c0-3.295-2.83-5.965-6.354-5.965S3.646 4.375 3.646 7.67v3.523a4.879 4.879 0 0 1-1.79 3.41c-.116.113-.174.283-.116.454.115.17.289.284.462.284h.635Zm9.878 2.84C12.31 19.262 11.27 20 10 20c-1.27 0-2.31-.739-2.715-1.818h5.43Z" fill="currentColor"/></symbol>
<symbol id="crowdfund" viewBox="0 0 24 24"><path d="M8 13.854a.4.4 0 1 0 .4.692l-.4-.692Zm8-3.708a.4.4 0 1 0-.4-.692l.4.692Zm-.4 4.4a.4.4 0 1 0 .4-.692l-.4.692ZM8.4 9.454a.4.4 0 1 0-.4.692l.4-.692ZM11.6 7.6v8.8h.8V7.6h-.8ZM13.2 6A1.2 1.2 0 0 1 12 7.2V8a2 2 0 0 0 2-2h-.8ZM12 7.2A1.2 1.2 0 0 1 10.8 6H10a2 2 0 0 0 2 2v-.8ZM10.8 6A1.2 1.2 0 0 1 12 4.8V4a2 2 0 0 0-2 2h.8ZM12 4.8A1.2 1.2 0 0 1 13.2 6h.8a2 2 0 0 0-2-2v.8ZM13.2 18a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2h-.8ZM12 19.2a1.2 1.2 0 0 1-1.2-1.2H10a2 2 0 0 0 2 2v-.8ZM10.8 18a1.2 1.2 0 0 1 1.2-1.2V16a2 2 0 0 0-2 2h.8Zm1.2-1.2a1.2 1.2 0 0 1 1.2 1.2h.8a2 2 0 0 0-2-2v.8Zm-3.6-2.254 7.6-4.4-.4-.692-7.6 4.4.4.692ZM8 15a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2H8Zm-1.2 1.2A1.2 1.2 0 0 1 5.6 15h-.8a2 2 0 0 0 2 2v-.8ZM5.6 15a1.2 1.2 0 0 1 1.2-1.2V13a2 2 0 0 0-2 2h.8Zm1.2-1.2A1.2 1.2 0 0 1 8 15h.8a2 2 0 0 0-2-2v.8ZM18.4 9a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2h-.8Zm-1.2 1.2A1.2 1.2 0 0 1 16 9h-.8a2 2 0 0 0 2 2v-.8ZM16 9a1.2 1.2 0 0 1 1.2-1.2V7a2 2 0 0 0-2 2h.8Zm1.2-1.2A1.2 1.2 0 0 1 18.4 9h.8a2 2 0 0 0-2-2v.8ZM16 13.854l-7.6-4.4-.4.692 7.6 4.4.4-.692ZM18.4 15a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2h-.8Zm-1.2 1.2A1.2 1.2 0 0 1 16 15h-.8a2 2 0 0 0 2 2v-.8ZM16 15a1.2 1.2 0 0 1 1.2-1.2V13a2 2 0 0 0-2 2h.8Zm1.2-1.2a1.2 1.2 0 0 1 1.2 1.2h.8a2 2 0 0 0-2-2v.8ZM8 9a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2H8Zm-1.2 1.2A1.2 1.2 0 0 1 5.6 9h-.8a2 2 0 0 0 2 2v-.8ZM5.6 9a1.2 1.2 0 0 1 1.2-1.2V7a2 2 0 0 0-2 2h.8Zm1.2-1.2A1.2 1.2 0 0 1 8 9h.8a2 2 0 0 0-2-2v.8Z" fill="#343A40"/><path d="M8 13.854a.4.4 0 1 0 .4.692l-.4-.692Zm8-3.708a.4.4 0 1 0-.4-.692l.4.692Zm-.4 4.4a.4.4 0 1 0 .4-.692l-.4.692ZM8.4 9.454a.4.4 0 1 0-.4.692l.4-.692ZM11.6 7.6v8.8h.8V7.6h-.8ZM13.2 6A1.2 1.2 0 0 1 12 7.2V8a2 2 0 0 0 2-2h-.8ZM12 7.2A1.2 1.2 0 0 1 10.8 6H10a2 2 0 0 0 2 2v-.8ZM10.8 6A1.2 1.2 0 0 1 12 4.8V4a2 2 0 0 0-2 2h.8ZM12 4.8A1.2 1.2 0 0 1 13.2 6h.8a2 2 0 0 0-2-2v.8ZM13.2 18a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2h-.8ZM12 19.2a1.2 1.2 0 0 1-1.2-1.2H10a2 2 0 0 0 2 2v-.8ZM10.8 18a1.2 1.2 0 0 1 1.2-1.2V16a2 2 0 0 0-2 2h.8Zm1.2-1.2a1.2 1.2 0 0 1 1.2 1.2h.8a2 2 0 0 0-2-2v.8Zm-3.6-2.254 7.6-4.4-.4-.692-7.6 4.4.4.692ZM8 15a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2H8Zm-1.2 1.2A1.2 1.2 0 0 1 5.6 15h-.8a2 2 0 0 0 2 2v-.8ZM5.6 15a1.2 1.2 0 0 1 1.2-1.2V13a2 2 0 0 0-2 2h.8Zm1.2-1.2A1.2 1.2 0 0 1 8 15h.8a2 2 0 0 0-2-2v.8ZM18.4 9a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2h-.8Zm-1.2 1.2A1.2 1.2 0 0 1 16 9h-.8a2 2 0 0 0 2 2v-.8ZM16 9a1.2 1.2 0 0 1 1.2-1.2V7a2 2 0 0 0-2 2h.8Zm1.2-1.2A1.2 1.2 0 0 1 18.4 9h.8a2 2 0 0 0-2-2v.8ZM16 13.854l-7.6-4.4-.4.692 7.6 4.4.4-.692ZM18.4 15a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2h-.8Zm-1.2 1.2A1.2 1.2 0 0 1 16 15h-.8a2 2 0 0 0 2 2v-.8ZM16 15a1.2 1.2 0 0 1 1.2-1.2V13a2 2 0 0 0-2 2h.8Zm1.2-1.2a1.2 1.2 0 0 1 1.2 1.2h.8a2 2 0 0 0-2-2v.8ZM8 9a1.2 1.2 0 0 1-1.2 1.2v.8a2 2 0 0 0 2-2H8Zm-1.2 1.2A1.2 1.2 0 0 1 5.6 9h-.8a2 2 0 0 0 2 2v-.8ZM5.6 9a1.2 1.2 0 0 1 1.2-1.2V7a2 2 0 0 0-2 2h.8Zm1.2-1.2A1.2 1.2 0 0 1 8 9h.8a2 2 0 0 0-2-2v.8Z" stroke="currentColor" stroke-width=".4" /></symbol>
<symbol id="pointofsale" viewBox="0 0 24 24"><path d="M18.475 12v-.075h-.008V7.024a.64.64 0 0 0-.267-.497.995.995 0 0 0-.608-.202H6.4a.995.995 0 0 0-.608.202.64.64 0 0 0-.267.497v9.952a.64.64 0 0 0 .267.497c.16.125.376.202.608.202h11.2a.995.995 0 0 0 .608-.202.64.64 0 0 0 .267-.497V12Zm-6.4-.725h4.65v1.45h-4.65v-1.45Zm4.65 3.2v1.45h-9.45v-1.45h9.45Zm-6.4-6.4v4.65h-3.05v-4.65h3.05Zm9.6-1.051v9.952c0 1.185-1.033 2.149-2.325 2.149H6.4c-1.276 0-2.325-.972-2.325-2.15V7.025c0-1.17 1.049-2.15 2.325-2.15h11.2c1.276 0 2.325.973 2.325 2.15Zm-3.2 2.5h-4.65v-1.45h4.65v1.45Z" fill="currentColor" /></symbol>
<symbol id="account" viewBox="0 0 24 24"><path d="M6.5 16.74a7.83 7.83 0 0 0 11 0 4.16 4.16 0 0 0-3.58-4.17c-.55.37-1.22.59-1.9.59-.68 0-1.35-.22-1.9-.59-2.03.2-3.6 2-3.62 4.17ZM12 5c1.8 0 3.26 1.55 3.26 3.46 0 1.92-1.46 3.49-3.26 3.49-1.8 0-3.26-1.55-3.26-3.47C8.74 6.58 10.2 5 12 5Z" fill="currentColor"/></symbol>
<symbol id="settings" viewBox="0 0 24 24"><path fill="none" d="M19.2 13.37a.62.62 0 0 0 .43-.6v-1.6c0-.27-.18-.5-.43-.59l-1.67-.55-.24-.57.76-1.6a.63.63 0 0 0-.12-.7L16.8 6.01a.62.62 0 0 0-.72-.12l-1.56.79-.57-.24-.6-1.66a.62.62 0 0 0-.58-.42h-1.6a.63.63 0 0 0-.6.44l-.53 1.66-.59.24-1.59-.76a.63.63 0 0 0-.71.12L6.02 7.2a.63.63 0 0 0-.12.73l.78 1.56-.23.57-1.66.58a.62.62 0 0 0-.42.6v1.59c0 .27.18.5.43.59l1.67.55.24.57-.76 1.6a.63.63 0 0 0 .12.7l1.13 1.14c.19.19.48.24.72.11l1.56-.78.57.24.6 1.66c.08.25.32.41.58.41h1.59c.27 0 .51-.17.6-.43l.53-1.66.59-.24 1.59.76c.24.12.52.07.71-.12l1.13-1.13c.2-.2.24-.49.12-.73l-.79-1.55.25-.57 1.66-.58ZM12 14.39A2.37 2.37 0 0 1 9.62 12a2.37 2.37 0 1 1 4.76 0A2.37 2.37 0 0 1 12 14.39Z" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round"/></symbol>
<symbol id="server-settings" viewBox="0 0 24 24"><rect x="4.75" y="4.75" width="14.5" height="14.5" rx="3.25" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="m8 8 1.6 1.6L8 11.2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="new" viewBox="0 0 24 24"><path d="M17 11H13V7C13 6.45 12.55 6 12 6C11.45 6 11 6.45 11 7V11H7C6.45 11 6 11.45 6 12C6 12.55 6.45 13 7 13H11V17C11 17.55 11.45 18 12 18C12.55 18 13 17.55 13 17V13H17C17.55 13 18 12.55 18 12C18 11.45 17.55 11 17 11Z" fill="currentColor"/></symbol>
<symbol id="wallet-onchain" viewBox="0 0 24 24"><path fill-rule="evenodd" clip-rule="evenodd" d="m16.05 12.26 1.08-1.08a3.05 3.05 0 0 0-4.31-4.32l-2.7 2.7a2.28 2.28 0 0 0 0 3.23l.54.54 1.08-1.07a1.52 1.52 0 0 1 0-2.15l2.15-2.16a1.52 1.52 0 0 1 2.6 1.08 1.52 1.52 0 0 1-.44 1.07l-1.08 1.08 1.08 1.08Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7.97 11.72 6.89 12.8a3.05 3.05 0 0 0 4.3 4.31l2.7-2.7a2.28 2.28 0 0 0 0-3.23l-.54-.54-1.07 1.08a1.52 1.52 0 0 1 0 2.15l-2.16 2.16a1.52 1.52 0 0 1-2.6-1.08 1.52 1.52 0 0 1 .45-1.08l1.08-1.08-1.08-1.07Z" fill="currentColor"/></symbol>
<symbol id="wallet-lightning" viewBox="0 0 24 24"><path d="M17.57 10.7c-.1-.23-.27-.34-.5-.34h-4.3l.5-3.76a.48.48 0 0 0-.33-.55.52.52 0 0 0-.66.17l-5.45 6.54a.59.59 0 0 0-.05.6c.1.17.27.28.49.28h4.3l-.49 3.76c-.05.22.11.5.33.55.06.05.17.05.22.05a.5.5 0 0 0 .44-.22l5.45-6.54c.1-.17.16-.39.05-.55Z" fill="currentColor"/></symbol>
<symbol id="payment-1" viewBox="0 0 24 24"><path d="M7.2 11.2h9.6v1.6H7.2v-1.6Zm0 4.8h5.6v-1.6H7.2V16ZM20 7.02v9.96c0 1.23-1.07 2.22-2.4 2.22H6.4c-1.31 0-2.4-1-2.4-2.22V7.02C4 5.81 5.09 4.8 6.4 4.8h11.2c1.31 0 2.4 1 2.4 2.22ZM18.4 12V7.02c0-.33-.38-.62-.8-.62H6.4c-.43 0-.8.29-.8.62v9.96c0 .33.37.62.8.62h11.2c.43 0 .8-.29.8-.62V12ZM7.2 9.6h9.6V8H7.2v1.6Z" fill="currentColor"/></symbol>
<symbol id="payment-2" viewBox="0 0 24 24"><path d="M12 20a8 8 0 1 1 0-16 8 8 0 0 1 0 16Zm0-15.19a7.2 7.2 0 0 0 0 14.38A7.2 7.2 0 0 0 12 4.8Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M9.48 14.85a.44.44 0 0 1-.3-.14c-.14-.16-.14-.43.05-.57l5.02-4.31c.16-.14.43-.14.57.05.14.17.14.44-.05.57l-5.05 4.29c-.05.08-.16.1-.24.1Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M14.39 14.28a.4.4 0 0 1-.41-.4l.1-3.42-3.08-.17a.4.4 0 0 1-.38-.43c0-.22.19-.4.43-.38l3.47.19c.22 0 .38.19.38.4l-.13 3.83c.02.19-.17.38-.38.38Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/></symbol>
<symbol id="invoice" viewBox="0 0 24 24"><path d="M17.1 20H6.9c-.83 0-1.53-.7-1.53-1.52V5.52c0-.82.7-1.52 1.52-1.52h10.22c.83 0 1.52.7 1.52 1.52v12.96c0 .82-.7 1.52-1.52 1.52ZM6.9 5.3c-.14 0-.23.1-.23.22v12.96c0 .13.1.22.22.22h10.22c.13 0 .22-.1.22-.22V5.52c0-.13-.09-.22-.22-.22H6.89Z" fill="currentColor"/><path d="M12.24 7.95H8.11c-.09 0-.13-.05-.13-.15v-1c0-.05.04-.1.09-.1h4.13c.04 0 .08.05.08.1v1c.05.1 0 .15-.04.15ZM16.2 17.6H8.1c-.08 0-.12-.08-.12-.12V9.87a.1.1 0 0 1 .09-.09h8.08a.1.1 0 0 1 .09.09v7.44c0 .11.06.3-.04.3Z" fill="currentColor"/></symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,10 +1,17 @@
<svg class="logo" viewBox="0 0 192 84" xmlns="http://www.w3.org/2000/svg"> <svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<g> <symbol id="small" viewBox="0 0 46 84">
<path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/> <path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/>
<path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/> <path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/>
<path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/> <path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/>
<path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/> <path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/>
<path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/> <path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/>
<path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" fill="#FFFFFF" class="logo-brand-text"/> </symbol>
</g> <symbol id="large" viewBox="0 0 192 84">
<path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/>
<path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/>
<path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/>
<path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/>
<path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/>
<path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" fill="currentColor" class="logo-brand-text"/>
</symbol>
</svg> </svg>

View File

@@ -3521,7 +3521,7 @@ fieldset:disabled .btn {
} }
.dropdown-item.active, .dropdown-item:active { .dropdown-item.active, .dropdown-item:active {
color: var(--btcpay-body-text); color: var(--btcpay-body-text-active);
text-decoration: none; text-decoration: none;
background-color: var(--btcpay-body-bg-active); background-color: var(--btcpay-body-bg-active);
} }
@@ -3567,7 +3567,7 @@ fieldset:disabled .btn {
} }
.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active { .dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active {
color: var(--btcpay-body-text); color: var(--btcpay-body-text-active);
background-color: var(--btcpay-body-bg-active); background-color: var(--btcpay-body-bg-active);
} }
@@ -10295,7 +10295,7 @@ html[data-devenv]:before {
z-index: 1000; z-index: 1000;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: var(--btcpay-secondary); background: var(--btcpay-bg-tile);
color: var(--btcpay-secondary-text); color: var(--btcpay-secondary-text);
opacity: .7; opacity: .7;
padding: 4px 5px 3px 7px; padding: 4px 5px 3px 7px;

View File

@@ -0,0 +1,430 @@
/* Breakpoints:
XS <576px
SM ≥576px
MD ≥768px
LG ≥992px
XL ≥1200px */
:root {
--mobile-header-height: 4rem;
--desktop-header-height: 8rem;
--sidebar-width: 15%;
--sidebar-min-width: 250px;
--sidebar-max-width: 350px;
}
/* Main Menu */
#mainMenu {
--button-width: 40px;
--button-height: 40px;
--button-padding: 7px;
height: var(--header-height);
z-index: 1;
}
#mainMenuHead .mainMenuButton {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--button-width);
height: var(--button-height);
padding: var(--button-padding);
background: transparent;
border: none;
cursor: pointer;
outline: none;
color: var(--btcpay-body-text-muted);
}
#mainNav {
height: calc(100vh - var(--mobile-header-height));
overflow-y: auto;
padding-top: var(--btcpay-space-m);
}
#mainNav .nav-item i.fa,
#mainNav .nav-item svg.icon {
font-size: 1.125rem;
width: 1.5rem;
height: 1.5rem;
margin-right: var(--btcpay-space-xs);
}
#mainNav .nav-item i.fa {
padding: .15rem 0 0 var(--btcpay-space-xs);
}
#mainNav .accordion-button {
padding: var(--btcpay-space-s) 0;
text-transform: uppercase;
color: var(--btcpay-body-text-muted);
font-weight: var(--btcpay-font-weight-semibold);
}
#mainNav .accordion-item {
border: none !important;
}
#mainNav .navbar-nav > li.nav-item > .nav-link {
display: inline-flex;
align-items: center;
font-weight: var(--btcpay-font-weight-semibold);
color: var(--btcpay-header-link);
transition-property: color;
transition-duration: var(--btcpay-transition-duration-fast);
}
#mainNav .navbar-nav > li.nav-item > .nav-link:focus,
#mainNav .navbar-nav > li.nav-item > .nav-link:hover {
color: var(--btcpay-header-link-accent);
}
#mainNav .navbar-nav > li.nav-item > .nav-link.active,
#mainNav .navbar-nav > li.nav-item > .nav-link.active:focus,
#mainNav .navbar-nav > li.nav-item > .nav-link.active:hover {
color: var(--btcpay-header-link-active);
}
#mainNavSettings {
margin-top: auto;
}
.navbar-brand,
.navbar-brand:hover,
.navbar-brand:focus {
color: inherit;
}
.btcpay-header {
color: var(--btcpay-header-text);
background: var(--btcpay-header-bg);
}
#mainContent {
flex: 1;
display: flex;
flex-direction: column;
}
#mainContent > section {
padding: 0;
flex: 1;
}
#StoreSelector {
display: flex;
align-items: center;
z-index: 2000;
}
#StoreSelector hr {
height: 1px;
}
#StoreSelectorDropdown,
#StoreSelectorToggle {
width: 100%;
}
#StoreSelectorToggle {
overflow: hidden;
text-overflow: ellipsis;
}
#StoreSelectorMenu {
min-width: 100%;
}
/* Logo */
@media (max-width: 575px) {
.logo {
width: 1.125rem;
height: 2rem;
}
.logo-large {
display: none;
}
}
@media (min-width: 576px) {
.logo {
width: 4.6rem;
height: 2rem;
}
.logo-small {
display: none;
}
}
/* Theme Switch */
.btcpay-theme-switch {
display: inline-flex;
align-items: center;
background: none;
cursor: pointer;
border: 0;
}
.btcpay-theme-switch svg {
height: 1rem;
width: 1rem;
}
.btcpay-theme-switch svg[class="d-inline-block"] + span {
margin-left: var(--btcpay-space-xs);
}
.btcpay-theme-switch path {
stroke-width: .5px;
fill: currentColor;
}
.btcpay-theme-switch:hover .btcpay-theme-switch-light,
.btcpay-theme-switch:hover .btcpay-theme-switch-dark {
fill: currentColor;
}
.btcpay-theme-switch-dark {
stroke: currentColor;
}
:root[data-btcpay-theme="dark"] .btcpay-theme-switch-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme="dark"]) .btcpay-theme-switch-dark {
display: inline-block;
}
}
.btcpay-theme-switch-light {
display: none;
}
:root[data-btcpay-theme="dark"] .btcpay-theme-switch-light {
display: inline-block;
}
@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme="light"]) .btcpay-theme-switch-light {
display: inline-block;
}
}
/* Notifications */
#Notifications {
flex: 0 0 var(--button-width);
}
#NotificationsBadge {
position: absolute;
top: 0;
right: 0;
min-width: 1.75em;
}
#NotificationsHandle svg {
width: 1.4rem;
height: 1.4rem;
color: var(--btcpay-body-text-muted);
}
#NotificationsHandle:hover svg {
color: var(--btcpay-header-text);
}
#NotificationsDropdown {
border: 0;
border-radius: 4px;
background: var(--btcpay-bg-tile);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08);
padding: 0;
z-index: 2000;
top: var(--btcpay-space-xs) !important;
}
/* Footer */
.btcpay-footer {
font-size: var(--btcpay-font-size-s);
overflow: hidden;
padding: .5em 0;
color: var(--btcpay-footer-text);
background: var(--btcpay-footer-bg);
}
.btcpay-footer a {
color: var(--btcpay-footer-link);
text-decoration: none;
}
.btcpay-footer a:focus,
.btcpay-footer a:hover {
color: var(--btcpay-footer-link-accent);
}
@media (max-width: 991px) {
#mainMenu {
--header-height: var(--mobile-header-height);
}
#mainNav {
position: fixed;
top: var(--mobile-header-height);
bottom: 0;
left: 0;
width: var(--sidebar-width);
min-width: var(--sidebar-min-width);
max-width: var(--sidebar-max-width);
z-index: 1045;
color: var(--btcpay-body-text);
visibility: hidden;
background-color: inherit;
background-clip: padding-box;
outline: 0;
transform: translateX(-100%);
transition: transform var(--btcpay-transition-duration-fast) ease-in-out;
}
#mainNav.show {
transform: none;
}
.offcanvas-backdrop {
top: var(--mobile-header-height);
transition-duration: var(--btcpay-transition-duration-fast);
}
.offcanvas-backdrop.show {
opacity: 0.8;
}
#StoreSelector {
margin: 0 auto;
}
#StoreSelectorDropdown {
max-width: 40vw;
}
#Notifications {
margin-left: var(--btcpay-space-s);
}
#mainMenuToggle {
--line-thickness: 2px;
--transition-easing: ease-in-out;
--transition-duration: var(--btcpay-transition-duration-fast);
flex: 0 0 var(--button-width);
margin-right: calc(var(--button-padding) * -1);
margin-left: var(--btcpay-space-s);
}
#mainMenuToggle span {
position: relative;
display: inline-block;
width: calc(var(--button-width) - var(--button-padding) * 2);
height: calc(var(--button-height) - (var(--button-padding) * 2) - (var(--line-thickness) * 4));
border-top: var(--line-thickness) solid;
border-bottom: var(--line-thickness) solid;
color: var(--btcpay-body-text-muted);
font-size: 0;
transition: all var(--transition-duration) var(--transition-easing);
}
#mainMenuToggle span:before,
#mainMenuToggle span:after {
position: absolute;
display: block;
content: '';
width: 100%;
height: var(--line-thickness);
top: 50%;
left: 50%;
background: currentColor;
transform: translate(-50%, -50%);
transition: transform var(--transition-duration) var(--transition-easing);
}
#mainMenuToggle:hover span {
color: var(--btcpay-header-text);
}
#mainMenuToggle[aria-expanded="true"] span {
border-color: transparent;
}
#mainMenuToggle[aria-expanded="true"] span:before {
transform: translate(-50%, -50%) rotate(45deg);
}
#mainMenuToggle[aria-expanded="true"] span:after {
transform: translate(-50%, -50%) rotate(-45deg);
}
#mainContent > section {
padding: var(--btcpay-space-l) 0 var(--btcpay-space-xl);
}
}
@media (min-width: 992px) {
#mainMenu {
--header-height: var(--desktop-header-height);
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: var(--sidebar-width);
min-width: var(--sidebar-min-width);
max-width: var(--sidebar-max-width);
height: 100vh;
}
#mainNav {
visibility: visible !important;
}
#Notifications {
order: 1;
margin-left: auto;
}
#StoreSelector {
order: 2;
margin-top: var(--btcpay-space-s);
width: 100%;
}
#mainMenuToggle,
.offcanvas-backdrop{
display: none !important;
}
#NotificationsDropdown {
inset: calc(var(--button-height) * -1 - var(--btcpay-space-s)) auto auto calc(var(--button-width) + var(--btcpay-space-s)) !important;
width: 400px;
}
#mainContent {
margin-left: clamp(var(--sidebar-min-width), var(--sidebar-width), var(--sidebar-max-width));
}
#mainContent > section {
padding: var(--btcpay-space-xl) var(--btcpay-space-l);
}
#mainContent > section > .container {
margin: 0;
}
.btcpay-footer {
padding-left: var(--btcpay-space-l);
padding-right: var(--btcpay-space-l);
}
}

View File

@@ -12,30 +12,18 @@ p {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
hr { hr.primary {
width: 50px; width: 50px;
height: 3px; height: 3px;
background: var(--btcpay-primary); background: var(--btcpay-primary);
display: inline-block; display: inline-block;
} }
hr.light {
background: var(--btcpay-white);
}
.no-gutter > [class*='col-'] { .no-gutter > [class*='col-'] {
padding-right: 0; padding-right: 0;
padding-left: 0; padding-left: 0;
} }
.logo {
height: 2rem;
}
.logo-brand-text {
fill: currentColor;
}
.hide-when-js, .hide-when-js,
.input-group-clear { .input-group-clear {
display: none; display: none;
@@ -76,148 +64,6 @@ hr.light {
} }
} }
/* Navigation bar */
@media (max-width: 575px) {
.offcanvas-header,
.offcanvas-body {
padding-left: var(--btcpay-gutter-x, 0.75rem);
padding-right: var(--btcpay-gutter-x, 0.75rem);
}
}
@media (max-width: 991px) {
.offcanvas-header {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.offcanvas-fade {
top: 0;
right: 0;
left: 0;
bottom: 0;
background: none;
}
.offcanvas-fade .offcanvas-header {
background-color: var(--btcpay-header-bg);
}
.offcanvas-fade .offcanvas-body {
opacity: 0;
transition: opacity 0.15s linear, transform 0.15s linear;
}
.offcanvas-fade.show .offcanvas-body {
opacity: 1;
transform: translate(0, .5rem);
}
}
#mainNav {
color: var(--btcpay-header-text);
background: var(--btcpay-header-bg);
transition-property: background, color;
transition-duration: 0.2s;
}
#mainNav .navbar-nav > li.nav-item > .nav-link {
font-weight: var(--btcpay-font-weight-bold);
color: var(--btcpay-header-link);
}
#mainNav .navbar-nav > li.nav-item > .nav-link:focus,
#mainNav .navbar-nav > li.nav-item > .nav-link:hover {
color: var(--btcpay-header-link-accent);
}
#mainNav .navbar-nav > li.nav-item > .nav-link.active,
#mainNav .navbar-nav > li.nav-item > .nav-link.active:focus,
#mainNav .navbar-nav > li.nav-item > .nav-link.active:hover {
color: var(--btcpay-header-link-active);
}
@media (min-width: 992px) {
#mainNav .navbar-nav > li.nav-item {
padding: 0 .5rem;
}
#mainNav .navbar-nav:last-child > li.nav-item {
padding-right: 0;
}
}
.navbar-brand,
.navbar-brand:hover,
.navbar-brand:focus {
color: inherit;
}
.navbar-toggler {
color: inherit;
border-color: inherit;
opacity: .5;
}
/* Theme Switch */
.btcpay-theme-switch {
background: none;
cursor: pointer;
border: 0;
}
.btcpay-theme-switch:not(.nav-link) {
display: inline-flex;
align-items: center;
}
.btcpay-theme-switch svg {
height: 1rem;
width: 1rem;
}
.btcpay-theme-switch svg[class="d-inline-block"] + span {
margin-left: var(--btcpay-space-xs);
}
.btcpay-theme-switch path {
stroke-width: .5px;
fill: currentColor;
}
.btcpay-theme-switch:hover .btcpay-theme-switch-light,
.btcpay-theme-switch:hover .btcpay-theme-switch-dark {
fill: currentColor;
}
.btcpay-theme-switch-dark {
stroke: currentColor;
}
:root[data-btcpay-theme="dark"] .btcpay-theme-switch-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme="dark"]) .btcpay-theme-switch-dark {
display: inline-block;
}
}
.btcpay-theme-switch-light {
display: none;
}
:root[data-btcpay-theme="dark"] .btcpay-theme-switch-light {
display: inline-block;
}
@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme="light"]) .btcpay-theme-switch-light {
display: inline-block;
}
}
/* Info icons in main headline */ /* Info icons in main headline */
h2 small .fa-question-circle-o { h2 small .fa-question-circle-o {
position: relative; position: relative;
@@ -225,48 +71,39 @@ h2 small .fa-question-circle-o {
font-size: var(--btcpay-font-size-l); font-size: var(--btcpay-font-size-l);
} }
/* Admin Sidebar Navigation */ /* Section Navigation */
.col-md-3 .nav-pills { #SectionNav {
margin-left: -1rem; --border-size: 2px;
width: 100%;
margin-top: calc(var(--btcpay-space-s) * -1);
margin-bottom: var(--btcpay-space-l);
border-bottom: var(--border-size) solid var(--btcpay-body-border-light);
} }
#sideNav .nav-link { #SectionNav .nav-link {
margin: .3rem 0; color: var(--btcpay-nav-link);
border-left: 2px solid transparent; margin-right: var(--btcpay-space-l);
padding: .2rem 1rem; margin-bottom: calc(var(--border-size) * -1);
border-bottom: var(--border-size) solid transparent;
padding: var(--btcpay-space-m) 0;
font-weight: var(--btcpay-font-weight-semibold); font-weight: var(--btcpay-font-weight-semibold);
} }
#sideNav .nav-link.active, #SectionNav .nav-link:last-child {
#sideNav .show > .nav-link { margin-right: 0;
}
#SectionNav .nav-link:hover {
color: var(--btcpay-nav-link-accent);
}
#SectionNav .nav-link.active {
color: var(--btcpay-nav-link-active); color: var(--btcpay-nav-link-active);
border-left-color: var(--btcpay-nav-border-active); border-bottom-color: var(--btcpay-nav-border-active);
background: var(--btcpay-nav-bg-active); background: var(--btcpay-nav-bg-active);
} }
/* Footer */
.btcpay-footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
font-size: 12px;
overflow: hidden;
padding: .5em 0;
color: var(--btcpay-footer-text);
background: var(--btcpay-footer-bg);
}
.btcpay-footer a {
color: var(--btcpay-footer-link);
text-decoration: none;
}
.btcpay-footer a:focus,
.btcpay-footer a:hover {
color: var(--btcpay-footer-link-accent);
}
/* Prevent layout from breaking on hyperlinks with very long URLs as the visible text */ /* Prevent layout from breaking on hyperlinks with very long URLs as the visible text */
.invoice-details a { .invoice-details a {
word-break: break-word; word-break: break-word;
@@ -383,161 +220,3 @@ svg.icon {
svg.icon-note { svg.icon-note {
color: var(--btcpay-neutral-500); color: var(--btcpay-neutral-500);
} }
/* Custom notification dropdown styling */
.notification-dropdown {
border: 0;
border-radius: 4px;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08);
padding: 0;
}
@media (min-width: 992px) {
.notification-dropdown {
width: 420px;
top: 64px;
right: -32px;
}
}
.notification:hover {
background-color: var(--btcpay-body-bg);
}
.notification-badge {
position: relative;
top: -1px;
min-width: 1.5em;
padding: .25em;
}
@media (min-width: 992px) {
#NotificationsDropdownToggle {
position: relative;
}
.notification-badge {
position: absolute;
top: .125rem;
left: 1rem;
}
}
section {
padding: 5rem 0;
}
@media (min-width: 768px) {
section {
padding: 6rem 0;
}
}
/* Homepage */
header.masthead {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: auto;
margin-top: 4rem;
padding: var(--btcpay-space-xxl) var(--btcpay-space-m);
}
header.masthead::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("../img/bg.png");
background-position: center;
background-size: cover;
}
header.masthead .header-content {
position: relative;
text-align: center;
}
header.masthead .header-content .header-content-inner {
max-width: 1000px;
margin-right: auto;
margin-left: auto;
}
header.masthead .header-content .header-content-inner h1 {
font-size: var(--btcpay-font-size-xl);
font-weight: var(--btcpay-font-weight-bold);
margin: 0;
text-transform: uppercase;
}
header.masthead .header-content .header-content-inner hr {
margin: var(--btcpay-space-m) auto;
}
header.masthead .header-content .header-content-inner p {
font-size: var(--btcpay-font-size-l);
margin: var(--btcpay-space-l) 0;
max-width: 38rem;
}
#services img {
margin-bottom: 1rem;
}
.social-row {
margin: 2rem 0;
}
.social-row > div {
margin-bottom: 3rem;
}
.social-row img {
height: 50px;
}
.social-row span {
display: block;
margin-top: 1rem;
}
.service-box {
max-width: 400px;
margin: 50px auto 0;
}
.service-box p {
margin-bottom: 0;
}
.call-to-action {
padding: 50px 0;
}
.call-to-action h2 {
margin: 0 auto 20px;
}
@media (min-width: 768px) {
header.masthead .header-content .header-content-inner h1 {
font-size: var(--btcpay-font-size-xxl);
}
}
@media (min-width: 992px) {
header.masthead {
margin-top: 4rem;
}
.service-box {
margin: 20px auto 0;
}
}
.social-link {
height: 1rem;
padding: 0 0.5rem 0 1rem;
}

View File

@@ -99,12 +99,44 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
}); });
delegate('click', '.btcpay-theme-switch', function (e) { // Theme Switch
e.preventDefault(); delegate('click', '.btcpay-theme-switch', e => {
const current = document.documentElement.getAttribute(THEME_ATTR) || COLOR_MODES[0]; e.preventDefault()
const mode = current === COLOR_MODES[0] ? COLOR_MODES[1] : COLOR_MODES[0]; const current = document.documentElement.getAttribute(THEME_ATTR) || COLOR_MODES[0]
setColorMode(mode); const mode = current === COLOR_MODES[0] ? COLOR_MODES[1] : COLOR_MODES[0]
setColorMode(mode)
e.target.closest('.btcpay-theme-switch').blur()
}) })
// Offcanvas navigation
const mainMenuToggle = document.getElementById('mainMenuToggle')
if (mainMenuToggle) {
delegate('show.bs.offcanvas', '#mainNav', () => {
mainMenuToggle.setAttribute('aria-expanded', 'true')
})
delegate('hide.bs.offcanvas', '#mainNav', () => {
mainMenuToggle.setAttribute('aria-expanded', 'false')
})
}
// Menu collapses
const mainNav = document.getElementById('mainNav')
if (mainNav) {
const COLLAPSED_KEY = 'btcpay-nav-collapsed'
delegate('show.bs.collapse', '#mainNav', (e) => {
const { id } = e.target
const navCollapsed = window.localStorage.getItem(COLLAPSED_KEY)
const collapsed = navCollapsed ? JSON.parse(navCollapsed).filter(i => i !== id ) : []
window.localStorage.setItem(COLLAPSED_KEY, JSON.stringify(collapsed))
})
delegate('hide.bs.collapse', '#mainNav', (e) => {
const { id } = e.target
const navCollapsed = window.localStorage.getItem(COLLAPSED_KEY)
const collapsed = navCollapsed ? JSON.parse(navCollapsed) : []
if (!collapsed.includes(id)) collapsed.push(id)
window.localStorage.setItem(COLLAPSED_KEY, JSON.stringify(collapsed))
})
}
}); });
function switchTimeFormat() { function switchTimeFormat() {

View File

@@ -28,7 +28,6 @@
--btcpay-nav-link: var(--btcpay-neutral-500); --btcpay-nav-link: var(--btcpay-neutral-500);
--btcpay-nav-link-accent: var(--btcpay-neutral-300); --btcpay-nav-link-accent: var(--btcpay-neutral-300);
--btcpay-nav-link-active: var(--btcpay-white); --btcpay-nav-link-active: var(--btcpay-white);
--btcpay-footer-bg: var(--btcpay-bg-dark);
--btcpay-footer-text: var(--btcpay-neutral-400); --btcpay-footer-text: var(--btcpay-neutral-400);
--btcpay-footer-link: var(--btcpay-neutral-400); --btcpay-footer-link: var(--btcpay-neutral-400);
--btcpay-footer-link-accent: var(--btcpay-neutral-200); --btcpay-footer-link-accent: var(--btcpay-neutral-200);

View File

@@ -148,10 +148,10 @@
--btcpay-neutral-800: var(--btcpay-neutral-light-800); --btcpay-neutral-800: var(--btcpay-neutral-light-800);
--btcpay-neutral-900: var(--btcpay-neutral-light-900); --btcpay-neutral-900: var(--btcpay-neutral-light-900);
--btcpay-font-size-base: var(--btcpay-font-size-m); --btcpay-font-size-base: var(--btcpay-font-size-m);
--btcpay-bg-tile: var(--btcpay-white); --btcpay-bg-tile: var(--btcpay-neutral-100);
--btcpay-bg-dark: var(--btcpay-brand-dark); --btcpay-bg-dark: var(--btcpay-brand-dark);
--btcpay-body-bg: var(--btcpay-neutral-100); --btcpay-body-bg: var(--btcpay-white);
--btcpay-body-bg-light: var(--btcpay-white); --btcpay-body-bg-light: var(--btcpay-neutral-100);
--btcpay-body-bg-medium: var(--btcpay-neutral-200); --btcpay-body-bg-medium: var(--btcpay-neutral-200);
--btcpay-body-bg-striped: var(--btcpay-neutral-200); --btcpay-body-bg-striped: var(--btcpay-neutral-200);
--btcpay-body-bg-hover: var(--btcpay-white); --btcpay-body-bg-hover: var(--btcpay-white);
@@ -170,11 +170,11 @@
--btcpay-body-shadow: rgba(25, 135, 84, 0.33); --btcpay-body-shadow: rgba(25, 135, 84, 0.33);
--btcpay-wizard-bg: var(--btcpay-body-bg); --btcpay-wizard-bg: var(--btcpay-body-bg);
--btcpay-wizard-text: var(--btcpay-body-text); --btcpay-wizard-text: var(--btcpay-body-text);
--btcpay-header-bg: var(--btcpay-white); --btcpay-header-bg: var(--btcpay-neutral-100);
--btcpay-header-text: var(--btcpay-body-text); --btcpay-header-text: var(--btcpay-body-text);
--btcpay-header-link: var(--btcpay-header-text); --btcpay-header-link: var(--btcpay-header-text);
--btcpay-header-link-accent: var(--btcpay-primary); --btcpay-header-link-accent: var(--btcpay-primary);
--btcpay-header-link-active: var(--btcpay-primary-accent); --btcpay-header-link-active: var(--btcpay-primary);
--btcpay-nav-link: var(--btcpay-neutral-600); --btcpay-nav-link: var(--btcpay-neutral-600);
--btcpay-nav-link-accent: var(--btcpay-neutral-700); --btcpay-nav-link-accent: var(--btcpay-neutral-700);
--btcpay-nav-link-active: var(--btcpay-neutral-900); --btcpay-nav-link-active: var(--btcpay-neutral-900);
@@ -198,10 +198,10 @@
--btcpay-form-shadow-focus: var(--btcpay-primary-shadow); --btcpay-form-shadow-focus: var(--btcpay-primary-shadow);
--btcpay-form-shadow-valid: var(--btcpay-success-shadow); --btcpay-form-shadow-valid: var(--btcpay-success-shadow);
--btcpay-form-shadow-invalid: var(--btcpay-danger-shadow); --btcpay-form-shadow-invalid: var(--btcpay-danger-shadow);
--btcpay-footer-bg: var(--btcpay-brand-dark); --btcpay-footer-bg: var(--btcpay-body-bg);
--btcpay-footer-text: var(--btcpay-neutral-400); --btcpay-footer-text: var(--btcpay-neutral-500);
--btcpay-footer-link: var(--btcpay-neutral-400); --btcpay-footer-link: var(--btcpay-neutral-500);
--btcpay-footer-link-accent: var(--btcpay-neutral-100); --btcpay-footer-link-accent: var(--btcpay-neutral-600);
--btcpay-code-text: var(--btcpay-body-text); --btcpay-code-text: var(--btcpay-body-text);
--btcpay-code-bg: transparent; --btcpay-code-bg: transparent;
--btcpay-pre-text: var(--btcpay-white); --btcpay-pre-text: var(--btcpay-white);
@@ -384,7 +384,6 @@
--btcpay-dark-rgb: 33, 38, 45; --btcpay-dark-rgb: 33, 38, 45;
} }
header.masthead::before,
.service-box img { .service-box img {
filter: hue-rotate(318deg); filter: hue-rotate(318deg);
} }