Refactor plugin apps (#4780)

* Refactor plugins

* Add missing names to view models

* Cleanups

* Replace SalesAppBaseType by two interfaces

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Nicolas Dorier 2023-03-20 10:39:26 +09:00 committed by GitHub
parent 53f3758abc
commit 04ba1430ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 163 additions and 160 deletions

View file

@ -623,7 +623,7 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = PointOfSaleApp.AppType;
var appType = PointOfSaleAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);

View file

@ -37,7 +37,7 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var appType = CrowdfundApp.AppType;
var appType = CrowdfundAppType.AppType;
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
Assert.Equal(appType, vm.SelectedAppType);
Assert.Null(vm.AppName);
@ -77,7 +77,7 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
var appType = CrowdfundApp.AppType;
var appType = CrowdfundAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
@ -169,7 +169,7 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var crowdfund = user.GetController<UICrowdfundController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = CrowdfundApp.AppType;
var appType = CrowdfundAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);

View file

@ -31,7 +31,7 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
var appType = PointOfSaleApp.AppType;
var appType = PointOfSaleAppType.AppType;
vm.AppName = "test";
vm.SelectedAppType = appType;
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);

View file

@ -1955,7 +1955,7 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>();
var appType = PointOfSaleApp.AppType;
var appType = PointOfSaleAppType.AppType;
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
Assert.Equal(appType, vm.SelectedAppType);
Assert.Null(vm.AppName);

View file

@ -7,6 +7,8 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Components.AppSales;
@ -27,6 +29,9 @@ public class AppSales : ViewComponent
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{
var type = _appService.GetAppType(appType);
if (type is not IHasSaleStatsAppType salesAppType || type is not AppBaseType appBaseType)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppSalesViewModel
{
Id = appId,
@ -42,7 +47,8 @@ public class AppSales : ViewComponent
vm.SalesCount = stats.SalesCount;
vm.Series = stats.Series;
vm.AppType = app.AppType;
vm.AppUrl = await _appService.ConfigureLink(app, app.AppType);
vm.AppUrl = await appBaseType.ConfigureLink(app);
vm.Name = app.Name;
return View(vm);
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.AppSales;

View file

@ -3,7 +3,7 @@
@using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppSales.AppSalesViewModel
@{
var label = Model.AppType == CrowdfundApp.AppType ? "Contributions" : "Sales";
var label = Model.AppType == CrowdfundAppType.AppType ? "Contributions" : "Sales";
}
<div id="AppSales-@Model.Id" class="widget app-sales">

View file

@ -7,6 +7,8 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Components.AppTopItems;
@ -19,8 +21,12 @@ public class AppTopItems : ViewComponent
_appService = appService;
}
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType = null)
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{
var type = _appService.GetAppType(appType);
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppTopItemsViewModel
{
Id = appId,
@ -36,7 +42,8 @@ public class AppTopItems : ViewComponent
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
vm.Entries = entries.ToList();
vm.AppType = app.AppType;
vm.AppUrl = await _appService.ConfigureLink(app, app.AppType);
vm.AppUrl = await appBaseType.ConfigureLink(app);
vm.Name = app.Name;
return View(vm);
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.AppTopItems;

View file

@ -1,12 +1,12 @@
@using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
@{
var label = Model.AppType == CrowdfundApp.AppType ? "contribution" : "sale";
var label = Model.AppType == CrowdfundAppType.AppType ? "contribution" : "sale";
}
<div id="AppTopItems-@Model.Id" class="widget app-top-items">
<header class="mb-3">
<h3>Top @(Model.AppType == CrowdfundApp.AppType ? "Perks" : "Items")</h3>
<h3>Top @(Model.AppType == CrowdfundAppType.AppType ? "Perks" : "Items")</h3>
@if (!string.IsNullOrEmpty(Model.AppUrl))
{
<a href="@Model.AppUrl">View All</a>

View file

@ -66,7 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = CrowdfundApp.AppType
AppType = CrowdfundAppType.AppType
};
appData.SetSettings(ToCrowdfundSettings(request));
@ -97,7 +97,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = PointOfSaleApp.AppType
AppType = PointOfSaleAppType.AppType
};
appData.SetSettings(ToPointOfSaleSettings(request));
@ -111,7 +111,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, PointOfSaleApp.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
{
return AppNotFound();
@ -184,7 +184,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, PointOfSaleApp.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
{
return AppNotFound();
@ -197,7 +197,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, CrowdfundApp.AppType);
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
if (app == null)
{
return AppNotFound();
@ -245,7 +245,7 @@ namespace BTCPayServer.Controllers.Greenfield
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
NotificationUrl = request.NotificationUrl?.Trim(),
Tagline = request.Tagline?.Trim(),
PerksTemplate = request.PerksTemplate != null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate?.Trim(), request.TargetCurrency)) : null,
PerksTemplate = request.PerksTemplate is not null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate.Trim(), request.TargetCurrency!)) : null,
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
DisqusShortname = request.DisqusShortname?.Trim(),
@ -363,7 +363,8 @@ namespace BTCPayServer.Controllers.Greenfield
{
try
{
_appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency));
// Just checking if we can serialize, we don't care about the currency
_appService.SerializeTemplate(_appService.Parse(request.Template, "USD"));
}
catch
{
@ -452,7 +453,8 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, request.TargetCurrency));
// Just checking if we can serialize, we don't care about the currency
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, "USD"));
}
catch
{

View file

@ -21,7 +21,7 @@ namespace BTCPayServer.Controllers
app.StoreData = GetCurrentStore();
return ViewComponent("AppTopItems", new { appId = app.Id });
return ViewComponent("AppTopItems", new { appId = app.Id, appType = app.AppType });
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
@ -33,7 +33,7 @@ namespace BTCPayServer.Controllers
return NotFound();
app.StoreData = GetCurrentStore();
return ViewComponent("AppSales", new { appId = app.Id });
return ViewComponent("AppSales", new { appId = app.Id, appType = app.AppType });
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]

