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 string RedirectUrl { get; set; } = null;
public bool? RedirectAutomatically { get; set; } = null; public bool? RedirectAutomatically { get; set; } = null;
public bool? RequiresRefundEmail { get; set; } = null; public bool? RequiresRefundEmail { get; set; } = null;
public bool? Archived { get; set; } = null;
public string FormId { get; set; } = null; public string FormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null; public string EmbeddedCSS { get; set; } = null;
public CheckoutType? CheckoutType { 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? DisplayPerksValue { get; set; } = null;
public bool? DisplayPerksRanking { get; set; } = null; public bool? DisplayPerksRanking { get; set; } = null;
public bool? SortPerksByPopularity { get; set; } = null; public bool? SortPerksByPopularity { get; set; } = null;
public bool? Archived { get; set; } = null;
public string[] Sounds { get; set; } = null; public string[] Sounds { get; set; } = null;
public string[] AnimationColors { 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 AppType { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? Archived { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
} }

View File

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

View File

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

View File

@@ -48,6 +48,7 @@ namespace BTCPayServer.Data
public IEnumerable<StoreSettingData> Settings { get; set; } public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; } public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; } public IEnumerable<StoreRole> StoreRoles { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade) 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") b.Property<string>("AppType")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Created") b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@@ -751,6 +754,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id") b.Property<string>("Id")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<string>("DefaultCrypto") b.Property<string>("DefaultCrypto")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" /> <PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" 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" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets> <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.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
Assert.Single(appList.Apps); Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps); Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName); Assert.Equal("test", app.AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); Assert.Equal(apps.CreatedAppId, app.Id);
Assert.True(appList.Apps[0].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId)); Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId); Assert.Equal(user.StoreId, app.StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id)); // Archive
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id)); redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].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); Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>(); appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps); Assert.Empty(appList.Apps);

View File

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

View File

@@ -1,8 +1,10 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Hosting; using BTCPayServer.Hosting;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers; using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Plugins.PointOfSale.Models;
@@ -123,12 +125,14 @@ donation:
price: 1.02 price: 1.02
custom: true custom: true
"; ";
vmpos.Currency = "EUR";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template)); vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>(); await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIPointOfSaleController>(); var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>(); 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 // apple shouldn't be available since we it's set to "disabled: true" above
Assert.Equal(2, vmview.Items.Length); Assert.Equal(2, vmview.Items.Length);
Assert.Equal("orange", vmview.Items[0].Title); Assert.Equal("orange", vmview.Items[0].Title);
@@ -139,6 +143,41 @@ donation:
// apple is not found // apple is not found
Assert.IsType<NotFoundResult>(publicApps Assert.IsType<NotFoundResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result); .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.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.GoToUrl(storeUrl); s.GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url); 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)] [Fact(Timeout = TestTimeout)]
@@ -978,6 +999,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SaveSettings")).Click(); s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text); Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
s.Driver.FindElement(By.Id("ViewApp")).Click(); s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles; var windows = s.Driver.WindowHandles;
@@ -1046,6 +1068,24 @@ namespace BTCPayServer.Tests
// We are only if explicitly going to / // We are only if explicitly going to /
s.GoToUrl("/"); s.GoToUrl("/");
Assert.Contains("Tea shop", s.Driver.PageSource); 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)] [Fact(Timeout = TestTimeout)]
@@ -1079,17 +1119,37 @@ namespace BTCPayServer.Tests
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''"); s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
s.Driver.FindElement(By.Id("SaveSettings")).Click(); s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text); Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
s.Driver.FindElement(By.Id("ViewApp")).Click(); s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles; var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count); Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]); s.Driver.SwitchTo().Window(windows[1]);
var cfUrl = s.Driver.Url;
Assert.Equal("Currently active!", Assert.Equal("Currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text); s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
s.Driver.Close(); s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]); 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)] [Fact(Timeout = TestTimeout)]

View File

