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_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)
where T : IConvertible
@@ -28,25 +29,38 @@ namespace BTCPayServer.Abstractions.Extensions
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))
{
return null;
}
var activeId = viewData[ACTIVE_ID_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
{
if (!viewData.ContainsKey(ACTIVE_PAGE_KEY))
{
return null;
}
var activeId = viewData[ACTIVE_ID_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)

View File

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

View File

@@ -607,15 +607,15 @@ namespace BTCPayServer.Tests
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
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.SelectedAppType = AppType.PointOfSale.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model)
.Apps[0].Id;
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
@@ -658,13 +658,11 @@ donation:
.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();
Assert.Equal(10.00m, orangeInvoice.Price);
Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
@@ -673,7 +671,6 @@ donation:
Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc);
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps
.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.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 appService = tester.PayTester.GetService<AppService>();
var eventAggregator = tester.PayTester.GetService<EventAggregator>();
@@ -786,9 +783,7 @@ noninventoryitem:
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
//test payment methods option
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
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 == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, PosViewType.Static, null, null, null, null, null, "g").Result);
invoices = user.BitPay.GetInvoices();

View File

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

View File

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

View File

@@ -28,7 +28,6 @@ namespace BTCPayServer.Tests
public static void AssertNoError(this IWebDriver driver)
{
Assert.NotEmpty(driver.FindElements(By.ClassName("navbar-brand")));
if (!driver.PageSource.Contains("alert-danger")) return;
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}");

View File

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

View File