View file

@ -124,8 +124,8 @@ namespace BTCPayServer.Controllers
{
var store = GetCurrentStore();
vm.StoreId = store.Id;
var types = _appService.GetAvailableAppTypes();
if (!types.ContainsKey(vm.SelectedAppType))
var type = _appService.GetAppType(vm.SelectedAppType);
if (type is null)
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
if (!ModelState.IsValid)
@ -147,7 +147,8 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
CreatedAppId = appData.Id;
var url = await _appService.ConfigureLink(appData, vm.SelectedAppType);
var url = await type.ConfigureLink(appData);
return Redirect(url);
}

View file

@ -256,12 +256,12 @@ namespace BTCPayServer
PointOfSaleSettings posS = null;
switch (app.AppType)
{
case CrowdfundApp.AppType:
case CrowdfundAppType.AppType:
var cfS = app.GetSettings<CrowdfundSettings>();
currencyCode = cfS.TargetCurrency;
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
break;
case PointOfSaleApp.AppType:
case PointOfSaleAppType.AppType:
posS = app.GetSettings<PointOfSaleSettings>();
currencyCode = posS.Currency;
items = _appService.Parse(posS.Template, posS.Currency);
@ -287,7 +287,7 @@ namespace BTCPayServer
return NotFound();
}
}
else if (app.AppType == PointOfSaleApp.AppType && posS?.ShowCustomAmount is not true)
else if (app.AppType == PointOfSaleAppType.AppType && posS?.ShowCustomAmount is not true)
{
return NotFound();
}
@ -400,7 +400,7 @@ namespace BTCPayServer
var redirectUrl = app?.AppType switch
{
PointOfSaleApp.AppType => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
PointOfSaleAppType.AppType => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
_ => null
};

View file

