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,5 +1,5 @@
@model BTCPayServer.Plugins.Test.TestPluginPageViewModel @model BTCPayServer.Plugins.Test.TestPluginPageViewModel
<section>
<div class="container"> <div class="container">
<h1>Challenge Completed!!</h1> <h1>Challenge Completed!!</h1>
Here is also an image loaded from the plugin<br/> Here is also an image loaded from the plugin<br/>
@@ -18,4 +18,4 @@
</ul> </ul>
</div> </div>
</div> </div>
</section>

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));
@@ -71,7 +66,6 @@ namespace BTCPayServer.Tests
.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
<div id="Notifications">
@if (Model.UnseenCount > 0) @if (Model.UnseenCount > 0)
{ {
<li class="nav-item dropdown" id="notifications-nav-item"> <button id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications" type="button" data-bs-toggle="dropdown">
<a class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" href="#" id="NotificationsDropdownToggle" role="button" data-bs-toggle="dropdown"> <vc:icon symbol="notifications" />
<span class="d-inline-block d-lg-none">Notifications</span> <span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@Model.UnseenCount</span>
<i class="fa fa-bell d-lg-inline-block d-none"></i> </button>
<span class="notification-badge badge rounded-pill bg-danger">@Model.UnseenCount</span> <div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
</a>
<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
{ {
<li class="nav-item" id="notifications-nav-item"> <a asp-controller="Notifications" asp-action="Index" id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications">
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" id="Notifications"> <vc:icon symbol="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,48 +15,41 @@ 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)
{ {
@@ -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();
if (await _AppService.DeleteApp(appData))
TempData[WellKnownTempData.SuccessMessage] = "App deleted successfully.";
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
[Route("create")]
public async Task<IActionResult> CreateApp()
{ {
var stores = await _AppService.GetOwnedStores(GetUserId()); 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] [HttpPost("/stores/{storeId}/apps/create")]
[Route("create")] public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{ {
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 selectedStore = vm.SelectedStore;
vm.SetStores(stores);
vm.SelectedStore = selectedStore;
if (!Enum.TryParse<AppType>(vm.SelectedAppType, out AppType appType)) if (!Enum.TryParse(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("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
} public async Task<IActionResult> EditPaymentRequest(string payReqId, UpdatePaymentRequestViewModel viewModel)
[HttpPost("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, 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)
{ {
@@ -39,13 +52,44 @@ namespace BTCPayServer.Security
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)
{
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; 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>
<hr />
<p class="text-danger">You do not have access to this resource.</p> <p class="text-danger">You do not have access to this resource.</p>
</div> </div>
</div> </div>
</div> </div>
</section>

View File

@@ -4,8 +4,6 @@
Layout = "_LayoutSimple"; Layout = "_LayoutSimple";
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<div class="row justify-content-center"> <div class="row justify-content-center">
@@ -32,8 +30,6 @@
</form> </form>
</div> </div>
</div> </div>
</div>
</section>
@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>
<p>
</p>
<section>
<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>
<hr />
<p> <p>
Please check your email to reset your password. Please check your email to reset your password.
</p> </p>
</div> </div>
</div> </div>
</div>
</section>

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>
<div class="col-lg-12 lead"> <div class="col-lg-12 lead">
<p>This account has been locked out because of multiple invalid login attempts. Please try again later.</p> <p>This account has been locked out because of multiple invalid login attempts. Please try again later.</p>
</div> </div>
</div> </div>
</div>
</section>

View File

@@ -1,8 +1,6 @@
@model LoginWith2faViewModel @model LoginWith2faViewModel
<section class="pt-5"> <div class="row pt-5">
<div>
<div class="row">
<div class="col-lg-12 section-heading"> <div class="col-lg-12 section-heading">
<h2>Two-factor authentication</h2> <h2>Two-factor authentication</h2>
<hr class="primary"> <hr class="primary">
@@ -39,6 +37,3 @@
</p> </p>
</div> </div>
</div> </div>
</div>
</section>

View File

@@ -7,9 +7,7 @@
<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="row">
<div class="col-lg-12 section-heading"> <div class="col-lg-12 section-heading">
<h2>FIDO2 Authentication</h2> <h2>FIDO2 Authentication</h2>
<hr class="primary"> <hr class="primary">
@@ -21,8 +19,6 @@
<button id="btn-retry" class="btn btn-secondary d-none" type="button">Retry</button> <button id="btn-retry" class="btn btn-secondary d-none" type="button">Retry</button>
</div> </div>
</div> </div>
</div>
</section>
<script> <script>
document.getElementById('btn-retry').addEventListener('click', () => window.location.reload()) document.getElementById('btn-retry').addEventListener('click', () => window.location.reload())

View File

@@ -3,10 +3,6 @@
ViewData["Title"] = "Recovery code verification"; ViewData["Title"] = "Recovery code verification";
} }
<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>
@@ -28,8 +24,6 @@
<button type="submit" class="btn btn-primary">Log in</button> <button type="submit" class="btn btn-primary">Log in</button>
</form> </form>
</div> </div>
</div>
</section>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />

View File

@@ -3,8 +3,6 @@
ViewData["Title"] = "Two-factor/U2F authentication"; ViewData["Title"] = "Two-factor/U2F authentication";
} }
<section>
<div class="container">
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null) @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>
@@ -33,8 +31,6 @@
</div> </div>
} }
</div> </div>
</div>
</section>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />

View File

@@ -7,8 +7,6 @@
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["Title"]</h2> <h2 class="mb-4">@ViewData["Title"]</h2>
@@ -17,10 +15,6 @@
<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">
<label asp-for="SelectedStore" class="form-label" data-required></label>
<select asp-for="SelectedStore" asp-items="Model.Stores" class="form-select"></select>
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="SelectedAppType" class="form-label" data-required></label> <label asp-for="SelectedAppType" class="form-label" data-required></label>
<select asp-for="SelectedAppType" asp-items="Model.AppTypes" class="form-select"></select> <select asp-for="SelectedAppType" asp-items="Model.AppTypes" class="form-select"></select>
@@ -32,10 +26,8 @@
</div> </div>
<div class="form-group mt-4"> <div class="form-group mt-4">
<input type="submit" value="Create" class="btn btn-primary" id="Create" /> <input type="submit" value="Create" class="btn btn-primary" id="Create" />
<a asp-action="ListApps" class="btn btn-link px-0 ms-3">Back to list</a> <a asp-action="ListApps" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-link px-0 ms-3">Back to list</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div>
</section>

View File

@@ -9,8 +9,6 @@
var sortByAsc = "Sort by ascending..."; var sortByAsc = "Sort by ascending...";
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <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">
@@ -22,7 +20,7 @@
</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">
@@ -35,6 +33,7 @@
<th> <th>
<a <a
asp-action="ListApps" asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")" asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName" asp-route-sortOrderColumn="StoreName"
class="text-nowrap" class="text-nowrap"
@@ -47,6 +46,7 @@
<th> <th>
<a <a
asp-action="ListApps" asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(appNameSortOrder ?? "asc")" asp-route-sortOrder="@(appNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppName" asp-route-sortOrderColumn="AppName"
class="text-nowrap" class="text-nowrap"
@@ -59,6 +59,7 @@
<th> <th>
<a <a
asp-action="ListApps" asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(appTypeSortOrder ?? "asc")" asp-route-sortOrder="@(appTypeSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppType" asp-route-sortOrderColumn="AppType"
class="text-nowrap" class="text-nowrap"
@@ -95,10 +96,10 @@
@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>
@@ -120,7 +121,5 @@
} }
</div> </div>
</div> </div>
</div>
</section>
<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,8 +16,6 @@
<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>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2> <h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2>
@@ -236,8 +235,8 @@
<div class="d-grid gap-3 d-md-block"> <div class="d-grid gap-3 d-md-block">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button> <button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button>
<div class="btn-group me-md-2"> <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 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-searchterm="@Model.SearchTerm" <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> target="viewinvoices_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div> </div>
@if (Model.ModelWithMinimumData) @if (Model.ModelWithMinimumData)
@@ -252,5 +251,3 @@
</div> </div>
</div> </div>
</form> </form>
</div>
</section>

View File

@@ -3,9 +3,9 @@
@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" /> <partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2> <h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2>
@@ -252,8 +252,6 @@
</div> </div>
</div> </div>
</form> </form>
</div>
</section>
@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,68 +1,50 @@
@{ @{
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">
<div>
<h1>Welcome to BTCPay Server</h1> <h1>Welcome to BTCPay Server</h1>
<p class="fw-semibold">BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p> <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 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> <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>
<section id="services">
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2>A Payment Server for Bitcoin</h2>
<hr class="primary">
</div>
</div>
</div>
<div class="container">
<div class="row"> <div class="row">
<div class="col-lg-4 col-md-6 text-center"> <div class="col-lg-4 col-md-6 text-center">
<div class="service-box"> <div class="py-4 service-box">
<img src="~/img/lock-logo.png" alt="" asp-append-version="true" /> <img src="~/img/lock-logo.png" class="mb-2" alt="" asp-append-version="true" />
<h3>Secure</h3> <h3>Secure</h3>
<p class="text-muted">The payment server does not need to know your private keys, so your money can't be stolen.</p> <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>
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-6 text-center"> <div class="col-lg-4 col-md-6 text-center">
<div class="service-box"> <div class="py-4 service-box">
<img src="~/img/qr-logo.png" alt="" asp-append-version="true" /> <img src="~/img/qr-logo.png" class="mb-2" alt="" asp-append-version="true" />
<h3>Easy</h3> <h3>Easy</h3>
<p class="text-muted">A user-friendly Bitcoin checkout page for your customers.</p> <p class="text-muted mb-0">A user-friendly Bitcoin checkout page for your customers.</p>
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-6 text-center"> <div class="col-lg-4 col-md-6 text-center">
<div class="service-box"> <div class="py-4 service-box">
<img src="~/img/money-logo.png" alt="" asp-append-version="true" /> <img src="~/img/money-logo.png" class="mb-2" alt="" asp-append-version="true" />
<h3>Visibility</h3> <h3>Visibility</h3>
<p class="text-muted">Manage, generate reports, and search for your invoices easily.</p> <p class="text-muted mb-0">Manage, generate reports, and search for your invoices easily.</p>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="row text-center my-5">
</section> <h2 class="mb-4">Video tutorials</h2>
<div class="text-center">
<div class="call-to-action bg-tile">
<div class="container text-center">
<h2>Video tutorials</h2>
<div class="row">
<div class="col-lg-12 text-center">
<a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank" rel="noreferrer noopener"> <a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank" rel="noreferrer noopener">
<img src="~/img/youtube.png" class="img-fluid" asp-append-version="true" /> <img src="~/img/youtube.png" alt="Video tutorials" class="img-fluid" asp-append-version="true" />
</a> </a>
</div> </div>
</div> </div>
</div>
</div>
<section class="mb-5">
<div class="container">
<div class="row"> <div class="row">
<div class="col-lg-8 mx-auto text-center"> <div class="col-lg-8 mx-auto text-center">
<h2>Donate</h2> <h2>Donate</h2>
@@ -100,4 +82,3 @@
</div> </div>
</div> </div>
</div> </div>
</section>

View File

@@ -25,8 +25,6 @@
</script> </script>
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["Title"]</h2> <h2 class="mb-4">@ViewData["Title"]</h2>
@@ -35,12 +33,20 @@
<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>
@if (Model.StoreId != null)
{
<input type="hidden" asp-for="StoreId" />
}
else
{
<div class="form-group"> <div class="form-group">
<label asp-for="Stores" class="form-label"></label> <label asp-for="Stores" class="form-label"></label>
<select asp-for="StoreId" asp-items="Model.Stores" class="form-select"></select> <select asp-for="StoreId" asp-items="Model.Stores" class="form-select"></select>
<span asp-validation-for="StoreId" class="text-danger"></span> <span asp-validation-for="StoreId" class="text-danger"></span>
</div> </div>
<h4 class="mt-5 mb-4">Invoice Details</h4> <h4 class="mt-5 mb-4">Invoice Details</h4>
}
<div class="form-group"> <div class="form-group">
<label asp-for="Amount" class="form-label"></label> <label asp-for="Amount" class="form-label"></label>
<input asp-for="Amount" class="form-control" /> <input asp-for="Amount" class="form-control" />
@@ -147,5 +153,3 @@
</form> </form>
</div> </div>
</div> </div>
</div>
</section>

View File

@@ -44,8 +44,7 @@
</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>
@@ -393,4 +392,3 @@
</div> </div>
</div> </div>
</div> </div>
</section>

View File

@@ -178,8 +178,7 @@
} }
@Html.HiddenFor(a => a.Count) @Html.HiddenFor(a => a.Count)
<section>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-4"> <div class="d-sm-flex align-items-center justify-content-between mb-4">
@@ -191,16 +190,17 @@
</a> </a>
</small> </small>
</h2> </h2>
<a id="CreateNewInvoice" asp-action="CreateInvoice" asp-route-searchTerm="@Model.SearchTerm" class="btn btn-primary mt-3 mt-sm-0"> <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> <span class="fa fa-plus"></span>
Create an invoice Create an invoice
</a> </a>
</div> </div>
<partial name="InvoiceStatusChangePartial"/> <partial name="InvoiceStatusChangePartial"/>
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto"> <div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto">
<form asp-action="ListInvoices" method="get"> <form asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
<input type="hidden" asp-for="Count"/> <input type="hidden" asp-for="Count"/>
<input asp-for="TimezoneOffset" type="hidden"/> <input asp-for="TimezoneOffset" type="hidden"/>
<div class="input-group"> <div class="input-group">
@@ -216,17 +216,17 @@
</button> </button>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle"> <div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="status:invalid@{@storeIds}">Invalid Invoices</a> <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>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="status:processing,status:settled@{@storeIds}">Paid Invoices</a> <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>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidLate@{@storeIds}">Paid Late Invoices</a> <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>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidPartial@{@storeIds}">Paid Partial Invoices</a> <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>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidOver@{@storeIds}">Paid Over Invoices</a> <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 class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="unusual:true@{@storeIds}">Unusual Invoices</a> <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>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="includearchived:true@{@storeIds}">Archived Invoices</a> <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>
<div role="separator" class="dropdown-divider"></div> <div role="separator" class="dropdown-divider"></div>
<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> <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>
<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> <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>
<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> <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 type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button> <button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button>
<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>
@@ -296,7 +296,7 @@
<li><code>storeid:id</code> for filtering a specific store</li> <li><code>storeid:id</code> for filtering a specific store</li>
<li><code>orderid:id</code> for filtering a specific order</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>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>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>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>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>startdate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created after certain date</li>
@@ -308,6 +308,7 @@
@if (Model.Total > 0) @if (Model.Total > 0)
{ {
<form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5"> <form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5">
<input type="hidden" name="storeId" value="@Model.StoreId" />
<span class="me-2"> <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"> <button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions Actions
@@ -384,7 +385,8 @@
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@ @invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None) @if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{ {
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString()); @String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
} }
</span> </span>
<div class="dropdown-menu pull-right"> <div class="dropdown-menu pull-right">
@@ -409,7 +411,8 @@
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@ @invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None) @if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{ {
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString()); @String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
} }
</span> </span>
} }
@@ -459,5 +462,3 @@
There are no invoices matching your criteria. There are no invoices matching your criteria.
</p> </p>
} }
</div>
</section>

View File

@@ -3,7 +3,6 @@
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">
@@ -91,4 +90,3 @@
</div> </div>
</div> </div>
</div> </div>
</section>

View File

@@ -4,8 +4,6 @@
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">
@@ -28,7 +26,7 @@
</ul> </ul>
</div> </div>
</div> </div>
</div>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
<script> <script>

View File

@@ -4,8 +4,7 @@
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)
{ {
@@ -21,5 +20,3 @@
<span class="badge fs-5">@(item.Result == PayResult.Ok ? "Sent" : "Failed")</span> <span class="badge fs-5">@(item.Result == PayResult.Ok ? "Sent" : "Failed")</span>
</div> </div>
} }
</div>
</section>