@@ -1,16 +1,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
@@ -32,16 +27,17 @@ namespace BTCPayServer.Tests
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount();
user2.GrantAccess();
await user2.GrantAccessAsync();
var paymentRequestController = user.GetController<PaymentRequestController>();
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
var request = new UpdatePaymentRequestViewModel()
var request = new UpdatePaymentRequestViewModel
{
Title = "original juice",
Currency = "BTC",
@@ -49,14 +45,13 @@ namespace BTCPayServer.Tests
StoreId = user.StoreId,
Description = "description"
};
var id = (Assert
var id = Assert
.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
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(id));
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id));
request.Title = "update";
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request));
@@ -71,7 +66,6 @@ namespace BTCPayServer.Tests
.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model);
// Archive
Assert
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
Assert.True(Assert
@@ -80,8 +74,9 @@ namespace BTCPayServer.Tests
Assert.Empty(Assert
.IsType<ListPaymentRequestsViewModel>(Assert
.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests()).Model).Items);
//unarchive
.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests(user.StoreId)).Model).Items);
// Unarchive
Assert
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
@@ -91,7 +86,7 @@ namespace BTCPayServer.Tests
Assert.Single(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();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>();
@@ -122,7 +117,7 @@ namespace BTCPayServer.Tests
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
.RouteValues.Last();
var invoiceId = Assert
.IsType<OkObjectResult>(
@@ -153,7 +148,7 @@ namespace BTCPayServer.Tests
response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
.RouteValues.Last();
Assert
.IsType<BadRequestObjectResult>(
@@ -187,7 +182,7 @@ namespace BTCPayServer.Tests
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
.RouteValues.Last();
var invoiceId = response.Value.ToString();
await paymentRequestController.PayPaymentRequest(invoiceId, false);
Assert.IsType<BadRequestObjectResult>(await
@@ -197,7 +192,7 @@ namespace BTCPayServer.Tests
response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
.RouteValues.Last();
var paymentRequestId = response.Value.ToString();
@@ -231,7 +226,7 @@ namespace BTCPayServer.Tests
.Value
.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:
user.BitPay.CreateInvoice(new Invoice(1, "USD")

View File

@@ -2,14 +2,10 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
@@ -146,14 +142,14 @@ namespace BTCPayServer.Tests
public (string storeName, string storeId) CreateNewStore(bool keepId = true)
{
Driver.WaitForElement(By.Id("Stores")).Click();
Driver.WaitForElement(By.Id("CreateStore")).Click();
Driver.WaitForElement(By.Id("StoreSelectorToggle")).Click();
Driver.WaitForElement(By.Id("StoreSelectorMenuItem-Create")).Click();
var name = "Store" + RandomUtils.GetUInt64();
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
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");
Driver.FindElement(By.Id($"Nav-{StoreNavPages.PaymentMethods.ToString()}")).Click();
Driver.FindElement(By.Id($"SectionNav-{StoreNavPages.PaymentMethods.ToString()}")).Click();
if (keepId)
StoreId = storeId;
return (name, storeId);
@@ -279,9 +275,9 @@ namespace BTCPayServer.Tests
}
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();
foreach (var l in links)
{
@@ -315,6 +311,11 @@ namespace BTCPayServer.Tests
Assert.Contains("404 - Page not found</h1>", Driver.PageSource);
}
internal void AssertAccessDenied()
{
Assert.Contains("Access denied</h", Driver.PageSource);
}
public void GoToHome()
{
Driver.Navigate().GoToUrl(ServerUri);
@@ -331,24 +332,29 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Password")).SendKeys(password);
Driver.FindElement(By.Id("LoginButton")).Click();
}
public void GoToApps()
{
Driver.FindElement(By.Id("Apps")).Click();
}
public void GoToStores()
{
Driver.FindElement(By.Id("Stores")).Click();
Driver.FindElement(By.Id("StoreNav-Apps")).Click();
}
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.PaymentMethods)
{
GoToHome();
Driver.WaitForAndClick(By.Id("Stores"));
Driver.FindElement(By.Id($"update-store-{storeId}")).Click();
Driver.WaitForAndClick(By.Id("StoreSelectorToggle"));
Driver.WaitForAndClick(By.Id($"StoreSelectorMenuItem-{storeId}"));
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)
{
Driver.FindElement(By.Id("Invoices")).Click();
Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click();
CheckForJSErrors();
}
public void GoToInvoices()
{
Driver.FindElement(By.Id("Invoices")).Click();
GoToHome();
Driver.FindElement(By.Id("Nav-Invoices")).Click();
}
public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index)
{
Driver.FindElement(By.Id("MySettings")).Click();
Driver.FindElement(By.Id("Nav-Account")).Click();
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}"));
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)
{
Driver.FindElement(By.Id("ServerSettings")).Click();
Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
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.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@@ -13,15 +11,11 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.Charge;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning.LND;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
@@ -38,8 +32,6 @@ using OpenQA.Selenium.Support.UI;
using Renci.SshNet.Security.Cryptography;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using CreateInvoiceRequest = BTCPayServer.Lightning.Charge.CreateInvoiceRequest;
namespace BTCPayServer.Tests
{
@@ -60,9 +52,9 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser(true);
s.Driver.FindElement(By.Id("ServerSettings")).Click();
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
s.Driver.AssertNoError();
s.ClickOnAllSideMenus();
s.ClickOnAllSectionLinks();
s.Driver.FindElement(By.LinkText("Services")).Click();
TestLogs.LogInformation("Let's check if we can access the logs");
@@ -82,7 +74,7 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
s.RegisterNewUser(true);
s.Driver.FindElement(By.Id("ServerSettings")).Click();
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
s.Driver.AssertNoError();
s.Driver.FindElement(By.LinkText("Services")).Click();
@@ -174,8 +166,8 @@ namespace BTCPayServer.Tests
Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url);
//Change Password & Log Out
s.Driver.FindElement(By.Id("MySettings")).Click();
s.Driver.FindElement(By.Id("ChangePassword")).Click();
s.Driver.FindElement(By.Id("Nav-Account")).Click();
s.Driver.FindElement(By.Id("SectionNav-ChangePassword")).Click();
s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456");
s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???");
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???");
@@ -189,8 +181,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("LoginButton")).Click();
Assert.True(s.Driver.PageSource.Contains("Stores"), "Can't Access Stores");
s.Driver.FindElement(By.Id("MySettings")).Click();
s.ClickOnAllSideMenus();
s.Driver.FindElement(By.Id("Nav-Account")).Click();
s.ClickOnAllSectionLinks();
//let's test invite link
s.Logout();
@@ -300,7 +292,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit();
s.FindAlertMessage();
}
CanSetupEmailCore(s);
s.CreateNewStore();
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(offchainHint), "Lightning hint not present");
s.GoToStores();
Assert.True(s.Driver.PageSource.Contains($"warninghint_{storeId}"), "Warning hint on list not present");
s.GoToStore(storeId);
Assert.Contains(storeName, s.Driver.PageSource);
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");
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
s.ClickOnAllSectionLinks();
s.GoToInvoices();
var invoiceId = s.CreateInvoice(storeName);
s.FindAlertMessage();
@@ -425,19 +413,20 @@ namespace BTCPayServer.Tests
s.GoToInvoices();
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.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
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();
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
s.Driver.Navigate().GoToUrl(invoiceUrl);
s.AssertNotFound();
s.AssertAccessDenied();
s.GoToHome();
s.Logout();
@@ -458,11 +447,10 @@ namespace BTCPayServer.Tests
// Alice should be able to delete the store
s.Logout();
s.LogIn(alice);
s.Driver.FindElement(By.Id("Stores")).Click();
s.Driver.FindElement(By.LinkText("Delete")).Click();
s.GoToStore(storeId, StoreNavPages.GeneralSettings);
s.Driver.FindElement(By.Id("DeleteStore")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.FindElement(By.Id("Stores")).Click();
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
}
@@ -481,7 +469,7 @@ namespace BTCPayServer.Tests
s.CreateNewStore();
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("RequestPairing")).Click();
var pairingCode = AssertUrlHasPairingCode(s);
@@ -520,13 +508,11 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser();
var (storeName, _) = s.CreateNewStore();
(string storeName, _) = s.CreateNewStore();
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
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("SelectedStore")).SendKeys(storeName);
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.Id("BuyButtonText")).SendKeys("Take my money");
@@ -560,14 +546,12 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser();
var (storeName, _) = s.CreateNewStore();
(string storeName, _) = s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
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("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
@@ -590,13 +574,13 @@ namespace BTCPayServer.Tests
s.CreateNewStore();
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("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
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());
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
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");
s.GoToStore(storeId, StoreNavPages.GeneralSettings);
s.Driver.FindElement(By.Id("delete-store")).Click();
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
s.Driver.FindElement(By.Id("DeleteStore")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage();
}
}
@@ -830,23 +815,22 @@ namespace BTCPayServer.Tests
{
await s.StartAsync();
s.RegisterNewUser(true);
var (storeName, storeId) = s.CreateNewStore();
var cryptoCode = "BTC";
(string storeName, string storeId) = s.CreateNewStore();
const string cryptoCode = "BTC";
// 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
s.GenerateWallet(cryptoCode, "", true);
//let's test quickly the receive wallet page
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
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
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
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
s.GoToStore(storeId);
s.GenerateWallet(cryptoCode, "", true);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletReceive")).Click();
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("SectionNav-Receive")).Click();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
@@ -905,9 +888,9 @@ namespace BTCPayServer.Tests
Money.Coins(3.0m));
await s.Server.ExplorerNode.GenerateAsync(1);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.ClickOnAllSideMenus();
s.GoToStore(storeId);
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.ClickOnAllSectionLinks();
// Make sure wallet info is correct
s.GoToWalletSettings(storeId, cryptoCode);
@@ -916,21 +899,20 @@ namespace BTCPayServer.Tests
Assert.Contains("m/84'/1'/0'",
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
// 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);
// 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;
Assert.Contains(tx.ToString(), s.Driver.PageSource);
// 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);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
@@ -941,9 +923,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m);
@@ -959,9 +940,8 @@ namespace BTCPayServer.Tests
//let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
@@ -1126,7 +1106,7 @@ namespace BTCPayServer.Tests
});
s.GoToHome();
//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();
var newStore = s.CreateNewStore();
@@ -1241,7 +1221,6 @@ namespace BTCPayServer.Tests
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
if (!s.Driver.PageSource.Contains(bolt))
{
@@ -1272,22 +1251,21 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
var cryptoCode = "BTC";
(_, string storeId) = s.CreateNewStore();
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode).NBitcoinNetwork;
s.GoToStore(storeId);
s.AddLightningNode(cryptoCode, LightningConnectionType.CLightning, false);
s.GoToLightningSettings(storeId, cryptoCode);
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.GoToApps();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
s.Driver.FindElement(By.Id("SelectedAppType")).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("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.CssSelector("option[value='3']")).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();
var btns = s.Driver.FindElements(By.ClassName("lnurl"));
@@ -1299,7 +1277,6 @@ namespace BTCPayServer.Tests
Assert.EndsWith(choice, parsed.ToString());
Assert.IsType<LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
}
}
[Fact]
@@ -1611,7 +1588,7 @@ retry:
private static void CanSetupEmailCore(SeleniumTester s)
{
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.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)
{
var result =
(Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>()
(InvoicesModel)((ViewResult)acc.GetController<InvoiceController>()
.ListInvoices(new InvoicesModel { SearchTerm = filter }).Result).Model;
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
}
@@ -2017,7 +2017,6 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteApps()
@@ -2026,21 +2025,21 @@ namespace BTCPayServer.Tests
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
var user2 = tester.NewAccount();
user2.GrantAccess();
await user2.GrantAccessAsync();
var apps = user.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.Null(vm.AppName);
vm.AppName = "test";
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);
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 =
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.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
@@ -2051,7 +2050,7 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
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);
}
}

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