@@ -6,7 +6,10 @@
@using BTCPayServer.Views.Wallets @using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions @using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client @using BTCPayServer.Client
@using BTCPayServer.Components.ThemeSwitch
@using BTCPayServer.Components.UIExtensionPoint
@using BTCPayServer.Services @using BTCPayServer.Services
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.CustodianAccounts @using BTCPayServer.Views.CustodianAccounts
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext; @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerEnvironment Env @inject BTCPayServerEnvironment Env
@@ -187,6 +190,14 @@
<span>Manage Plugins</span> <span>Manage Plugins</span>
</a> </a>
</li> </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> </ul>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Extensions @using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts @using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client @using BTCPayServer.Client
@using BTCPayServer.Components.MainLogo
@using BTCPayServer.Services @using BTCPayServer.Services
@using BTCPayServer.Views.Stores
@inject BTCPayServerEnvironment Env @inject BTCPayServerEnvironment Env
@inject IFileService FileService @inject IFileService FileService
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel @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="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> <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="StoreSelector">
<div id="StoreSelectorDropdown" class="dropdown only-for-js"> <div id="StoreSelectorDropdown" class="dropdown only-for-js">
@@ -64,8 +65,16 @@ else
} }
</li> </li>
} }
@if (Model.Options.Any())
{
<li><hr class="dropdown-divider"></li> <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> </ul>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -33,6 +33,7 @@ namespace BTCPayServer.Controllers.Greenfield
_storeRepository = storeRepository; _storeRepository = storeRepository;
_userManager = userManager; _userManager = userManager;
} }
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores")] [HttpGet("~/api/v1/stores")]
public Task<ActionResult<IEnumerable<Client.Models.StoreData>>> GetStores() public Task<ActionResult<IEnumerable<Client.Models.StoreData>>> GetStores()
@@ -112,7 +113,7 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(FromModel(store)); 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(); var storeBlob = data.GetStoreBlob();
return new Client.Models.StoreData return new Client.Models.StoreData
@@ -120,6 +121,7 @@ namespace BTCPayServer.Controllers.Greenfield
Id = data.Id, Id = data.Id,
Name = data.StoreName, Name = data.StoreName,
Website = data.StoreWebsite, Website = data.StoreWebsite,
Archived = data.Archived,
SupportUrl = storeBlob.StoreSupportUrl, SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy, SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(), DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
@@ -166,6 +168,7 @@ namespace BTCPayServer.Controllers.Greenfield
var blob = model.GetStoreBlob(); var blob = model.GetStoreBlob();
model.StoreName = restModel.Name; model.StoreName = restModel.Name;
model.StoreWebsite = restModel.Website; model.StoreWebsite = restModel.Website;
model.Archived = restModel.Archived;
model.SpeedPolicy = restModel.SpeedPolicy; model.SpeedPolicy = restModel.SpeedPolicy;
model.SetDefaultPaymentId(defaultPaymentMethod); 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 //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( public async Task<IActionResult> ListApps(
string storeId, string storeId,
string sortOrder = null, string sortOrder = null,
string sortOrderColumn = null string sortOrderColumn = null,
bool archived = false
) )
{ {
var store = GetCurrentStore(); 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) if (sortOrder != null && sortOrderColumn != null)
{ {
apps = apps.OrderByDescending(app => apps = apps.OrderByDescending(app =>
{ {
switch (sortOrderColumn) return sortOrderColumn switch
{ {
case nameof(app.AppName): nameof(app.AppName) => app.AppName,
return app.AppName; nameof(app.StoreName) => app.StoreName,
case nameof(app.StoreName): nameof(app.AppType) => app.AppType,
return app.StoreName; _ => app.Id
case nameof(app.AppType): };
return app.AppType; });
default:
return app.Id;
}
}).ToArray();
switch (sortOrder) switch (sortOrder)
{ {
@@ -105,7 +103,7 @@ namespace BTCPayServer.Controllers
ViewData[$"{sortOrderColumn}SortOrder"] = "asc"; ViewData[$"{sortOrderColumn}SortOrder"] = "asc";
break; break;
case "asc": case "asc":
apps = apps.Reverse().ToArray(); apps = apps.Reverse();
ViewData[$"{sortOrderColumn}SortOrder"] = "desc"; ViewData[$"{sortOrderColumn}SortOrder"] = "desc";
break; break;
} }
@@ -113,7 +111,7 @@ namespace BTCPayServer.Controllers
return View(new ListAppsViewModel return View(new ListAppsViewModel
{ {
Apps = apps Apps = apps.ToArray()
}); });
} }
@@ -161,7 +159,6 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = "App successfully created"; TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
CreatedAppId = appData.Id; CreatedAppId = appData.Id;
var url = await type.ConfigureLink(appData); var url = await type.ConfigureLink(appData);
return Redirect(url); return Redirect(url);
} }
@@ -191,6 +188,36 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId }); 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)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/upload-file")] [HttpPost("{appId}/upload-file")]
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]

