Archive stores and apps (#5296)

* Add flags and migration

* Archive store

* Archive apps
This commit is contained in:
d11n
2023-09-11 02:59:17 +02:00
committed by GitHub
parent 089e16020e
commit 57bc90ad03
40 changed files with 545 additions and 114 deletions

View File

@@ -37,6 +37,7 @@ namespace BTCPayServer.Client.Models
public string RedirectUrl { get; set; } = null;
public bool? RedirectAutomatically { get; set; } = null;
public bool? RequiresRefundEmail { get; set; } = null;
public bool? Archived { get; set; } = null;
public string FormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
public CheckoutType? CheckoutType { get; set; } = null;
@@ -78,6 +79,7 @@ namespace BTCPayServer.Client.Models
public bool? DisplayPerksValue { get; set; } = null;
public bool? DisplayPerksRanking { get; set; } = null;
public bool? SortPerksByPopularity { get; set; } = null;
public bool? Archived { get; set; } = null;
public string[] Sounds { get; set; } = null;
public string[] AnimationColors { get; set; } = null;
}

View File

@@ -9,6 +9,8 @@ namespace BTCPayServer.Client.Models
public string AppType { get; set; }
public string Name { get; set; }
public string StoreId { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? Archived { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Created { get; set; }
}

View File

@@ -45,6 +45,9 @@ namespace BTCPayServer.Client.Models
public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool Archived { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool ShowRecommendedFee { get; set; } = true;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]

View File

@@ -14,6 +14,7 @@ namespace BTCPayServer.Data
public DateTimeOffset Created { get; set; }
public bool TagAllInvoices { get; set; }
public string Settings { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
{

View File

@@ -48,6 +48,7 @@ namespace BTCPayServer.Data
public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{

View File

@@ -0,0 +1,39 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230906135844_AddArchivedFlagForStoresAndApps")]
public partial class AddArchivedFlagForStoresAndApps : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "Stores",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "Apps",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Archived",
table: "Stores");
migrationBuilder.DropColumn(
name: "Archived",
table: "Apps");
}
}
}

View File

@@ -79,6 +79,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("AppType")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
@@ -751,6 +754,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<string>("DefaultCrypto")
.HasColumnType("TEXT");

View File

@@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="114.0.5735.9000" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="116.0.5845.9600" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>

View File

@@ -54,13 +54,33 @@ namespace BTCPayServer.Tests
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);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal("test", app.AppName);
Assert.Equal(apps.CreatedAppId, app.Id);
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, app.StoreId);
// Archive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
app = appList.Apps[0];
Assert.True(app.Archived);
Assert.IsType<NotFoundResult>(await crowdfund.ViewCrowdfund(app.Id));
// Unarchive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
Assert.False(app.Archived);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
// Delete
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -16,8 +15,6 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Plugins;
using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services.Notifications;
@@ -25,7 +22,6 @@ using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
@@ -301,6 +297,7 @@ namespace BTCPayServer.Tests
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("PointOfSale", app.AppType);
Assert.Equal("test app title", app.Title);
Assert.False(app.Archived);
// Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () =>
@@ -324,17 +321,20 @@ namespace BTCPayServer.Tests
new CreatePointOfSaleAppRequest()
{
AppName = "new app name",
Title = "new app title"
Title = "new app title",
Archived = true
}
);
// Test generic GET app endpoint first
retrievedApp = await client.GetApp(app.Id);
Assert.Equal("new app name", retrievedApp.Name);
Assert.True(retrievedApp.Archived);
// Test the POS-specific endpoint also
var retrievedPosApp = await client.GetPosApp(app.Id);
Assert.Equal("new app name", retrievedPosApp.Name);
Assert.Equal("new app title", retrievedPosApp.Title);
Assert.True(retrievedPosApp.Archived);
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () =>
@@ -466,6 +466,7 @@ namespace BTCPayServer.Tests
Assert.Equal("test app from API", app.Name);
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("Crowdfund", app.AppType);
Assert.False(app.Archived);
// Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () =>
@@ -482,11 +483,13 @@ namespace BTCPayServer.Tests
Assert.Equal(app.Name, retrievedApp.Name);
Assert.Equal(app.StoreId, retrievedApp.StoreId);
Assert.Equal(app.AppType, retrievedApp.AppType);
Assert.False(retrievedApp.Archived);
// Test the crowdfund-specific endpoint also
var retrievedPosApp = await client.GetCrowdfundApp(app.Id);
Assert.Equal(app.Name, retrievedPosApp.Name);
Assert.Equal(app.Title, retrievedPosApp.Title);
var retrievedCfApp = await client.GetCrowdfundApp(app.Id);
Assert.Equal(app.Name, retrievedCfApp.Name);
Assert.Equal(app.Title, retrievedCfApp.Title);
Assert.False(retrievedCfApp.Archived);
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () =>
@@ -536,10 +539,12 @@ namespace BTCPayServer.Tests
Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.False(apps[0].Archived);
Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.False(apps[1].Archived);
// Get all apps for all store now
apps = await client.GetAllApps();
@@ -549,15 +554,17 @@ namespace BTCPayServer.Tests
Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.False(apps[0].Archived);
Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.False(apps[1].Archived);
Assert.Equal(newApp.Name, apps[2].Name);
Assert.Equal(newApp.StoreId, apps[2].StoreId);
Assert.Equal(newApp.AppType, apps[2].AppType);
Assert.False(apps[2].Archived);
}
[Fact(Timeout = TestTimeout)]
@@ -1272,7 +1279,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.GrantAccessAsync();
await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted);
@@ -1351,6 +1358,13 @@ namespace BTCPayServer.Tests
}
tester.DeleteStore = false;
Assert.Empty(await client.GetStores());
// Archive
var archivableStore = await client.CreateStore(new CreateStoreRequest { Name = "Archivable" });
Assert.False(archivableStore.Archived);
archivableStore = await client.UpdateStore(archivableStore.Id, new UpdateStoreRequest { Name = "Archived", Archived = true });
Assert.Equal("Archived", archivableStore.Name);
Assert.True(archivableStore.Archived);
}
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)