View File

@@ -1,9 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Models.NotificationViewModels;
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
<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">
<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-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"/>
<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(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(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>
@if (!string.IsNullOrEmpty(Model.Responsive))
{
<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>
}
<span class="d-inline-block"><span class="btcpay-theme-switch-dark">Dark theme</span><span class="btcpay-theme-switch-light">Light theme</span></span>
</button>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
using System;
using System.Linq;
using BTCPayServer.Data;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
@@ -88,13 +87,13 @@ namespace BTCPayServer.Controllers
public bool? RedirectAutomatically { get; set; }
}
[HttpGet]
[Route("{appId}/settings/pos")]
[HttpGet("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId)
{
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false;
@@ -143,8 +142,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = _AppService.Parse(settings.Template, settings.Currency);
var items = _appService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
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}";
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/pos")]
[HttpPost("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
var app = await GetOwnedApp(appId, AppType.PointOfSale);
@@ -178,7 +176,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
vm.Template = _AppService.SerializeTemplate(_AppService.Parse(vm.Template, vm.Currency));
vm.Template = _appService.SerializeTemplate(_appService.Parse(vm.Template, vm.Currency));
}
catch
{
@@ -211,12 +209,11 @@ namespace BTCPayServer.Controllers
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically),
RequiresRefundEmail = vm.RequiresRefundEmail,
});
await _AppService.UpdateOrCreateApp(app);
await _appService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale), new { appId });
}
private int[] ListSplit(string list, string separator = ",")
{
if (string.IsNullOrEmpty(list))
@@ -229,7 +226,7 @@ namespace BTCPayServer.Controllers
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
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.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@@ -18,48 +15,41 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
[Route("apps")]
public partial class AppsController : Controller
{
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencies,
EmailSenderFactory emailSenderFactory,
Services.Stores.StoreRepository storeRepository,
AppService AppService)
StoreRepository storeRepository,
AppService appService)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_userManager = userManager;
_eventAggregator = eventAggregator;
_currencies = currencies;
_emailSenderFactory = emailSenderFactory;
_storeRepository = storeRepository;
_AppService = AppService;
_appService = appService;
}
private readonly UserManager<ApplicationUser> _UserManager;
private readonly ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private readonly BTCPayNetworkProvider _NetworkProvider;
private readonly UserManager<ApplicationUser> _userManager;
private readonly EventAggregator _eventAggregator;
private readonly CurrencyNameTable _currencies;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly StoreRepository _storeRepository;
private readonly AppService _AppService;
private readonly AppService _appService;
public string CreatedAppId { get; set; }
[HttpGet("/stores/{storeId}/apps")]
public async Task<IActionResult> ListApps(
string storeId,
string sortOrder = null,
string sortOrderColumn = null
)
{
var apps = await _AppService.GetAllApps(GetUserId());
var apps = await _appService.GetAllApps(GetUserId(), false, CurrentStore.Id);
if (sortOrder != null && sortOrderColumn != null)
{
@@ -90,64 +80,27 @@ namespace BTCPayServer.Controllers
}
}
return View(new ListAppsViewModel()
return View(new ListAppsViewModel
{
Apps = apps
});
}
[HttpPost]
[Route("{appId}/delete")]
public async Task<IActionResult> DeleteAppPost(string appId)
[HttpGet("/stores/{storeId}/apps/create")]
public IActionResult CreateApp(string storeId)
{
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));
}
[HttpGet]
[Route("create")]
public async Task<IActionResult> CreateApp()
return View(new CreateAppViewModel
{
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
StoreId = CurrentStore.Id
});
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
vm.SetStores(stores);
return View(vm);
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
[HttpPost("/stores/{storeId}/apps/create")]
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
{
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}' class='alert-link'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;
vm.SetStores(stores);
vm.SelectedStore = selectedStore;
vm.StoreId = CurrentStore.Id;
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");
if (!ModelState.IsValid)
@@ -155,14 +108,9 @@ namespace BTCPayServer.Controllers
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
{
StoreDataId = selectedStore,
StoreDataId = CurrentStore.Id,
Name = vm.AppName,
AppType = appType.ToString()
};
@@ -171,18 +119,16 @@ namespace BTCPayServer.Controllers
switch (appType)
{
case AppType.Crowdfund:
var emptyCrowdfund = new CrowdfundSettings();
emptyCrowdfund.TargetCurrency = defaultCurrency;
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
appData.SetSettings(emptyCrowdfund);
break;
case AppType.PointOfSale:
var empty = new PointOfSaleSettings();
empty.Currency = defaultCurrency;
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
appData.SetSettings(empty);
break;
}
await _AppService.UpdateOrCreateApp(appData);
await _appService.UpdateOrCreateApp(appData);
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
CreatedAppId = appData.Id;
@@ -193,19 +139,10 @@ namespace BTCPayServer.Controllers
case AppType.Crowdfund:
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = appData.Id });
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")]
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"));
}
[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)
{
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
return _appService.GetAppDataIfOwner(GetUserId(), appId, type);
}
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}")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Invoice(string invoiceId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
@@ -97,12 +97,12 @@ namespace BTCPayServer.Controllers
var store = await _StoreRepository.FindStore(invoice.StoreId);
var invoiceState = invoice.GetInvoiceState();
var model = new InvoiceDetailsModel()
var model = new InvoiceDetailsModel
{
StoreId = store.Id,
StoreName = store.StoreName,
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,
State = invoiceState.Status.ToModernStatus().ToString(),
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
@@ -183,6 +183,8 @@ namespace BTCPayServer.Controllers
new { pullPaymentId = ppId });
}
HttpContext.SetStoreData(invoice.StoreData);
var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods();
var pmis = paymentMethods.Select(method => method.GetId()).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")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken)
{
using var ctx = _dbContextFactory.CreateContext();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice is null)
return NotFound();
var store = await _StoreRepository.FindStore(invoice.StoreId, GetUserId());
if (store is null)
return NotFound();
if (!CanRefund(invoice.GetInvoiceState()))
return NotFound();
var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod);
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
@@ -421,7 +426,7 @@ namespace BTCPayServer.Controllers
}
[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)
{
@@ -435,7 +440,7 @@ namespace BTCPayServer.Controllers
}
}
return RedirectToAction(nameof(ListInvoices));
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
[HttpGet("i/{invoiceId}")]
@@ -732,18 +737,30 @@ namespace BTCPayServer.Controllers
return Ok("{}");
}
[HttpGet("/stores/{storeId}/invoices")]
[HttpGet("invoices")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[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());
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;
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);
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Take = model.Count;
@@ -823,6 +840,7 @@ namespace BTCPayServer.Controllers
nameof(SelectListItem.Text));
}
[HttpGet("/stores/{storeId}/invoices/create")]
[HttpGet("invoices/create")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
@@ -840,15 +858,25 @@ namespace BTCPayServer.Controllers
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
{
Stores = stores,
StoreId = model?.StoreId,
AvailablePaymentMethods = GetPaymentMethodsSelectList()
};
return View(vm);
}
[HttpPost("/stores/{storeId}/invoices/create")]
[HttpPost("invoices/create")]
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
@@ -901,7 +929,7 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
CreatedInvoiceId = result.Data.Id;
return RedirectToAction(nameof(ListInvoices));
return RedirectToAction(nameof(ListInvoices), new { result.Data.StoreId });
}
catch (BitpayHttpException ex)
{

View File

@@ -4,8 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Filters;
@@ -18,15 +17,14 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
{
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("payment-requests")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class PaymentRequestController : Controller
{
private readonly InvoiceController _InvoiceController;
@@ -61,16 +59,17 @@ namespace BTCPayServer.Controllers
_linkGenerator = linkGenerator;
}
[HttpGet("")]
[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());
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(),
StoreId = CurrentStore.Id,
Skip = model.Skip,
Count = model.Count,
IncludeArchived = includeArchived
@@ -81,40 +80,30 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id)
[HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
{
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
var data = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (data == null && !string.IsNullOrEmpty(payReqId))
{
return NotFound();
}
SelectList stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
if (!stores.Any())
return View(nameof(EditPaymentRequest), new UpdatePaymentRequestViewModel(data)
{
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
StoreId = CurrentStore.Id
});
return RedirectToAction("GetPaymentRequests");
}
return View(nameof(EditPaymentRequest), new UpdatePaymentRequestViewModel(data) { Stores = stores });
}
[HttpPost("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
[HttpPost("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
public async Task<IActionResult> EditPaymentRequest(string payReqId, UpdatePaymentRequestViewModel viewModel)
{
if (string.IsNullOrEmpty(viewModel.Currency) ||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
var data = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (data == null && !string.IsNullOrEmpty(payReqId))
{
return NotFound();
}
@@ -126,10 +115,6 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
{
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
return View(nameof(EditPaymentRequest), viewModel);
}
@@ -153,7 +138,7 @@ namespace BTCPayServer.Controllers
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
data.SetBlob(blob);
if (string.IsNullOrEmpty(id))
if (string.IsNullOrEmpty(payReqId))
{
data.Created = DateTimeOffset.UtcNow;
}
@@ -162,14 +147,14 @@ namespace BTCPayServer.Controllers
_EventAggregator.Publish(new PaymentRequestUpdated { Data = data, PaymentRequestId = data.Id, });
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]
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)
{
return NotFound();
@@ -179,9 +164,9 @@ namespace BTCPayServer.Controllers
return View(result);
}
[HttpGet("{id}/pay")]
[HttpGet("{payReqId}/pay")]
[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)
{
if (amount.HasValue && amount.Value <= 0)
@@ -189,7 +174,7 @@ namespace BTCPayServer.Controllers
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)
{
return NotFound();
@@ -199,7 +184,7 @@ namespace BTCPayServer.Controllers
{
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");
@@ -210,7 +195,7 @@ namespace BTCPayServer.Controllers
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new { Id = id });
return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
}
return BadRequest("Payment Request has already been settled.");
@@ -220,7 +205,7 @@ namespace BTCPayServer.Controllers
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new { Id = id });
return RedirectToAction("ViewPaymentRequest", new { Id = payReqId });
}
return BadRequest("Payment Request has expired");
@@ -249,18 +234,18 @@ namespace BTCPayServer.Controllers
else
amount = result.AmountDue;
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null, cancellationToken);
var pr = await _PaymentRequestRepository.FindPaymentRequest(payReqId, null, cancellationToken);
var blob = pr.GetBlob();
var store = pr.StoreData;
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 =
new InvoiceMetadata
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(id),
PaymentRequestId = id,
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(payReqId),
PaymentRequestId = payReqId,
BuyerEmail = result.Email
};
@@ -273,7 +258,7 @@ namespace BTCPayServer.Controllers
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);
if (redirectToInvoice)
@@ -289,10 +274,10 @@ namespace BTCPayServer.Controllers
}
}
[HttpGet("{id}/cancel")]
public async Task<IActionResult> CancelUnpaidPendingInvoice(string id, bool redirect = true)
[HttpGet("{payReqId}/cancel")]
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)
{
return NotFound();
@@ -318,21 +303,16 @@ namespace BTCPayServer.Controllers
if (redirect)
{
TempData[WellKnownTempData.SuccessMessage] = "Payment cancelled";
return RedirectToAction(nameof(ViewPaymentRequest), new { Id = id });
return RedirectToAction(nameof(ViewPaymentRequest), new { Id = payReqId });
}
return Ok("Payment cancelled");
}
private string GetUserId()
[HttpGet("{payReqId}/clone")]
public async Task<IActionResult> ClonePaymentRequest(string payReqId)
{
return _UserManager.GetUserId(User);
}
[HttpGet("{id}/clone")]
public async Task<IActionResult> ClonePaymentRequest(string id)
{
var result = await EditPaymentRequest(id);
var result = await EditPaymentRequest(CurrentStore.Id, payReqId);
if (result is ViewResult viewResult)
{
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
@@ -346,15 +326,15 @@ namespace BTCPayServer.Controllers
return NotFound();
}
[HttpGet("{id}/archive")]
public async Task<IActionResult> TogglePaymentRequestArchival(string id)
[HttpGet("{payReqId}/archive")]
public async Task<IActionResult> TogglePaymentRequestArchival(string payReqId)
{
var result = await EditPaymentRequest(id);
var result = await EditPaymentRequest(CurrentStore.Id, payReqId);
if (result is ViewResult viewResult)
{
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
model.Archived = !model.Archived;
await EditPaymentRequest(id, model);
await EditPaymentRequest(payReqId, model);
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 unarchived and will appear in the payment request list by default.";
@@ -363,5 +343,15 @@ namespace BTCPayServer.Controllers
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.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
@@ -17,7 +14,6 @@ using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
@@ -25,18 +21,13 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BundlerMinifier.TagHelpers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using StoreData = BTCPayServer.Data.StoreData;
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)
{
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
@@ -850,13 +841,6 @@ namespace BTCPayServer.Controllers
var userId = GetUserId();
if (string.IsNullOrWhiteSpace(userId))
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();
ViewBag.HidePublicKey = true;
ViewBag.ShowStores = true;

View File

@@ -26,18 +26,7 @@ namespace BTCPayServer.Models.AppViewModels
public string AppName { get; set; }
[Display(Name = "Store")]
public string SelectedStore { 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; }
public string StoreId { get; set; }
[Display(Name = "App Type")]
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 string[] StoreIds { get; set; }
public string StoreId { get; set; }
}
public class InvoiceModel

View File

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

View File

@@ -2,10 +2,14 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
namespace BTCPayServer.Security
{
@@ -14,14 +18,23 @@ namespace BTCPayServer.Security
private readonly HttpContext _HttpContext;
private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly PaymentRequestService _paymentRequestService;
private readonly InvoiceRepository _invoiceRepository;
public CookieAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository)
StoreRepository storeRepository,
AppService appService,
InvoiceRepository invoiceRepository,
PaymentRequestService paymentRequestService)
{
_HttpContext = httpContextAccessor.HttpContext;
_userManager = userManager;
_appService = appService;
_storeRepository = storeRepository;
_invoiceRepository = invoiceRepository;
_paymentRequestService = paymentRequestService;
}
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();
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;
}
}
var userid = _userManager.GetUserId(context.User);
if (string.IsNullOrEmpty(userid))
return;
var store = await _storeRepository.FindStore(storeId, userid);
bool success = false;