@ -42,6 +42,8 @@ namespace BTCPayServer.Controllers
return View(vm);
var userId = GetUserId();
if (userId is null)
return NotFound();
var apps = await _appService.GetAllApps(userId, false, store.Id);
foreach (var app in apps)
{

View file

@ -37,11 +37,11 @@ namespace BTCPayServer.HostedServices
{
switch (data.AppType)
{
case PointOfSaleApp.AppType:
case PointOfSaleAppType.AppType:
var possettings = data.GetSettings<PointOfSaleSettings>();
return (Data: data, Settings: (object)possettings,
Items: _appService.Parse(possettings.Template, possettings.Currency));
case CrowdfundApp.AppType:
case CrowdfundAppType.AppType:
var cfsettings = data.GetSettings<CrowdfundSettings>();
return (Data: data, Settings: (object)cfsettings,
Items: _appService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
@ -68,11 +68,11 @@ namespace BTCPayServer.HostedServices
switch (valueTuple.Data.AppType)
{
case PointOfSaleApp.AppType:
case PointOfSaleAppType.AppType:
((PointOfSaleSettings)valueTuple.Settings).Template =
_appService.SerializeTemplate(valueTuple.Items);
break;
case CrowdfundApp.AppType:
case CrowdfundAppType.AppType:
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
_appService.SerializeTemplate(valueTuple.Items);
break;

View file

@ -470,7 +470,7 @@ WHERE cte.""Id""=p.""Id""
string newTemplate;
switch (app.AppType)
{
case CrowdfundApp.AppType:
case CrowdfundAppType.AppType:
var settings1 = app.GetSettings<CrowdfundSettings>();
if (string.IsNullOrEmpty(settings1.TargetCurrency))
{
@ -486,7 +486,7 @@ WHERE cte.""Id""=p.""Id""
};
break;
case PointOfSaleApp.AppType:
case PointOfSaleAppType.AppType:
var settings2 = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(settings2.Currency))

View file

@ -33,7 +33,7 @@ namespace BTCPayServer.Models.AppViewModels
private void SetApps(AppService appService)
{
var defaultAppType = PointOfSaleApp.AppType;
var defaultAppType = PointOfSaleAppType.AppType;
var choices = appService.GetAvailableAppTypes().Select(pair =>
new SelectListItem(pair.Value, pair.Key, pair.Key == defaultAppType));

View file

@ -36,7 +36,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
StoreRepository storeRepository,
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
CrowdfundApp app)
CrowdfundAppType app)
{
_currencies = currencies;
_appService = appService;
@ -53,21 +53,21 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
private readonly UserManager<ApplicationUser> _userManager;
private readonly CrowdfundApp _app;
private readonly CrowdfundAppType _app;
[HttpGet("/")]
[HttpGet("/apps/{appId}/crowdfund")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
[DomainMappingConstraint(CrowdfundApp.AppType)]
[DomainMappingConstraint(CrowdfundAppType.AppType)]
public async Task<IActionResult> ViewCrowdfund(string appId)
{
var app = await _appService.GetApp(appId, CrowdfundApp.AppType, true);
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundApp.AppType) != null;
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundAppType.AppType) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
if (!hasEnoughSettingsToLoad)
@ -92,17 +92,17 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(CrowdfundApp.AppType)]
[DomainMappingConstraint(CrowdfundAppType.AppType)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _appService.GetApp(appId, CrowdfundApp.AppType, true);
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundApp.AppType) != null;
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundAppType.AppType) != null;
if (!settings.Enabled && !isAdmin)
{
@ -398,7 +398,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
{
var app = await _appService.GetApp(appId, CrowdfundApp.AppType, true);
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true);
if (app is null)
{
return null;

View file

@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
@ -9,6 +10,7 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
@ -29,14 +31,14 @@ namespace BTCPayServer.Plugins.Crowdfund
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("Crowdfund/NavExtension", "apps-nav"));
services.AddSingleton<CrowdfundApp>();
services.AddSingleton<IApp>(provider => provider.GetRequiredService<CrowdfundApp>());
services.AddSingleton<CrowdfundAppType>();
services.AddSingleton<AppBaseType, CrowdfundAppType>();
base.Execute(services);
}
}
public class CrowdfundApp: IApp
public class CrowdfundAppType: AppBaseType, IHasSaleStatsAppType, IHasItemStatsAppType
{
private readonly LinkGenerator _linkGenerator;
private readonly IOptions<BTCPayServerOptions> _options;
@ -45,12 +47,8 @@ namespace BTCPayServer.Plugins.Crowdfund
private readonly HtmlSanitizer _htmlSanitizer;
private readonly InvoiceRepository _invoiceRepository;
public const string AppType = "Crowdfund";
public string Description => AppType;
public string Type => AppType;
public bool SupportsSalesStats => true;
public bool SupportsItemStats => true;
public CrowdfundApp(
public CrowdfundAppType(
LinkGenerator linkGenerator,
IOptions<BTCPayServerOptions> options,
InvoiceRepository invoiceRepository,
@ -58,6 +56,7 @@ namespace BTCPayServer.Plugins.Crowdfund
CurrencyNameTable currencyNameTable,
HtmlSanitizer htmlSanitizer)
{
Description = Type = AppType;
_linkGenerator = linkGenerator;
_options = options;
_displayFormatter = displayFormatter;
@ -66,10 +65,10 @@ namespace BTCPayServer.Plugins.Crowdfund
_invoiceRepository = invoiceRepository;
}
public Task<string> ConfigureLink(AppData app)
public override Task<string> ConfigureLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UICrowdfundController.UpdateCrowdfund),
"UICrowdfund", new { appId = app.Id }, _options.Value.RootPath));
"UICrowdfund", new { appId = app.Id }, _options.Value.RootPath)!);
}
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
@ -114,14 +113,14 @@ namespace BTCPayServer.Plugins.Crowdfund
return Task.FromResult<IEnumerable<ItemStats>>(perkCount);
}
public async Task<object> GetInfo(AppData appData)
public override async Task<object?> GetInfo(AppData appData)
{
var settings = appData.GetSettings<CrowdfundSettings>();
var resetEvery = settings.StartDate.HasValue ? settings.ResetEvery : CrowdfundResetEvery.Never;
DateTime? lastResetDate = null;
DateTime? nextResetDate = null;
if (resetEvery != CrowdfundResetEvery.Never)
if (resetEvery != CrowdfundResetEvery.Never && settings.StartDate is not null)
{
lastResetDate = settings.StartDate.Value;
@ -187,7 +186,7 @@ namespace BTCPayServer.Plugins.Crowdfund
.ToList();
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
newPerksOrder.AddRange(remainingPerks);
perks = newPerksOrder.ToArray();
perks = newPerksOrder.ToArray()!;
}
var store = appData.StoreData;
@ -247,17 +246,17 @@ namespace BTCPayServer.Plugins.Crowdfund
};
}
public Task SetDefaultSettings(AppData appData, string defaultCurrency)
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
{
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
appData.SetSettings(emptyCrowdfund);
return Task.CompletedTask;
}
public Task<string> ViewLink(AppData app)
public override Task<string> ViewLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UICrowdfundController.ViewCrowdfund),
"UICrowdfund", new {appId = app.Id}, _options.Value.RootPath));
"UICrowdfund", new {appId = app.Id}, _options.Value.RootPath)!);
}
private static bool IsPaid(InvoiceEntity entity)