View File

@@ -1,8 +1,10 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Hosting;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
@@ -123,12 +125,14 @@ donation:
price: 1.02
custom: true
";
vmpos.Currency = "EUR";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
Assert.Equal("EUR", vmview.CurrencyCode);
// apple shouldn't be available since we it's set to "disabled: true" above
Assert.Equal(2, vmview.Items.Length);
Assert.Equal("orange", vmview.Items[0].Title);
@@ -139,6 +143,41 @@ donation:
// apple is not found
Assert.IsType<NotFoundResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
// List
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
apps = user.GetController<UIAppsController>();
appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType, Settings = "{\"currency\":\"EUR\"}" };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
Assert.Single(appList.Apps);
Assert.Equal("test", app.AppName);
Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, app.StoreId);
Assert.False(app.Archived);
// Archive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
app = appList.Apps[0];
Assert.True(app.Archived);
Assert.IsType<NotFoundResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
// Unarchive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
Assert.False(app.Archived);
Assert.IsType<ViewResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
// Delete
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);
}
}
}

View File

@@ -809,6 +809,27 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
// Archive store
(storeName, storeId) = s.CreateNewStore();
s.Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
Assert.Contains(storeName, s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
s.Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
s.GoToStore();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The store has been archived and will no longer appear in the stores list by default.", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
Assert.DoesNotContain(storeName, s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
Assert.Contains("1 Archived Store", s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
s.Driver.FindElement(By.Id("StoreSelectorArchived")).Click();
var storeLink = s.Driver.FindElement(By.Id($"Store-{storeId}"));
Assert.Contains(storeName, storeLink.Text);
storeLink.Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The store has been unarchived and will appear in the stores list by default again.", s.FindAlertMessage().Text);
}
[Fact(Timeout = TestTimeout)]
@@ -978,6 +999,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
@@ -1046,6 +1068,24 @@ namespace BTCPayServer.Tests
// We are only if explicitly going to /
s.GoToUrl("/");
Assert.Contains("Tea shop", s.Driver.PageSource);
// Archive
Assert.True(s.Driver.ElementDoesNotExist(By.Id("Nav-ArchivedApps")));
s.Driver.SwitchTo().Window(windows[0]);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been archived and will no longer appear in the apps list by default.", s.FindAlertMessage().Text);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ViewApp")));
Assert.Contains("1 Archived App", s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Text);
s.Driver.Navigate().GoToUrl(posBaseUrl);
Assert.Contains("Page not found", s.Driver.Title, StringComparison.OrdinalIgnoreCase);
s.Driver.Navigate().Back();
s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Click();
// Unarchive
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
}
[Fact(Timeout = TestTimeout)]
@@ -1079,17 +1119,37 @@ namespace BTCPayServer.Tests
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
var cfUrl = s.Driver.Url;
Assert.Equal("Currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
// Archive
Assert.True(s.Driver.ElementDoesNotExist(By.Id("Nav-ArchivedApps")));
s.Driver.SwitchTo().Window(windows[0]);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been archived and will no longer appear in the apps list by default.", s.FindAlertMessage().Text);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ViewApp")));
Assert.Contains("1 Archived App", s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Text);
s.Driver.Navigate().GoToUrl(cfUrl);
Assert.Contains("Page not found", s.Driver.Title, StringComparison.OrdinalIgnoreCase);
s.Driver.Navigate().Back();
s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Click();
// Unarchive
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
}
[Fact(Timeout = TestTimeout)]