View File

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

View File

@@ -4,8 +4,6 @@
Layout = "_LayoutSimple";
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="row justify-content-center">
@@ -32,8 +30,6 @@
</form>
</div>
</div>
</div>
</section>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,10 +3,6 @@
ViewData["Title"] = "Recovery code verification";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 section-heading">
<h2>@ViewData["Title"]</h2>
@@ -28,8 +24,6 @@
<button type="submit" class="btn btn-primary">Log in</button>
</form>
</div>
</div>
</section>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />

View File

@@ -3,8 +3,6 @@
ViewData["Title"] = "Two-factor/U2F authentication";
}
<section>
<div class="container">
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null)
{
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
@@ -33,8 +31,6 @@
</div>
}
</div>
</div>
</section>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />

View File

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

View File

@@ -9,8 +9,6 @@
var sortByAsc = "Sort by ascending...";
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-2">
@@ -22,7 +20,7 @@
</a>
</small>
</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 class="row">
@@ -35,6 +33,7 @@
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName"
class="text-nowrap"
@@ -47,6 +46,7 @@
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(appNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppName"
class="text-nowrap"
@@ -59,6 +59,7 @@
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(appTypeSortOrder ?? "asc")"
asp-route-sortOrderColumn="AppType"
class="text-nowrap"
@@ -95,10 +96,10 @@
@app.ViewStyle
</td>
<td style="text-align:right">
<td class="text-end">
@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>
}
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id">View</a>
@@ -120,7 +121,5 @@
}
</div>
</div>
</div>
</section>
<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
@{
ViewData.SetActivePageAndTitle(AppsNavPages.Update, "Update Crowdfund", Model.StoreName);
ViewData.SetActiveId(Model.AppId);
}
@section PageHeadContent {
@@ -15,8 +16,6 @@
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js" asp-append-version="true"></bundle>
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2>
@@ -236,8 +235,8 @@
<div class="d-grid gap-3 d-md-block">
<button type="submit" class="btn btn-primary me-md-2" id="SaveSettings">Save Settings</button>
<div class="btn-group me-md-2">
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm"
<a class="btn btn-secondary flex-grow-1" asp-action="ListInvoices" asp-controller="Invoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary px-3 flex-grow-0" asp-action="ListInvoices" asp-controller="Invoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm"
target="viewinvoices_@Model.AppId"><span class="fa fa-external-link"></span></a>
</div>
@if (Model.ModelWithMinimumData)
@@ -252,5 +251,3 @@
</div>
</div>
</form>
</div>
</section>

View File

@@ -3,9 +3,9 @@
@model UpdatePointOfSaleViewModel
@{
ViewData.SetActivePageAndTitle(AppsNavPages.Update, "Update Point of Sale", Model.StoreName);
ViewData.SetActiveId(Model.Id);
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<h2 class="mb-4">@ViewData["PageTitle"] - @Model.AppName</h2>
@@ -252,8 +252,6 @@
</div>
</div>
</form>
</div>
</section>
@section PageHeadContent {
<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>
@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">
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">

View File

@@ -1,68 +1,50 @@
@{
ViewData["Title"] = "Home Page";
ViewBag.AlwaysShrinkNavBar = false;
}
<header class="masthead">
<div class="header-content">
<div class="header-content-inner text-white">
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<div>
<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>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">Official website</a>
<p class="lead" style="max-width:30em;margin:1.5em auto">BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p>
<a href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener" class="btn btn-primary rounded-pill fs-5">Official website</a>
</div>
<hr class="primary my-5">
<h2 class="mb-4">A Payment Server for Bitcoin</h2>
</div>
</div>
</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="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/lock-logo.png" alt="" asp-append-version="true" />
<div class="py-4 service-box">
<img src="~/img/lock-logo.png" class="mb-2" alt="" asp-append-version="true" />
<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 class="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/qr-logo.png" alt="" asp-append-version="true" />
<div class="py-4 service-box">
<img src="~/img/qr-logo.png" class="mb-2" alt="" asp-append-version="true" />
<h3>Easy</h3>
<p class="text-muted">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 class="col-lg-4 col-md-6 text-center">
<div class="service-box">
<img src="~/img/money-logo.png" alt="" asp-append-version="true" />
<div class="py-4 service-box">
<img src="~/img/money-logo.png" class="mb-2" alt="" asp-append-version="true" />
<h3>Visibility</h3>
<p class="text-muted">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>
</section>
<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">
<div class="row text-center my-5">
<h2 class="mb-4">Video tutorials</h2>
<div class="text-center">
<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>
</div>
</div>
</div>
</div>
<section class="mb-5">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<h2>Donate</h2>
@@ -100,4 +82,3 @@
</div>
</div>
</div>
</section>

View File

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

View File

@@ -44,8 +44,7 @@
</script>
}
<section class="invoice-details">
<div class="container">
<div class="invoice-details">
<partial name="_StatusMessage" />
<div class="row mb-5">
<h2 class="col-xs-12 col-lg-6 mb-4 mb-lg-0">@ViewData["Title"]</h2>
@@ -393,4 +392,3 @@
</div>
</div>
</div>
</section>

View File

@@ -178,8 +178,7 @@
}
@Html.HiddenFor(a => a.Count)
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-4">
@@ -191,16 +190,17 @@
</a>
</small>
</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>
Create an invoice
</a>
</div>
<partial name="InvoiceStatusChangePartial"/>
<div class="row">
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto">
<form asp-action="ListInvoices" method="get">
<form asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
<input type="hidden" asp-for="Count"/>
<input asp-for="TimezoneOffset" type="hidden"/>
<div class="input-group">
@@ -216,17 +216,17 @@
</button>
<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-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-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-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="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: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="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: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: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="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="includearchived:true@{@storeIds}">Archived Invoices</a>
<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-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:-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:-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:-7d@{@storeIds}">Last 7 days</a>
<button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#customRangeModal">Custom Range</button>
<div role="separator" class="dropdown-divider"></div>
<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>orderid:id</code> for filtering a specific order</li>
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
<li><code>status:(expired|invalid|settled|processing|new)</code> for filtering a specific status</li>
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
<li><code>startdate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created after certain date</li>
@@ -308,6 +308,7 @@
@if (Model.Total > 0)
{
<form method="post" id="MassAction" asp-action="MassAction" class="mt-lg-n5">
<input type="hidden" name="storeId" value="@Model.StoreId" />
<span class="me-2">
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
@@ -384,7 +385,8 @@
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString());
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
</span>
<div class="dropdown-menu pull-right">
@@ -409,7 +411,8 @@
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString());
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString())
;
}
</span>
}
@@ -459,5 +462,3 @@
There are no invoices matching your criteria.
</p>
}
</div>
</section>