View file

@ -64,11 +64,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
[HttpGet("/")]
[HttpGet("/apps/{appId}/pos")]
[HttpGet("/apps/{appId}/pos/{viewType?}")]
[DomainMappingConstraint(PointOfSaleApp.AppType)]
[DomainMappingConstraint(PointOfSaleAppType.AppType)]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
@ -121,7 +121,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
[HttpPost("/apps/{appId}/pos/{viewType?}")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(PointOfSaleApp.AppType)]
[DomainMappingConstraint(PointOfSaleAppType.AppType)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> ViewPointOfSale(string appId,
@ -137,7 +137,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
{
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
@ -334,7 +334,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> POSForm(string appId, PosViewType? viewType = null)
{
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
return NotFound();
@ -380,7 +380,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> POSFormSubmit(string appId, FormViewModel viewModel, PosViewType? viewType = null)
{
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
return NotFound();

View file

@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -28,7 +29,7 @@ namespace BTCPayServer.Plugins.PointOfSale
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("PointOfSale/NavExtension", "apps-nav"));
services.AddSingleton<IApp,PointOfSaleApp>();
services.AddSingleton<AppBaseType, PointOfSaleAppType>();
base.Execute(services);
}
}
@ -45,34 +46,37 @@ namespace BTCPayServer.Plugins.PointOfSale
Print
}
public class PointOfSaleApp: IApp
public class PointOfSaleAppType: AppBaseType, IHasSaleStatsAppType, IHasItemStatsAppType
{
private readonly LinkGenerator _linkGenerator;
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
private readonly DisplayFormatter _displayFormatter;
private readonly HtmlSanitizer _htmlSanitizer;
public const string AppType = "PointOfSale";
public string Description => "Point of Sale";
public string Type => AppType;
public bool SupportsSalesStats => true;
public bool SupportsItemStats => true;
public PointOfSaleApp(
public PointOfSaleAppType(
LinkGenerator linkGenerator,
IOptions<BTCPayServerOptions> btcPayServerOptions,
DisplayFormatter displayFormatter,
HtmlSanitizer htmlSanitizer)
{
Type = AppType;
Description = "Point of Sale";
_linkGenerator = linkGenerator;
_btcPayServerOptions = btcPayServerOptions;
_displayFormatter = displayFormatter;
_htmlSanitizer = htmlSanitizer;
}
public Task<string> ConfigureLink(AppData app)
public override Task<string> ConfigureLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIPointOfSaleController.UpdatePointOfSale),
"UIPointOfSale", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath));
"UIPointOfSale", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath)!);
}
public override Task<object?> GetInfo(AppData appData)
{
return Task.FromResult<object?>(null);
}
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
@ -114,22 +118,17 @@ namespace BTCPayServer.Plugins.PointOfSale
return Task.FromResult<IEnumerable<ItemStats>>(itemCount);
}
public Task<object> GetInfo(AppData appData)
{
throw new NotImplementedException();
}
public Task SetDefaultSettings(AppData appData, string defaultCurrency)
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
{
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
appData.SetSettings(empty);
return Task.CompletedTask;
}
public Task<string> ViewLink(AppData app)
public override Task<string> ViewLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIPointOfSaleController.ViewPointOfSale),
"UIPointOfSale", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath));
"UIPointOfSale", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath)!);
}
}
}