View File

@@ -680,6 +680,7 @@ namespace BTCPayServer.Controllers
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes, InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
DefaultCurrency = storeBlob.DefaultCurrency, DefaultCurrency = storeBlob.DefaultCurrency,
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays, BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays,
Archived = store.Archived,
CanDelete = _Repo.CanDeleteStores() 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")] [HttpGet("{storeId}/delete")]
public IActionResult DeleteStore(string storeId) public IActionResult DeleteStore(string storeId)
{ {

View File

@@ -1,12 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
@@ -37,6 +35,26 @@ namespace BTCPayServer.Controllers
_rateFactory = rateFactory; _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")] [HttpGet("create")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
public async Task<IActionResult> CreateStore(bool skipWizard) public async Task<IActionResult> CreateStore(bool skipWizard)

View File

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

View File

@@ -39,6 +39,8 @@ namespace BTCPayServer.Models.StoreViewModels
public bool CanDelete { get; set; } public bool CanDelete { get; set; }
public bool Archived { get; set; }
[Display(Name = "Allow anyone to create invoice")] [Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; } 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.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Crowdfund.Models; using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
@@ -237,6 +236,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
StoreName = app.StoreData?.StoreName, StoreName = app.StoreData?.StoreName,
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.TargetCurrency), StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.TargetCurrency),
AppName = app.Name, AppName = app.Name,
Archived = app.Archived,
Enabled = settings.Enabled, Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount, EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate, StartDate = settings.StartDate,
@@ -346,6 +346,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
} }
app.Name = vm.AppName; app.Name = vm.AppName;
app.Archived = vm.Archived;
var newSettings = new CrowdfundSettings var newSettings = new CrowdfundSettings
{ {
Title = vm.Title, Title = vm.Title,

View File

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

View File

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

View File

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

View File

@@ -99,5 +99,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
[Display(Name = "Request customer data on checkout")] [Display(Name = "Request customer data on checkout")]
public string FormId { get; set; } 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; 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(); await using var ctx = _ContextFactory.CreateContext();
var listApps = (await ctx.UserStore var listApps = (await ctx.UserStore
@@ -254,6 +262,7 @@ namespace BTCPayServer.Services.Apps
.Include(store => store.StoreRole) .Include(store => store.StoreRole)
.Include(store => store.StoreData) .Include(store => store.StoreData)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, (us, app) => new { us, app }) .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) .OrderBy(b => b.app.Created)
.ToArrayAsync()).Select(arg => new ListAppsViewModel.ListAppViewModel .ToArrayAsync()).Select(arg => new ListAppsViewModel.ListAppViewModel
{ {
@@ -264,6 +273,7 @@ namespace BTCPayServer.Services.Apps
AppType = arg.app.AppType, AppType = arg.app.AppType,
Id = arg.app.Id, Id = arg.app.Id,
Created = arg.app.Created, Created = arg.app.Created,
Archived = arg.app.Archived,
App = arg.app App = arg.app
}).ToArray(); }).ToArray();
@@ -300,11 +310,12 @@ namespace BTCPayServer.Services.Apps
return style; 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(); await using var ctx = _ContextFactory.CreateContext();
var query = ctx.Apps 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) if (includeStore)
{ {
query = query.Include(data => data.StoreData); query = query.Include(data => data.StoreData);
@@ -320,13 +331,12 @@ namespace BTCPayServer.Services.Apps
return await query.ToListAsync(); 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(); await using var ctx = _ContextFactory.CreateContext();
var query = ctx.Apps var query = ctx.Apps
.Where(us => us.Id == appId && .Where(us => us.Id == appId && (appType == null || us.AppType == appType))
(appType == null || us.AppType == appType)); .Where(app => !app.Archived || app.Archived == includeArchived);
if (includeStore) if (includeStore)
{ {
query = query.Include(data => data.StoreData); query = query.Include(data => data.StoreData);
@@ -339,7 +349,6 @@ namespace BTCPayServer.Services.Apps
return _storeRepository.FindStore(app.StoreDataId); return _storeRepository.FindStore(app.StoreDataId);
} }
public static string SerializeTemplate(ViewPointOfSaleViewModel.Item[] items) public static string SerializeTemplate(ViewPointOfSaleViewModel.Item[] items)
{ {
return JsonConvert.SerializeObject(items, Formatting.Indented, _defaultSerializer); return JsonConvert.SerializeObject(items, Formatting.Indented, _defaultSerializer);

View File

@@ -31,9 +31,13 @@
<h2 class="mb-0">@ViewData["Title"]</h2> <h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0"> <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> <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>
</div> </div>
@@ -41,6 +45,7 @@
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<input type="hidden" asp-for="StoreId" /> <input type="hidden" asp-for="StoreId" />
<input type="hidden" asp-for="Archived" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row"> <div class="row">
@@ -330,6 +335,18 @@
<div class="d-flex gap-3 mt-3"> <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> <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> <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> </div>

View File

@@ -18,13 +18,21 @@
<h2 class="mb-0">@ViewData["Title"]</h2> <h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0"> <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> <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>
</div> </div>
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<input type="hidden" asp-for="StoreId" /> <input type="hidden" asp-for="StoreId" />
<input type="hidden" asp-for="Archived" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row"> <div class="row">
@@ -266,6 +274,18 @@
<div class="d-flex gap-3 mt-3"> <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> <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> <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> </div>

View File

@@ -33,19 +33,6 @@
<table class="table table-hover table-responsive-md"> <table class="table table-hover table-responsive-md">
<thead> <thead>
<tr> <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> <th>
<a <a
asp-action="ListApps" asp-action="ListApps"
@@ -72,21 +59,36 @@
<span class="fa @(appTypeSortOrder == "asc" ? "fa-sort-alpha-desc" : appTypeSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" /> <span class="fa @(appTypeSortOrder == "asc" ? "fa-sort-alpha-desc" : appTypeSortOrder == "desc" ? "fa-sort-alpha-asc" : "fa-sort")" />
</a> </a>
</th> </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> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var app in Model.Apps) @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> <tr>
<td> <td>
<span permission="@Policies.CanModifyStoreSettings"> <a href="@url" permission="@Policies.CanModifyStoreSettings" id="App-@app.Id">@app.AppName</a>
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@app.StoreId">@app.StoreName</a> <span not-permission="@Policies.CanModifyStoreSettings">@app.AppName</span>
</span> @if (app.Archived)
{
<span not-permission="@Policies.CanModifyStoreSettings">@app.StoreName</span> <span class="badge bg-info ms-2">archived</span>
}
</td> </td>
<td>@app.AppName</td>
<td> <td>
@AppService.GetAvailableAppTypes()[app.AppType] @AppService.GetAvailableAppTypes()[app.AppType]
@{ @{
@@ -98,14 +100,11 @@
<span>@viewStyle</span> <span>@viewStyle</span>
} }
</td> </td>
<td class="text-end" permission="@Policies.CanModifyStoreSettings"> <td>
<span permission="@Policies.CanModifyStoreSettings">
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a> <a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@app.StoreId">@app.StoreName</a>
<span> - </span> </span>
<span not-permission="@Policies.CanModifyStoreSettings">@app.StoreName</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> </td>
</tr> </tr>
} }

View File

@@ -2,6 +2,7 @@
@using BTCPayServer.TagHelpers @using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts @using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@inject IFileService FileService; @inject IFileService FileService;
@model GeneralSettingsViewModel @model GeneralSettingsViewModel
@{ @{
@@ -172,15 +173,27 @@
<button type="submit" class="btn btn-primary mt-2" id="Save">Save</button> <button type="submit" class="btn btn-primary mt-2" id="Save">Save</button>
</form> </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) @if (Model.CanDelete)
{ {
<h3 class="mt-5 mb-3">Additional Actions</h3> <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 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>
} }
</div> </div>
</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"))" /> <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 public enum StoreNavPages
{ {
Index,
Create, Create,
Dashboard, Dashboard,
General, 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", "type": "string",
"example": "PointOfSale", "example": "PointOfSale",
"description": "Type of the app which was created" "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, "default": 0.0,
"description": "Consider an invoice fully paid, even if the payment is missing 'x' % of the full amount." "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": { "anyoneCanCreateInvoice": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,