View File

@@ -3,7 +3,6 @@
ViewData["Title"] = "Refund";
}
<section>
<div class="row">
<div class="modal-dialog">
<div class="modal-content">
@@ -91,4 +90,3 @@
</div>
</div>
</div>
</section>

View File

@@ -4,8 +4,6 @@
ViewData["Title"] = "Confirm Lightning Payout";
var cryptoCode = Context.GetRouteValue("cryptoCode");
}
<section>
<div class="container">
<h2 class="mb-4">@ViewData["Title"]</h2>
<div class="row">
@@ -28,7 +26,7 @@
</ul>
</div>
</div>
</div>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
<script>

View File

@@ -4,8 +4,7 @@
Layout = "_Layout";
ViewData["Title"] = "Lightning Payout Result";
}
<section>
<div class="container">
<h2 class="mb-4">@ViewData["Title"]</h2>
@foreach (var item in Model)
{
@@ -21,5 +20,3 @@
<span class="badge fs-5">@(item.Result == PayResult.Ok ? "Sent" : "Failed")</span>
</div>
}
</div>
</section>

View File

@@ -18,8 +18,7 @@
<input type="hidden" asp-for="ApplicationName" value="@Model.ApplicationName"/>
<input type="hidden" asp-for="SelectiveStores" value="@Model.SelectiveStores"/>
<input type="hidden" asp-for="ApplicationIdentifier" value="@Model.ApplicationIdentifier"/>
<section>
<div class="container">
<
<div class="row">
<div class="col-lg-12 section-heading">
<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>
</div>
</div>
</div>
</section>
</form>