View File

@@ -18,8 +18,7 @@
<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>
@@ -180,6 +179,4 @@
<button class="btn btn-secondary mx-2" name="command" id="consent-no" type="submit" value="Cancel">Cancel</button> <button class="btn btn-secondary mx-2" name="command" id="consent-no" type="submit" value="Cancel">Cancel</button>
</div> </div>
</div> </div>
</div>
</section>
</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,8 +3,6 @@
ViewData["Title"] = "Notifications"; ViewData["Title"] = "Notifications";
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <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">
@@ -94,8 +92,6 @@
There are no notifications. There are no notifications.
</p> </p>
} }
</div>
</section>
<style> <style>
.notification-row.loading { .notification-row.loading {

View File

@@ -15,43 +15,17 @@
<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>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["Title"]</h2> <h2 class="mb-4">@ViewData["Title"]</h2>
<form method="post" action="@Url.Action("EditPaymentRequest", "PaymentRequest", new { id = Model.Id}, Context.Request.Scheme)"> <form method="post" action="@Url.Action("EditPaymentRequest", "PaymentRequest", new { storeId = Model.StoreId, payReqId = Model.Id }, Context.Request.Scheme)">
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<input type="hidden" name="Id" value="@Model.Id" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Stores" class="form-label"></label>
@if (string.IsNullOrEmpty(Model.Id))
{
<select asp-for="StoreId" asp-items="Model.Stores" class="form-select"></select>
}
else
{
<input type="hidden" asp-for="StoreId" value="@Model.StoreId" />
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly />
}
<span asp-validation-for="StoreId" class="text-danger"></span>
</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"> <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="Amount" class="form-label" data-required></label> <label asp-for="Amount" class="form-label" data-required></label>
<input type="number" step="any" asp-for="Amount" class="form-control" required /> <input type="number" step="any" asp-for="Amount" class="form-control" required />
@@ -80,6 +54,14 @@
</div> </div>
<span asp-validation-for="ExpiryDate" class="text-danger"></span> <span asp-validation-for="ExpiryDate" 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>
</div> </div>
<div class="col-lg-9"> <div class="col-lg-9">
<div class="form-group"> <div class="form-group">
@@ -123,26 +105,24 @@
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button> <button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
@if (!string.IsNullOrEmpty(Model.Id)) @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="ViewPaymentRequest" asp-route-payReqId="@Model.Id" id="ViewAppButton">View</a>
<a class="btn btn-secondary" <a class="btn btn-secondary"
target="_blank" target="_blank"
asp-action="ListInvoices" asp-action="ListInvoices"
asp-controller="Invoice" asp-controller="Invoice"
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a> 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> <a class="btn btn-secondary" asp-route-payReqId="@Model.Id" asp-action="ClonePaymentRequest" id="@Model.Id">Clone</a>
@if (!Model.Archived) @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> <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 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 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" class="btn btn-link px-0 ms-3">Back to list</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>
</div> </div>
</form> </form>
</div>
</section>

View File

@@ -4,8 +4,7 @@
Layout = "_Layout"; Layout = "_Layout";
ViewData["Title"] = "Payment Requests"; ViewData["Title"] = "Payment Requests";
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-4"> <div class="d-sm-flex align-items-center justify-content-between mb-4">
@@ -17,7 +16,7 @@
</a> </a>
</small> </small>
</h2> </h2>
<a asp-action="EditPaymentRequest" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreatePaymentRequest"> <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> <span class="fa fa-plus"></span>
Create a payment request Create a payment request
</a> </a>
@@ -37,7 +36,7 @@
<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>
@@ -70,24 +69,24 @@
<td class="text-end">@item.Amount @item.Currency</td> <td class="text-end">@item.Amount @item.Currency</td>
<td class="text-end">@item.Status</td> <td class="text-end">@item.Status</td>
<td class="text-end"> <td class="text-end">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a> <a asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id">Edit</a>
<span> - </span> <span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a> <a asp-action="ViewPaymentRequest" asp-route-payReqId="@item.Id">View</a>
<span> - </span> <span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a> <a target="_blank" asp-controller="Invoice" asp-action="ListInvoices" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span> <span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a> <a target="_blank" asp-action="PayPaymentRequest" asp-route-payReqId="@item.Id">Pay</a>
<span> - </span> <span> - </span>
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-id="@item.Id">Clone</a> <a target="_blank" asp-action="ClonePaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id">Clone</a>
<span> - </span> <span> - </span>
<a asp-action="TogglePaymentRequestArchival" asp-route-id="@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a> <a asp-action="TogglePaymentRequestArchival" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a>
</td> </td>
</tr> </tr>
} }
</tbody> </tbody>
</table> </table>
<vc:pager view-model="Model"></vc:pager> <vc:pager view-model="Model" />
} }
else else
{ {
@@ -97,5 +96,3 @@
} }
</div> </div>
</div> </div>
</div>
</section>

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,8 +2,7 @@
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>
@@ -16,5 +15,3 @@
</ul> </ul>
</div> </div>
</div> </div>
</div>
</section>

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,127 +1,58 @@
@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">
<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)
{ {
ViewBag.AlwaysShrinkNavBar = true; <span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@_env.NetworkType.ToString()</span>
} }
var additionalStyle = ViewBag.AlwaysShrinkNavBar ? "navbar-shrink always-shrinked" : ""; </a>
} <vc:store-selector />
<!-- 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 /> <vc:notifications-dropdown />
@if (!theme.CustomTheme) <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>
<li class="nav-item"> </button>
<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> <vc:main-nav />
</div> </header>
<div id="badUrl" class="alert alert-danger alert-dismissible" style="display:none; position:absolute; top:75px;" role="alert"> <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"> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close"/> <vc:icon symbol="close"/>
</button> </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> <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>
@if (!Env.IsSecure) }
@if (!_env.IsSecure)
{ {
<div id="insecureEnv" class="alert alert-danger alert-dismissible" style="position:absolute; top:75px;" role="alert"> <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"> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
@@ -133,17 +64,17 @@
</span> </span>
</div> </div>
} }
</div> <section>
</nav> <div class="container">
@RenderBody() @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>
@@ -157,7 +88,7 @@
<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>
@@ -168,49 +99,22 @@
@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:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += "@linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")";
var newDataEndpoint = "@linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
try { try {
socket = new WebSocket(ws_uri); socket = new WebSocket(wsUri);
socket.onmessage = function (e) { socket.onmessage = e => {
if (e.data === "ping") if (e.data === "ping") return;
return; $.get(newDataEndpoint, data => {
$.get(newDataEndpoint, function (data) { $("#Notifications").replaceWith($(data));
$("#notifications-nav-item").replaceWith($(data));
}); });
}; };
socket.onerror = function (e) { socket.onerror = e => {
console.error("Error while connecting to websocket for notifications (callback)", e); console.error("Error while connecting to websocket for notifications (callback)", e);
}; };
} }
@@ -220,5 +124,6 @@
} }
</script> </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>
<div class="container">
@if (ViewBag.ShowBreadcrumb)
{
<nav aria-label="breadcrumb" class="mt-n3 mb-4">
<ol class="breadcrumb px-0">
@if (!string.IsNullOrEmpty(ViewData["CategoryTitle"] as string))
{
<li class="breadcrumb-item" aria-current="page">@ViewData["CategoryTitle"]</li>
}
@if (!string.IsNullOrEmpty(ViewData["MainTitle"] as string)) @if (!string.IsNullOrEmpty(ViewData["MainTitle"] as string))
{
<li class="breadcrumb-item" aria-current="page">@ViewData["MainTitle"]</li>
}
@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> <h2 class="mb-5">@ViewData["MainTitle"]</h2>
} }
<div class="row">
@if (ViewBag.ShowMenu) @if (ViewBag.ShowMenu)
{ {
<div class="col-md-3 ms-n3 ms-md-0"> <nav class="nav">
<partial name="@ViewData["NavPartialName"].ToString()" /> <partial name="@ViewData["NavPartialName"].ToString()" />
</div> </nav>
} }
<div class="col-md-9">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
@RenderBody() @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>
<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" /> <vc:ui-extension-point location="store-nav" model="@Model" />
</nav> </nav>