View File

@@ -6,7 +6,10 @@
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client
@using BTCPayServer.Components.ThemeSwitch
@using BTCPayServer.Components.UIExtensionPoint
@using BTCPayServer.Services
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.CustodianAccounts
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerEnvironment Env
@@ -187,6 +190,14 @@
<span>Manage Plugins</span>
</a>
</li>
@if (Model.Store != null && Model.ArchivedAppsCount > 0)
{
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIApps" asp-action="ListApps" asp-route-storeId="@Model.Store.Id" asp-route-archived="true" class="nav-link @ViewData.IsActivePage(AppsNavPages.Index)" id="Nav-ArchivedApps">
@Model.ArchivedAppsCount Archived App@(Model.ArchivedAppsCount == 1 ? "" : "s")
</a>
</li>
}
</ul>
</div>
</div>

View File

@@ -68,14 +68,18 @@ namespace BTCPayServer.Components.MainNav
vm.LightningNodes = lightningNodes;
// Apps
var apps = await _appService.GetAllApps(UserId, false, store.Id);
vm.Apps = apps.Select(a => new StoreApp
var apps = await _appService.GetAllApps(UserId, false, store.Id, true);
vm.Apps = apps
.Where(a => !a.Archived)
.Select(a => new StoreApp
{
Id = a.Id,
AppName = a.AppName,
AppType = a.AppType
}).ToList();
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
if (PoliciesSettings.Experimental)
{
// Custodian Accounts

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.MainNav
{
@@ -13,6 +12,7 @@ namespace BTCPayServer.Components.MainNav
public List<StoreApp> Apps { get; set; }
public CustodianAccountData[] CustodianAccounts { get; set; }
public bool AltcoinsBuild { get; set; }
public int ArchivedAppsCount { get; set; }
}
public class StoreApp

View File

@@ -1,8 +1,9 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Components.MainLogo
@using BTCPayServer.Services
@using BTCPayServer.Views.Stores
@inject BTCPayServerEnvironment Env
@inject IFileService FileService
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
@@ -34,7 +35,7 @@ else
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
@if (Model.Options.Any())
@if (Model.Options.Any() || Model.ArchivedCount > 0)
{
<div id="StoreSelector">
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
@@ -64,8 +65,16 @@ else
}
</li>
}
@if (Model.Options.Any())
{
<li><hr class="dropdown-divider"></li>
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item" id="StoreSelectorCreate">Create Store</a></li>
}
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
@if (Model.ArchivedCount > 0)
{
<li><hr class="dropdown-divider"></li>
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
}
</ul>
</div>
</div>

View File

@@ -30,7 +30,9 @@ namespace BTCPayServer.Components.StoreSelector
var userId = _userManager.GetUserId(UserClaimsPrincipal);
var stores = await _storeRepo.GetStoresByUserId(userId);
var currentStore = ViewContext.HttpContext.GetStoreData();
var archivedCount = stores.Count(s => s.Archived);
var options = stores
.Where(store => !store.Archived)
.Select(store =>
{
var cryptoCode = store
@@ -59,7 +61,8 @@ namespace BTCPayServer.Components.StoreSelector
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName,
CurrentStoreLogoFileId = blob?.LogoFileId
CurrentStoreLogoFileId = blob?.LogoFileId,
ArchivedCount = archivedCount
};
return View(vm);

View File

@@ -8,6 +8,7 @@ namespace BTCPayServer.Components.StoreSelector
public string CurrentStoreId { get; set; }
public string CurrentStoreLogoFileId { get; set; }
public string CurrentDisplayName { get; set; }
public int ArchivedCount { get; set; }
}
public class StoreSelectorOption

View File

@@ -66,7 +66,8 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = CrowdfundAppType.AppType
AppType = CrowdfundAppType.AppType,
Archived = request.Archived ?? false
};
appData.SetSettings(ToCrowdfundSettings(request));
@@ -97,7 +98,8 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = PointOfSaleAppType.AppType
AppType = PointOfSaleAppType.AppType,
Archived = request.Archived ?? false
};
appData.SetSettings(ToPointOfSaleSettings(request));
@@ -111,7 +113,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true);
if (app == null)
{
return AppNotFound();
@@ -129,6 +131,10 @@ namespace BTCPayServer.Controllers.Greenfield
}
app.Name = request.AppName;
if (request.Archived != null)
{
app.Archived = request.Archived.Value;
}
app.SetSettings(ToPointOfSaleSettings(request));
await _appService.UpdateOrCreateApp(app);
@@ -153,7 +159,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetAllApps()
{
var apps = await _appService.GetAllApps(_userManager.GetUserId(User));
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), includeArchived: true);
return Ok(apps.Select(ToModel).ToArray());
}
@@ -162,7 +168,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetAllApps(string storeId)
{
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), allowNoUser: false, storeId);
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), false, storeId, true);
return Ok(apps.Select(ToModel).ToArray());
}
@@ -171,7 +177,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetApp(string appId)
{
var app = await _appService.GetApp(appId, null);
var app = await _appService.GetApp(appId, null, includeArchived: true);
if (app == null)
{
return AppNotFound();
@@ -184,7 +190,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetPosApp(string appId)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true);
if (app == null)
{
return AppNotFound();
@@ -197,7 +203,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetCrowdfundApp(string appId)
{
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, includeArchived: true);
if (app == null)
{
return AppNotFound();
@@ -209,7 +215,7 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpDelete("~/api/v1/apps/{appId}")]
public async Task<IActionResult> DeleteApp(string appId)
{
var app = await _appService.GetApp(appId, null);
var app = await _appService.GetApp(appId, null, includeArchived: true);
if (app == null)
{
return AppNotFound();
@@ -293,6 +299,7 @@ namespace BTCPayServer.Controllers.Greenfield
return new AppDataBase
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.Name,
StoreId = appData.StoreDataId,
@@ -305,6 +312,7 @@ namespace BTCPayServer.Controllers.Greenfield
return new AppDataBase
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.AppName,
StoreId = appData.StoreId,
@@ -319,6 +327,7 @@ namespace BTCPayServer.Controllers.Greenfield
return new PointOfSaleAppData
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.Name,
StoreId = appData.StoreDataId,
@@ -387,6 +396,7 @@ namespace BTCPayServer.Controllers.Greenfield
return new CrowdfundAppData
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.Name,
StoreId = appData.StoreDataId,

View File

@@ -33,6 +33,7 @@ namespace BTCPayServer.Controllers.Greenfield
_storeRepository = storeRepository;
_userManager = userManager;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores")]
public Task<ActionResult<IEnumerable<Client.Models.StoreData>>> GetStores()
@@ -112,7 +113,7 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(FromModel(store));
}
internal static Client.Models.StoreData FromModel(Data.StoreData data)
internal static Client.Models.StoreData FromModel(StoreData data)
{
var storeBlob = data.GetStoreBlob();
return new Client.Models.StoreData
@@ -120,6 +121,7 @@ namespace BTCPayServer.Controllers.Greenfield
Id = data.Id,
Name = data.StoreName,
Website = data.StoreWebsite,
Archived = data.Archived,
SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
@@ -166,6 +168,7 @@ namespace BTCPayServer.Controllers.Greenfield
var blob = model.GetStoreBlob();
model.StoreName = restModel.Name;
model.StoreWebsite = restModel.Website;
model.Archived = restModel.Archived;
model.SpeedPolicy = restModel.SpeedPolicy;
model.SetDefaultPaymentId(defaultPaymentMethod);
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints

View File

@@ -76,28 +76,26 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListApps(
string storeId,
string sortOrder = null,
string sortOrderColumn = null
string sortOrderColumn = null,
bool archived = false
)
{
var store = GetCurrentStore();
var apps = await _appService.GetAllApps(GetUserId(), false, store.Id);
var apps = (await _appService.GetAllApps(GetUserId(), false, store.Id, archived))
.Where(app => app.Archived == archived);
if (sortOrder != null && sortOrderColumn != null)
{
apps = apps.OrderByDescending(app =>
{
switch (sortOrderColumn)
return sortOrderColumn switch
{
case nameof(app.AppName):
return app.AppName;
case nameof(app.StoreName):
return app.StoreName;
case nameof(app.AppType):
return app.AppType;
default:
return app.Id;
}
}).ToArray();
nameof(app.AppName) => app.AppName,
nameof(app.StoreName) => app.StoreName,
nameof(app.AppType) => app.AppType,
_ => app.Id
};
});
switch (sortOrder)
{
@@ -105,7 +103,7 @@ namespace BTCPayServer.Controllers
ViewData[$"{sortOrderColumn}SortOrder"] = "asc";
break;
case "asc":
apps = apps.Reverse().ToArray();
apps = apps.Reverse();
ViewData[$"{sortOrderColumn}SortOrder"] = "desc";
break;
}
@@ -113,7 +111,7 @@ namespace BTCPayServer.Controllers
return View(new ListAppsViewModel
{
Apps = apps
Apps = apps.ToArray()
});
}
@@ -161,7 +159,6 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
CreatedAppId = appData.Id;
var url = await type.ConfigureLink(appData);
return Redirect(url);
}
@@ -191,6 +188,36 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/archive")]
public async Task<IActionResult> ToggleArchive(string appId)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
var type = _appService.GetAppType(app.AppType);
if (type is null)
{
return UnprocessableEntity();
}
var archived = !app.Archived;
if (await _appService.SetArchived(app, archived))
{
TempData[WellKnownTempData.SuccessMessage] = archived
? "The app has been archived and will no longer appear in the apps list by default."
: "The app has been unarchived and will appear in the apps list by default again.";
}
else
{
TempData[WellKnownTempData.ErrorMessage] = $"Failed to {(archived ? "archive" : "unarchive")} the app.";
}
var url = await type.ConfigureLink(app);
return Redirect(url);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/upload-file")]
[IgnoreAntiforgeryToken]

View File

@@ -680,6 +680,7 @@ namespace BTCPayServer.Controllers
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
DefaultCurrency = storeBlob.DefaultCurrency,
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays,
Archived = store.Archived,
CanDelete = _Repo.CanDeleteStores()
};
@@ -827,6 +828,23 @@ namespace BTCPayServer.Controllers
});
}
[HttpPost("{storeId}/archive")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
public async Task<IActionResult> ToggleArchive(string storeId)
{
CurrentStore.Archived = !CurrentStore.Archived;
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived
? "The store has been archived and will no longer appear in the stores list by default."
: "The store has been unarchived and will appear in the stores list by default again.";
return RedirectToAction(nameof(GeneralSettings), new
{
storeId = CurrentStore.Id
});
}
[HttpGet("{storeId}/delete")]
public IActionResult DeleteStore(string storeId)
{

View File

@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
@@ -37,6 +35,26 @@ namespace BTCPayServer.Controllers
_rateFactory = rateFactory;
}
[HttpGet()]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
public async Task<IActionResult> ListStores(bool archived = false)
{
var stores = await _repo.GetStoresByUserId(GetUserId());
var vm = new ListStoresViewModel
{
Stores = stores
.Where(s => s.Archived == archived)
.Select(s => new ListStoresViewModel.StoreViewModel
{
StoreId = s.Id,
StoreName = s.StoreName,
Archived = s.Archived
}).ToList(),
Archived = archived
};
return View(vm);
}
[HttpGet("create")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
public async Task<IActionResult> CreateStore(bool skipWizard)

View File

@@ -21,6 +21,7 @@ namespace BTCPayServer.Models.AppViewModels
public DateTimeOffset Created { get; set; }
public AppData App { get; set; }
public StoreRepository.StoreRole Role { get; set; }
public bool Archived { get; set; }
}
public ListAppViewModel[] Apps { get; set; }

View File

@@ -39,6 +39,8 @@ namespace BTCPayServer.Models.StoreViewModels
public bool CanDelete { get; set; }
public bool Archived { get; set; }
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace BTCPayServer.Models.StoreViewModels;
public class ListStoresViewModel
{
public class StoreViewModel
{
public string StoreName { get; set; }
public string StoreId { get; set; }
public bool Archived { get; set; }
}
public List<StoreViewModel> Stores { get; set; } = new ();
public bool Archived { get; set; }
}

View File

@@ -10,7 +10,6 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
@@ -237,6 +236,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
StoreName = app.StoreData?.StoreName,
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.TargetCurrency),
AppName = app.Name,
Archived = app.Archived,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
@@ -346,6 +346,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
}
app.Name = vm.AppName;
app.Archived = vm.Archived;
var newSettings = new CrowdfundSettings
{
Title = vm.Title,

View File

@@ -116,5 +116,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
// NOTE: Improve validation if needed
public bool ModelWithMinimumData => Description != null && Title != null && TargetCurrency != null;
public bool Archived { get; set; }
}
}