View file

@ -31,7 +31,7 @@ namespace BTCPayServer.Services.Apps
{
public class AppService
{
private readonly IEnumerable<IApp> _apps;
private readonly Dictionary<string, AppBaseType> _appTypes;
readonly ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository;
readonly CurrencyNameTable _Currencies;
@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Apps
private readonly HtmlSanitizer _HtmlSanitizer;
public CurrencyNameTable Currencies => _Currencies;
public AppService(
IEnumerable<IApp> apps,
IEnumerable<AppBaseType> apps,
ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository,
CurrencyNameTable currencies,
@ -48,7 +48,7 @@ namespace BTCPayServer.Services.Apps
StoreRepository storeRepository,
HtmlSanitizer htmlSanitizer)
{
_apps = apps;
_appTypes = apps.ToDictionary(a => a.Type, a=> a);
_ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository;
_Currencies = currencies;
@ -56,40 +56,33 @@ namespace BTCPayServer.Services.Apps
_HtmlSanitizer = htmlSanitizer;
_displayFormatter = displayFormatter;
}
#nullable enable
public Dictionary<string, string> GetAvailableAppTypes()
{
return _apps.ToDictionary(app => app.Type, app => app.Description);
}
public Task<string> ConfigureLink(AppData app, string vmSelectedAppType)
{
return GetAppForType(vmSelectedAppType).ConfigureLink(app);
return _appTypes.ToDictionary(app => app.Key, app => app.Value.Description);
}
private IApp GetAppForType(string appType)
public AppBaseType? GetAppType(string appType)
{
return _apps.First(app => app.Type == appType);
_appTypes.TryGetValue(appType, out var a);
return a;
}
public async Task<object> GetInfo(string appId)
public async Task<object?> GetInfo(string appId)
{
var appData = await GetApp(appId, null);
if (appData is null)
{
return null;
}
var app = GetAppForType(appData.AppType);
if (app is null)
{
var appType = GetAppType(appData.AppType);
if (appType is null)
return null;
}
return app.GetInfo(appData);
return appType.GetInfo(appData);
}
public async Task<IEnumerable<ItemStats>> GetItemStats(AppData appData)
{
if (GetAppType(appData.AppType) is not IHasItemStatsAppType salesType)
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository,appData,
null, new []
{
@ -97,7 +90,7 @@ namespace BTCPayServer.Services.Apps
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
InvoiceState.ToString(InvoiceStatusLegacy.Complete)
});
return await GetAppForType(appData.AppType).GetItemStats(appData, paidInvoices);
return await salesType.GetItemStats(appData, paidInvoices);
}
public static Task<SalesStats> GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items,
@ -136,6 +129,8 @@ namespace BTCPayServer.Services.Apps
public async Task<SalesStats> GetSalesStats(AppData app, int numberOfDays = 7)
{
if (GetAppType(app.AppType) is not IHasSaleStatsAppType salesType)
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays),
new []
{
@ -143,12 +138,13 @@ namespace BTCPayServer.Services.Apps
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
InvoiceState.ToString(InvoiceStatusLegacy.Complete)
});
return await GetAppForType(app.AppType).GetSalesStats(app, paidInvoices, numberOfDays);
return await salesType.GetSalesStats(app, paidInvoices, numberOfDays);
}
public class InvoiceStatsItem
{
public string ItemCode { get; set; }
public string ItemCode { get; set; } = string.Empty;
public decimal FiatPrice { get; set; }
public DateTime Date { get; set; }
}
@ -204,8 +200,8 @@ namespace BTCPayServer.Services.Apps
public static string GetAppOrderId(string appType, string appId) =>
appType switch
{
CrowdfundApp.AppType => $"crowdfund-app_{appId}",
PointOfSaleApp.AppType => $"pos-app_{appId}",
CrowdfundAppType.AppType => $"crowdfund-app_{appId}",
PointOfSaleAppType.AppType => $"pos-app_{appId}",
_ => $"{appType}_{appId}"
};
@ -215,7 +211,7 @@ namespace BTCPayServer.Services.Apps
return invoice.GetInternalTags("APP#");
}
public static async Task<InvoiceEntity[]> GetInvoicesForApp(InvoiceRepository invoiceRepository, AppData appData, DateTimeOffset? startDate = null, string[] status = null)
public static async Task<InvoiceEntity[]> GetInvoicesForApp(InvoiceRepository invoiceRepository, AppData appData, DateTimeOffset? startDate = null, string[]? status = null)
{
var invoices = await invoiceRepository.GetInvoices(new InvoiceQuery
{
@ -252,7 +248,7 @@ 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<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null)
{
await using var ctx = _ContextFactory.CreateContext();
var listApps = await ctx.UserStore
@ -294,7 +290,7 @@ namespace BTCPayServer.Services.Apps
string style;
switch (appType)
{
case PointOfSaleApp.AppType:
case PointOfSaleAppType.AppType:
var settings = app.GetSettings<PointOfSaleSettings>();
string posViewStyle = (settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView).ToString();
style = typeof(PosViewType).DisplayName(posViewStyle);
@ -328,7 +324,7 @@ 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)
{
await using var ctx = _ContextFactory.CreateContext();
var query = ctx.Apps
@ -342,7 +338,7 @@ namespace BTCPayServer.Services.Apps
return await query.FirstOrDefaultAsync();
}
public Task<StoreData> GetStore(AppData app)
public Task<StoreData?> GetStore(AppData app)
{
return _storeRepository.FindStore(app.StoreDataId);
}
@ -354,7 +350,7 @@ namespace BTCPayServer.Services.Apps
{
var itemNode = new YamlMappingNode();
itemNode.Add("title", new YamlScalarNode(item.Title));
if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup && item.Price.Value is not null)
itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant()));
if (!string.IsNullOrEmpty(item.Description))
{
@ -436,8 +432,11 @@ namespace BTCPayServer.Services.Apps
case "false":
case null:
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed;
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
if (pValue?.Value.Value is not null)
{
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
}
break;
}
@ -464,13 +463,13 @@ namespace BTCPayServer.Services.Apps
{
return Parse(htmlSanitizer, displayFormatter, template, currency).Where(c => !c.Disabled).ToArray();
}
#nullable restore
private class PosHolder
{
private readonly HtmlSanitizer _htmlSanitizer;
public PosHolder(HtmlSanitizer htmlSanitizer)
public PosHolder(
HtmlSanitizer htmlSanitizer)
{
_htmlSanitizer = htmlSanitizer;
}
@ -506,8 +505,8 @@ namespace BTCPayServer.Services.Apps
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, string type = null)
#nullable enable
public async Task<AppData?> GetAppDataIfOwner(string userId, string appId, string? type = null)
{
if (userId == null || appId == null)
return null;
@ -542,7 +541,7 @@ namespace BTCPayServer.Services.Apps
await ctx.SaveChangesAsync();
}
private static bool TryParseJson(string json, out JObject result)
private static bool TryParseJson(string json, [MaybeNullWhen(false)] out JObject result)
{
result = null;
try
@ -584,7 +583,7 @@ namespace BTCPayServer.Services.Apps
public async Task SetDefaultSettings(AppData appData, string defaultCurrency)
{
var app = GetAppForType(appData.AppType);
var app = GetAppType(appData.AppType);
if (app is null)
{
appData.SetSettings(null);
@ -597,19 +596,10 @@ namespace BTCPayServer.Services.Apps
public async Task<string?> ViewLink(AppData app)
{
var appType = GetAppForType(app.AppType);
var appType = GetAppType(app.AppType);
return await appType?.ViewLink(app)!;
}
#nullable restore
public bool SupportsSalesStats(AppData app)
{
return GetAppForType(app.AppType).SupportsSalesStats;
}
public bool SupportsItemStats(AppData app)
{
return GetAppForType(app.AppType).SupportsItemStats;
}
}
public class ItemStats

View file

@ -1,3 +1,4 @@
#nullable enable
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
@ -6,18 +7,22 @@ using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Apps
{
public interface IApp
public abstract class AppBaseType
{
public string Description { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public abstract Task<object?> GetInfo(AppData appData);
public abstract Task<string> ConfigureLink(AppData app);
public abstract Task<string> ViewLink(AppData app);
public abstract Task SetDefaultSettings(AppData appData, string defaultCurrency);
}
public interface IHasSaleStatsAppType
{
public string Description { get; }
public string Type { get; }
public bool SupportsSalesStats { get; }
public bool SupportsItemStats { get; }
Task<string> ConfigureLink(AppData app);
Task<string> ViewLink(AppData app);
Task SetDefaultSettings(AppData appData, string defaultCurrency);
Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays);
}
public interface IHasItemStatsAppType
{
Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities);
Task<object> GetInfo(AppData appData);
}
public enum RequiresRefundEmail

View file

@ -8,7 +8,7 @@
@{ var store = Context.GetStoreData(); }
@if (store != null && Model.AppType == CrowdfundApp.AppType)
@if (store != null && Model.AppType == CrowdfundAppType.AppType)
{
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UICrowdfund" asp-action="UpdateCrowdfund" asp-route-appId="@Model.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, Model.Id)" id="@($"StoreNav-App-{Model.Id}")">

View file

@ -8,7 +8,7 @@
@{ var store = Context.GetStoreData(); }
@if (store != null && Model.AppType == PointOfSaleApp.AppType)
@if (store != null && Model.AppType == PointOfSaleAppType.AppType)
{
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIPointOfSale" asp-action="UpdatePointOfSale" asp-route-appId="@Model.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, Model.Id)" id="@($"StoreNav-App-{Model.Id}")">

View file

@ -97,7 +97,7 @@
@if (!string.IsNullOrEmpty(viewStyle))
{
<span>-</span>
@Safe.Raw(viewStyle)
<span>@viewStyle</span>
}
</td>
<td class="text-end">

View file

@ -119,14 +119,8 @@
<vc:store-recent-invoices vm="@(new StoreRecentInvoicesViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
@foreach (var app in Model.Apps)
{
@if (AppService.SupportsSalesStats(app))
{
<vc:app-sales app-id="@app.Id" app-type="@app.AppType" />
}
@if (AppService.SupportsItemStats(app))
{
<vc:app-top-items app-id="@app.Id" app-type="@app.AppType" />
}
<vc:app-sales app-id="@app.Id" app-type="@app.AppType" />
<vc:app-top-items app-id="@app.Id" app-type="@app.AppType" />
}
</div>
}