View File

@@ -1,9 +1,9 @@
@inject SignInManager<ApplicationUser> SignInManager
<nav id="sideNav" class="nav flex-column mb-4">
<a id="@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="@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="@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="Manage" asp-action="NotificationSettings">Notifications</a>
<nav id="SectionNav" class="nav">
<a id="SectionNav-@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-controller="Manage" asp-action="Index">Profile</a>
<a id="SectionNav-@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-controller="Manage" asp-action="ChangePassword">Password</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="SectionNav-@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="Manage" asp-action="APIKeys">API Keys</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"/>
</nav>

View File

@@ -3,8 +3,6 @@
ViewData["Title"] = "Notifications";
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
@@ -94,8 +92,6 @@
There are no notifications.
</p>
}
</div>
</section>
<style>
.notification-row.loading {

View File

@@ -15,43 +15,17 @@
<partial name="_ValidationScriptsPartial" />
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js" asp-append-version="true"></bundle>
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<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="col-lg-6">
<input type="hidden" name="Id" value="@Model.Id" />
<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">
<label asp-for="Title" class="form-label" data-required></label>
<input asp-for="Title" class="form-control" required />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Amount" class="form-label" data-required></label>
<input type="number" step="any" asp-for="Amount" class="form-control" required />
@@ -80,6 +54,14 @@
</div>
<span asp-validation-for="ExpiryDate" 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>
</div>
<div class="col-lg-9">
<div class="form-group">
@@ -123,26 +105,24 @@
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
@if (!string.IsNullOrEmpty(Model.Id))
{
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" asp-route-id="@Context.GetRouteValue("id")" id="@Model.Id" name="ViewAppButton">View</a>
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" asp-route-payReqId="@Model.Id" id="ViewAppButton">View</a>
<a class="btn btn-secondary"
target="_blank"
asp-action="ListInvoices"
asp-controller="Invoice"
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
<a class="btn btn-secondary" asp-route-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)
{
<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
{
<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>
</form>
</div>
</section>

View File

@@ -4,8 +4,7 @@
Layout = "_Layout";
ViewData["Title"] = "Payment Requests";
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-4">
@@ -17,7 +16,7 @@
</a>
</small>
</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>
Create a payment request
</a>
@@ -37,7 +36,7 @@
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<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>
<a class="dropdown-item" href="?searchTerm=">Unfiltered</a>
</div>
@@ -70,24 +69,24 @@
<td class="text-end">@item.Amount @item.Currency</td>
<td class="text-end">@item.Status</td>
<td class="text-end">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<a asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id">Edit</a>
<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>
<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>
<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>
<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>
<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>
</tr>
}
</tbody>
</table>
<vc:pager view-model="Model"></vc:pager>
<vc:pager view-model="Model" />
}
else
{
@@ -97,5 +96,3 @@
}
</div>
</div>
</div>
</section>

View File

@@ -70,7 +70,7 @@
</head>
<body>
<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="row align-items-center" style="width:calc(100% + 30px)">
<div class="col-12 col-md-8 col-lg-9">
@@ -107,7 +107,7 @@
{
@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="col col-12 col-sm-6 col-md-12">
<div class="input-group">
@@ -123,12 +123,12 @@
}
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
</a>
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>
</form>
}