View File

@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Rates;
@@ -61,8 +58,6 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
public DateTime? NextResetDate { get; set; }
}
public bool Started => !StartDate.HasValue || DateTime.UtcNow > StartDate;
public bool Ended => EndDate.HasValue && DateTime.UtcNow > EndDate;

View File

@@ -28,7 +28,6 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
@@ -537,6 +536,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.Currency),
Archived = app.Archived,
AppName = app.Name,
Title = settings.Title,
DefaultView = settings.DefaultView,
@@ -647,6 +647,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
};
app.Name = vm.AppName;
app.Archived = vm.Archived;
app.SetSettings(settings);
await _appService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";

View File

@@ -99,5 +99,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
[Display(Name = "Request customer data on checkout")]
public string FormId { get; set; }
public bool Archived { get; set; }
}
}

View File

@@ -244,7 +244,15 @@ namespace BTCPayServer.Services.Apps
return await ctx.SaveChangesAsync() == 1;
}
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null)
public async Task<bool> SetArchived(AppData appData, bool archived)
{
await using var ctx = _ContextFactory.CreateContext();
appData.Archived = archived;
ctx.Entry(appData).State = EntityState.Modified;
return await ctx.SaveChangesAsync() == 1;
}
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null, bool includeArchived = false)
{
await using var ctx = _ContextFactory.CreateContext();
var listApps = (await ctx.UserStore
@@ -254,6 +262,7 @@ namespace BTCPayServer.Services.Apps
.Include(store => store.StoreRole)
.Include(store => store.StoreData)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, (us, app) => new { us, app })
.Where(b => !b.app.Archived || b.app.Archived == includeArchived)
.OrderBy(b => b.app.Created)
.ToArrayAsync()).Select(arg => new ListAppsViewModel.ListAppViewModel
{
@@ -264,6 +273,7 @@ namespace BTCPayServer.Services.Apps
AppType = arg.app.AppType,
Id = arg.app.Id,
Created = arg.app.Created,
Archived = arg.app.Archived,
App = arg.app
}).ToArray();
@@ -300,11 +310,12 @@ namespace BTCPayServer.Services.Apps
return style;
}
public async Task<List<AppData>> GetApps(string[] appIds, bool includeStore = false)
public async Task<List<AppData>> GetApps(string[] appIds, bool includeStore = false, bool includeArchived = false)
{
await using var ctx = _ContextFactory.CreateContext();
var query = ctx.Apps
.Where(app => appIds.Contains(app.Id));
.Where(app => appIds.Contains(app.Id))
.Where(app => !app.Archived || app.Archived == includeArchived);
if (includeStore)
{
query = query.Include(data => data.StoreData);
@@ -320,13 +331,12 @@ namespace BTCPayServer.Services.Apps
return await query.ToListAsync();
}
public async Task<AppData?> GetApp(string appId, string? appType, bool includeStore = false)
public async Task<AppData?> GetApp(string appId, string? appType, bool includeStore = false, bool includeArchived = false)
{
await using var ctx = _ContextFactory.CreateContext();
var query = ctx.Apps
.Where(us => us.Id == appId &&
(appType == null || us.AppType == appType));
.Where(us => us.Id == appId && (appType == null || us.AppType == appType))
.Where(app => !app.Archived || app.Archived == includeArchived);
if (includeStore)
{
query = query.Include(data => data.StoreData);
@@ -339,7 +349,6 @@ namespace BTCPayServer.Services.Apps
return _storeRepository.FindStore(app.StoreDataId);
}
public static string SerializeTemplate(ViewPointOfSaleViewModel.Item[] items)
{
return JsonConvert.SerializeObject(items, Formatting.Indented, _defaultSerializer);

View File

@@ -31,9 +31,13 @@
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
@if (Model.ModelWithMinimumData)
@if (Model.Archived)
{
<a class="btn btn-secondary" asp-action="ViewCrowdfund" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank">View</a>
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False">Unarchive</button>
}
else if (Model.ModelWithMinimumData)
{
<a class="btn btn-secondary" asp-controller="UICrowdfund" asp-action="ViewCrowdfund" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank">View</a>
}
</div>
</div>
@@ -41,6 +45,7 @@
<partial name="_StatusMessage" />
<input type="hidden" asp-for="StoreId" />
<input type="hidden" asp-for="Archived" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row">
@@ -330,6 +335,18 @@
<div class="d-flex gap-3 mt-3">
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@Model.AppId">
<button type="submit" class="btn btn-outline-secondary" id="btn-archive-toggle">
@if (Model.Archived)
{
<span class="text-nowrap">Unarchive this app</span>
}
else
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this app so that it does not appear in the apps list by default">Archive this app</span>
}
</button>
</form>
<a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@Model.AppId" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(Model.AppName)</strong> and its settings will be permanently deleted." data-confirm-input="DELETE">Delete this app</a>
</div>

View File

@@ -18,13 +18,21 @@
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
<a class="btn btn-secondary" asp-action="ViewPointOfSale" asp-route-appId="@Model.Id" id="ViewApp" target="_blank">View</a>
@if (Model.Archived)
{
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False">Unarchive</button>
}
else
{
<a class="btn btn-secondary" asp-controller="UIPointOfSale" asp-action="ViewPointOfSale" asp-route-appId="@Model.Id" id="ViewApp" target="_blank">View</a>
}
</div>
</div>
<partial name="_StatusMessage" />
<input type="hidden" asp-for="StoreId" />
<input type="hidden" asp-for="Archived" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row">
@@ -266,6 +274,18 @@
<div class="d-flex gap-3 mt-3">
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@Model.Id">
<button type="submit" class="btn btn-outline-secondary" id="btn-archive-toggle">
@if (Model.Archived)
{
<span class="text-nowrap">Unarchive this app</span>
}
else
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this app so that it does not appear in the apps list by default">Archive this app</span>
}
</button>
</form>
<a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(Model.AppName)</strong> and its settings will be permanently deleted." data-confirm-input="DELETE">Delete this app</a>
</div>

View File

@@ -33,19 +33,6 @@
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName"
class="text-nowrap"
title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
Store
<span class="fa @(storeNameSortOrder == "asc" ? "fa-sort-alpha-desc" : storeNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th>
<a
asp-action="ListApps"
@@ -72,21 +59,36 @@
<span class="fa @(appTypeSortOrder == "asc" ? "fa-sort-alpha-desc" : appTypeSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
<th style="text-align:right">Actions</th>
<th>
<a
asp-action="ListApps"
asp-route-storeId="@Context.GetStoreData().Id"
asp-route-sortOrder="@(storeNameSortOrder ?? "asc")"
asp-route-sortOrderColumn="StoreName"
class="text-nowrap"
title="@(storeNameSortOrder == "desc" ? sortByDesc : sortByAsc)"
>
Store
<span class="fa @(storeNameSortOrder == "asc" ? "fa-sort-alpha-desc" : storeNameSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a>
</th>
</tr>
</thead>
<tbody>
@foreach (var app in Model.Apps)
{
var appType = AppService.GetAppType(app.AppType)!;
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = app.AppType };
var url = await appType.ConfigureLink(appData);
<tr>
<td>
<span permission="@Policies.CanModifyStoreSettings">
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@app.StoreId">@app.StoreName</a>
</span>
<span not-permission="@Policies.CanModifyStoreSettings">@app.StoreName</span>
<a href="@url" permission="@Policies.CanModifyStoreSettings" id="App-@app.Id">@app.AppName</a>
<span not-permission="@Policies.CanModifyStoreSettings">@app.AppName</span>
@if (app.Archived)
{
<span class="badge bg-info ms-2">archived</span>
}
</td>
<td>@app.AppName</td>
<td>
@AppService.GetAvailableAppTypes()[app.AppType]
@{
@@ -98,14 +100,11 @@
<span>@viewStyle</span>
}
</td>
<td class="text-end" permission="@Policies.CanModifyStoreSettings">
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
<span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(app.AppName)</strong> and its settings will be permanently deleted from your store <strong>@Html.Encode(app.StoreName)</strong>." data-confirm-input="DELETE">Delete</a>
</td>
<td class="text-end" no-permission="@Policies.CanModifyStoreSettings">
<td>
<span permission="@Policies.CanModifyStoreSettings">
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@app.StoreId">@app.StoreName</a>
</span>
<span not-permission="@Policies.CanModifyStoreSettings">@app.StoreName</span>
</td>
</tr>
}