View File

@@ -7,8 +7,6 @@
<partial name="_ValidationScriptsPartial"/> <partial name="_ValidationScriptsPartial"/>
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"]</h2> <h2 class="mb-4">@ViewData["PageTitle"]</h2>
@@ -28,5 +26,3 @@
</form> </form>
</div> </div>
</div> </div>
</div>
</section>

View File

@@ -7,8 +7,6 @@
var sortByAsc = "Sort by ascending..."; var sortByAsc = "Sort by ascending...";
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <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">
@@ -72,12 +70,13 @@
</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>
<a asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@store.Id">Pull Payments</a>
@if (store.IsOwner) @if (store.IsOwner)
{ {
<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="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> <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>
} }
<a asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@store.Id" >Pull Payments</a>
</td> </td>
</tr> </tr>
} }
@@ -92,7 +91,5 @@
} }
</div> </div>
</div> </div>
</div>
</section>
<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,8 +3,6 @@
ViewData.SetActivePageAndTitle(WalletsNavPages.Index, "Wallets"); ViewData.SetActivePageAndTitle(WalletsNavPages.Index, "Wallets");
} }
<section>
<div class="container">
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<div class="d-sm-flex justify-content-between mb-2"> <div class="d-sm-flex justify-content-between mb-2">
@@ -70,5 +68,3 @@
} }
</div> </div>
</div> </div>
</div>
</section>

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);
} }