View File

@@ -2,8 +2,7 @@
var allErrors = ViewData.ModelState.Values.SelectMany(v => v.Errors.Select(b => b.ErrorMessage));
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12">
<h2>Pay Button request failed</h2>
@@ -16,5 +15,3 @@
</ul>
</div>
</div>
</div>
</section>

View File

@@ -50,7 +50,7 @@
<div class="min-vh-100 d-flex flex-column">
@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">
<form asp-action="ClaimPullPayment" asp-route-pullPaymentId="@Model.Id" class="w-100">
<div class="row align-items-center" style="width:calc(100% + 30px)">

View File

@@ -1,17 +1,18 @@
@using BTCPayServer.Configuration
@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>
<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="Server-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</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="Server-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<nav id="SectionNav" class="nav">
<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="SectionNav-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email Server</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="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)
{
<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="Server-@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.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</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="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"/>
</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.Extensions
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@inject ISettingsRepository SettingsRepository
@inject LinkGenerator linkGenerator
@inject BTCPayServer.Services.BTCPayServerEnvironment _env
@inject UserManager<ApplicationUser> _userManager
@inject ISettingsRepository _settingsRepository
@inject LinkGenerator _linkGenerator
@{
var theme = await SettingsRepository.GetTheme();
}
@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 = "")
var logoSrc = $"{ViewContext.HttpContext.Request.PathBase}/img/logo.svg";
var notificationDisabled = (await _settingsRepository.GetPolicies()).DisableInstantNotifications;
if (!notificationDisabled)
{
<a href="~/" class="navbar-brand py-2 js-scroll-trigger @classes">
<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>
@if (Env.NetworkType != NBitcoin.ChainName.Mainnet)
{
<span class="badge bg-warning" style="font-size:10px;">@Env.NetworkType.ToString()</span>
var user = await _userManager.GetUserAsync(User);
notificationDisabled = user?.DisabledNotifications == "all";
}
</a>
}
#pragma warning restore CS1998
}
<!DOCTYPE html>
<html lang="en"@(Env.IsDeveloping ? " data-devenv" : "")>
<html lang="en"@(_env.IsDeveloping ? " data-devenv" : "")>
<head>
<partial name="LayoutHead" />
@await RenderSectionAsync("PageHeadContent", false)
</head>
<body id="page-top">
@{
if (ViewBag.AlwaysShrinkNavBar == null)
<body class="d-flex flex-column flex-lg-row min-vh-100">
<header id="mainMenu" class="btcpay-header d-flex flex-column">
<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" : "";
}
<!-- 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>
</a>
<vc:store-selector />
<vc:notifications-dropdown />
@if (!theme.CustomTheme)
{
<li class="nav-item">
<vc:theme-switch responsive="lg" css-class="nav-link" />
</li>
}
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Logout" class="nav-link js-scroll-trigger" id="Logout"><span class="d-lg-none d-sm-block">Logout</span><i class="fa fa-sign-out d-lg-inline-block d-none"></i></a>
</li>
</ul>
}
else if (Env.IsSecure)
{
<ul class="navbar-nav">
@if (!(await SettingsRepository.GetPolicies()).LockSubscription)
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger" id="Register">Register</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger" id="Login">Log in</a></li>
<vc:ui-extension-point location="header-nav" model="@Model"/>
</ul>
}
<button id="mainMenuToggle" class="mainMenuButton" type="button" data-bs-toggle="offcanvas" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span>Menu</span>
</button>
</div>
</div>
</div>
<div id="badUrl" class="alert alert-danger alert-dismissible" style="display:none; position:absolute; top:75px;" role="alert">
<vc:main-nav />
</header>
<main id="mainContent">
@if (_env.Context.Request.Host.ToString() != _env.ExpectedHost || _env.Context.Request.Scheme != _env.ExpectedProtocol)
{
<div id="badUrl" class="alert alert-danger alert-dismissible m-3" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close"/>
</button>
<span>BTCPay is expecting you to access this website from <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>
@if (!Env.IsSecure)
}
@if (!_env.IsSecure)
{
<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">
@@ -133,17 +64,17 @@
</span>
</div>
}
</div>
</nav>
<section>
<div class="container">
@RenderBody()
</div>
</section>
@if (User.Identity.IsAuthenticated)
{
<footer class="btcpay-footer">
<div class="container">
<div class="d-flex flex-column justify-content-between flex-lg-row py-1">
<div class="d-flex justify-content-center justify-content-lg-start mb-2 mb-lg-0">
<div class="d-flex flex-column justify-content-between flex-xl-row py-1">
<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">
<vc:icon symbol="github"/>
<span style="margin-left:.4rem">Github</span>
@@ -157,7 +88,7 @@
<span style="margin-left:.4rem">Twitter</span>
</a>
</div>
<div class="text-center text-lg-start">@Env.ToString()</div>
<div class="text-center text-xl-start">@_env.ToString()</div>
</div>
</div>
</footer>
@@ -168,49 +99,22 @@
@await RenderSectionAsync("PageFootContent", false)
<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)
{
<script>
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var loc = window.location, ws_uri;
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")";
if ('WebSocket' in window && window.WebSocket.CLOSING === 2) {
const { host, protocol } = window.location;
const wsUri = `${protocol === "https:" ? "wss:" : "ws:"}//${host}@_linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")`;
const newDataEndpoint = "@_linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
try {
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
if (e.data === "ping")
return;
$.get(newDataEndpoint, function (data) {
$("#notifications-nav-item").replaceWith($(data));
socket = new WebSocket(wsUri);
socket.onmessage = e => {
if (e.data === "ping") return;
$.get(newDataEndpoint, data => {
$("#Notifications").replaceWith($(data));
});
};
socket.onerror = function (e) {
socket.onerror = e => {
console.error("Error while connecting to websocket for notifications (callback)", e);
};
}
@@ -220,5 +124,6 @@
}
</script>
}
</main>
</body>
</html>

View File

@@ -2,7 +2,6 @@
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
ViewBag.ShowMainTitle = ViewBag.ShowMainTitle ?? true;
ViewBag.ShowBreadcrumb = ViewBag.ShowBreadcrumb ?? false;
if (!ViewData.ContainsKey("NavPartialName"))
{
ViewData["NavPartialName"] = "_Nav";
@@ -17,42 +16,17 @@
@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))
{
<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>
}
<div class="row">
@if (ViewBag.ShowMenu)
{
<div class="col-md-3 ms-n3 ms-md-0">
<nav class="nav">
<partial name="@ViewData["NavPartialName"].ToString()" />
</div>
</nav>
}
<div class="col-md-9">
<partial name="_StatusMessage" />
@RenderBody()
</div>
</div>
</div>
</section>

View File

@@ -58,7 +58,7 @@
{
<h4 class="mt-5 mb-3">Other actions</h4>
<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>

View File

@@ -1,15 +1,13 @@
@using BTCPayServer.Client
<nav id="sideNav" class="nav flex-column mb-4">
<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="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="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="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="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="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="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="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="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 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>
<nav id="SectionNav" class="nav">
<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="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="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="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="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="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="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="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="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>
<vc:ui-extension-point location="store-nav" model="@Model" />
</nav>

View File

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

View File

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

View File

@@ -3,8 +3,6 @@
ViewData.SetActivePageAndTitle(WalletsNavPages.Index, "Wallets");
}
<section>
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex justify-content-between mb-2">
@@ -70,5 +68,3 @@
}
</div>
</div>
</div>
</section>

View File

@@ -4,18 +4,17 @@
var wallet = WalletId.Parse(Context.GetRouteValue("walletId").ToString());
var network = BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(wallet.CryptoCode);
}
<nav id="sideNav" class="nav flex-column mb-4">
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions" asp-route-walletId="@Context.GetRouteValue("walletId")" id="WalletTransactions">Transactions</a>
<nav id="SectionNav" class="nav">
<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)
{
<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.Rescan)" asp-action="WalletRescan" asp-route-walletId="@Context.GetRouteValue("walletId")" id="WalletRescan">Rescan</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="SectionNav-Rescan">Rescan</a>
@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" />
</nav>