View File

@@ -2,6 +2,7 @@
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@inject IFileService FileService;
@model GeneralSettingsViewModel
@{
@@ -172,14 +173,26 @@
<button type="submit" class="btn btn-primary mt-2" id="Save">Save</button>
</form>
<h3 class="mt-5 mb-3">Additional Actions</h3>
<div id="danger-zone" class="d-flex flex-wrap align-items-center gap-3 mb-5 mt-2">
<form asp-action="ToggleArchive" asp-route-storeId="@Model.Id" method="post" permission="@Policies.CanModifyStoreSettings">
<button type="submit" class="btn btn-outline-secondary" id="btn-archive-toggle">
@if (Model.Archived)
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Unarchive this store">Unarchive this store</span>
}
else
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this store so that it does not appear in the stores list by default">Archive this store</span>
}
</button>
</form>
@if (Model.CanDelete)
{
<h3 class="mt-5 mb-3">Additional Actions</h3>
<div id="danger-zone">
<a id="DeleteStore" class="btn btn-outline-danger mb-5 mt-2" asp-action="DeleteStore" asp-route-storeId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@Html.Encode(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>
<a id="DeleteStore" class="btn btn-outline-danger" asp-action="DeleteStore" asp-route-storeId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@Html.Encode(Model.StoreName)</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store." data-confirm-input="DELETE">Delete this store</a>
}
</div>
</div>
</div>
<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

@@ -4,6 +4,7 @@ namespace BTCPayServer.Views.Stores
{
public enum StoreNavPages
{
Index,
Create,
Dashboard,
General,

View File

@@ -0,0 +1,50 @@
@using BTCPayServer.Client
@model BTCPayServer.Models.StoreViewModels.ListStoresViewModel
@{
ViewData.SetActivePage(StoreNavPages.Index, Model.Archived ? "Archived Stores" : "Stores");
}
<partial name="_StatusMessage" />
<div class="d-sm-flex justify-content-between mb-2">
<h2 class="mb-0">
@ViewData["Title"]
</h2>
</div>
@if (Model.Stores.Any())
{
<table class="table table-hover">
<thead>
<tr>
<th>Store Name</th>
</tr>
</thead>
<tbody>
@foreach (var store in Model.Stores)
{
<tr>
<td>
<a
permission="@Policies.CanModifyStoreSettings"
asp-action="GeneralSettings" asp-controller="UIStores" asp-route-storeId="@store.StoreId"
id="Store-@store.StoreId">
@store.StoreName
</a>
<span not-permission="@Policies.CanModifyStoreSettings">@store.StoreName</span>
@if (store.Archived)
{
<span class="badge bg-info ms-2">archived</span>
}
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p class="text-secondary mt-3">
There are no stores yet.
<span permission="@Policies.CanModifyStoreSettingsUnscoped"><a asp-action="CreateStore">Create a store</a>.</span>
</p>
}

View File

@@ -770,6 +770,12 @@
"type": "string",
"example": "PointOfSale",
"description": "Type of the app which was created"
},
"archived": {
"type": "boolean",
"description": "If true, the app does not appear in the apps list by default.",
"default": false,
"nullable": true
}
}
},

View File

@@ -454,6 +454,11 @@
"default": 0.0,
"description": "Consider an invoice fully paid, even if the payment is missing 'x' % of the full amount."
},
"archived": {
"type": "boolean",
"default": false,
"description": "If true, the store does not appear in the stores list by default."
},
"anyoneCanCreateInvoice": {
"type": "boolean",
"default": false,