View File

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

View File

@@ -1,4 +1,5 @@
delegate('click', '.payment-method', function(e) {
closePaymentMethodDialog(e.target.dataset.paymentMethod);
delegate('click', '.payment-method', e => {
const el = e.target.closest('.payment-method')
closePaymentMethodDialog(el.dataset.paymentMethod);
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="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="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="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="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"/></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>

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">
<g>
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<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.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="#FFFFFF" class="logo-brand-text"/>
</g>
</symbol>
<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>

View File

@@ -3521,7 +3521,7 @@ fieldset:disabled .btn {
}
.dropdown-item.active, .dropdown-item:active {
color: var(--btcpay-body-text);
color: var(--btcpay-body-text-active);
text-decoration: none;
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 {
color: var(--btcpay-body-text);
color: var(--btcpay-body-text-active);
background-color: var(--btcpay-body-bg-active);
}
@@ -10295,7 +10295,7 @@ html[data-devenv]:before {
z-index: 1000;
right: 0;
bottom: 0;
background: var(--btcpay-secondary);
background: var(--btcpay-bg-tile);
color: var(--btcpay-secondary-text);
opacity: .7;
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;
}
hr {
hr.primary {
width: 50px;
height: 3px;
background: var(--btcpay-primary);
display: inline-block;
}
hr.light {
background: var(--btcpay-white);
}
.no-gutter > [class*='col-'] {
padding-right: 0;
padding-left: 0;
}
.logo {
height: 2rem;
}
.logo-brand-text {
fill: currentColor;
}
.hide-when-js,
.input-group-clear {
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 */
h2 small .fa-question-circle-o {
position: relative;
@@ -225,48 +71,39 @@ h2 small .fa-question-circle-o {
font-size: var(--btcpay-font-size-l);
}
/* Admin Sidebar Navigation */
.col-md-3 .nav-pills {
margin-left: -1rem;
/* Section Navigation */
#SectionNav {
--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 {
margin: .3rem 0;
border-left: 2px solid transparent;
padding: .2rem 1rem;
#SectionNav .nav-link {
color: var(--btcpay-nav-link);
margin-right: var(--btcpay-space-l);
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);
}
#sideNav .nav-link.active,
#sideNav .show > .nav-link {
#SectionNav .nav-link:last-child {
margin-right: 0;
}
#SectionNav .nav-link:hover {
color: var(--btcpay-nav-link-accent);
}
#SectionNav .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);
}
/* 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 */
.invoice-details a {
word-break: break-word;
@@ -383,161 +220,3 @@ svg.icon {
svg.icon-note {
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) {
e.preventDefault();
const current = document.documentElement.getAttribute(THEME_ATTR) || COLOR_MODES[0];
const mode = current === COLOR_MODES[0] ? COLOR_MODES[1] : COLOR_MODES[0];
setColorMode(mode);
// Theme Switch
delegate('click', '.btcpay-theme-switch', e => {
e.preventDefault()
const current = document.documentElement.getAttribute(THEME_ATTR) || COLOR_MODES[0]
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() {

View File

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

View File

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