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

View file

@ -31,7 +31,7 @@ namespace BTCPayServer.Tests
var apps = user.GetController<UIAppsController>(); var apps = user.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>(); var pos = user.GetController<UIPointOfSaleController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model); 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.AppName = "test";
vm.SelectedAppType = appType; vm.SelectedAppType = appType;
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result); 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 apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>(); var apps2 = user2.GetController<UIAppsController>();
var pos = user.GetController<UIPointOfSaleController>(); 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); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
Assert.Equal(appType, vm.SelectedAppType); Assert.Equal(appType, vm.SelectedAppType);
Assert.Null(vm.AppName); Assert.Null(vm.AppName);

View file

@ -7,6 +7,8 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Components.AppSales; namespace BTCPayServer.Components.AppSales;
@ -27,6 +29,9 @@ public class AppSales : ViewComponent
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType) 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 var vm = new AppSalesViewModel
{ {
Id = appId, Id = appId,
@ -42,7 +47,8 @@ public class AppSales : ViewComponent
vm.SalesCount = stats.SalesCount; vm.SalesCount = stats.SalesCount;
vm.Series = stats.Series; vm.Series = stats.Series;
vm.AppType = app.AppType; 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); return View(vm);
} }

View file

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

View file

@ -3,7 +3,7 @@
@using BTCPayServer.Plugins.Crowdfund @using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppSales.AppSalesViewModel @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"> <div id="AppSales-@Model.Id" class="widget app-sales">

View file

@ -7,6 +7,8 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Components.AppTopItems; namespace BTCPayServer.Components.AppTopItems;
@ -19,8 +21,12 @@ public class AppTopItems : ViewComponent
_appService = appService; _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 var vm = new AppTopItemsViewModel
{ {
Id = appId, Id = appId,
@ -36,7 +42,8 @@ public class AppTopItems : ViewComponent
vm.SalesCount = entries.Select(e => e.SalesCount).ToList(); vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
vm.Entries = entries.ToList(); vm.Entries = entries.ToList();
vm.AppType = app.AppType; 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); return View(vm);
} }

View file

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

View file

@ -1,12 +1,12 @@
@using BTCPayServer.Plugins.Crowdfund @using BTCPayServer.Plugins.Crowdfund
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel @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"> <div id="AppTopItems-@Model.Id" class="widget app-top-items">
<header class="mb-3"> <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)) @if (!string.IsNullOrEmpty(Model.AppUrl))
{ {
<a href="@Model.AppUrl">View All</a> <a href="@Model.AppUrl">View All</a>

View file

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

View file

@ -21,7 +21,7 @@ namespace BTCPayServer.Controllers
app.StoreData = GetCurrentStore(); 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)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
@ -33,7 +33,7 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
app.StoreData = GetCurrentStore(); 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)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@ -28,7 +29,7 @@ namespace BTCPayServer.Plugins.PointOfSale
public override void Execute(IServiceCollection services) public override void Execute(IServiceCollection services)
{ {
services.AddSingleton<IUIExtension>(new UIExtension("PointOfSale/NavExtension", "apps-nav")); services.AddSingleton<IUIExtension>(new UIExtension("PointOfSale/NavExtension", "apps-nav"));
services.AddSingleton<IApp,PointOfSaleApp>(); services.AddSingleton<AppBaseType, PointOfSaleAppType>();
base.Execute(services); base.Execute(services);
} }
} }
@ -45,34 +46,37 @@ namespace BTCPayServer.Plugins.PointOfSale
Print Print
} }
public class PointOfSaleApp: IApp public class PointOfSaleAppType: AppBaseType, IHasSaleStatsAppType, IHasItemStatsAppType
{ {
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions; private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
private readonly DisplayFormatter _displayFormatter; private readonly DisplayFormatter _displayFormatter;
private readonly HtmlSanitizer _htmlSanitizer; private readonly HtmlSanitizer _htmlSanitizer;
public const string AppType = "PointOfSale"; 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, LinkGenerator linkGenerator,
IOptions<BTCPayServerOptions> btcPayServerOptions, IOptions<BTCPayServerOptions> btcPayServerOptions,
DisplayFormatter displayFormatter, DisplayFormatter displayFormatter,
HtmlSanitizer htmlSanitizer) HtmlSanitizer htmlSanitizer)
{ {
Type = AppType;
Description = "Point of Sale";
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_btcPayServerOptions = btcPayServerOptions; _btcPayServerOptions = btcPayServerOptions;
_displayFormatter = displayFormatter; _displayFormatter = displayFormatter;
_htmlSanitizer = htmlSanitizer; _htmlSanitizer = htmlSanitizer;
} }
public Task<string> ConfigureLink(AppData app) public override Task<string> ConfigureLink(AppData app)
{ {
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIPointOfSaleController.UpdatePointOfSale), 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) public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
@ -114,22 +118,17 @@ namespace BTCPayServer.Plugins.PointOfSale
return Task.FromResult<IEnumerable<ItemStats>>(itemCount); return Task.FromResult<IEnumerable<ItemStats>>(itemCount);
} }
public Task<object> GetInfo(AppData appData) public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
{
throw new NotImplementedException();
}
public Task SetDefaultSettings(AppData appData, string defaultCurrency)
{ {
var empty = new PointOfSaleSettings { Currency = defaultCurrency }; var empty = new PointOfSaleSettings { Currency = defaultCurrency };
appData.SetSettings(empty); appData.SetSettings(empty);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<string> ViewLink(AppData app) public override Task<string> ViewLink(AppData app)
{ {
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIPointOfSaleController.ViewPointOfSale), 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 public class AppService
{ {
private readonly IEnumerable<IApp> _apps; private readonly Dictionary<string, AppBaseType> _appTypes;
readonly ApplicationDbContextFactory _ContextFactory; readonly ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository; private readonly InvoiceRepository _InvoiceRepository;
readonly CurrencyNameTable _Currencies; readonly CurrencyNameTable _Currencies;
@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Apps
private readonly HtmlSanitizer _HtmlSanitizer; private readonly HtmlSanitizer _HtmlSanitizer;
public CurrencyNameTable Currencies => _Currencies; public CurrencyNameTable Currencies => _Currencies;
public AppService( public AppService(
IEnumerable<IApp> apps, IEnumerable<AppBaseType> apps,
ApplicationDbContextFactory contextFactory, ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
CurrencyNameTable currencies, CurrencyNameTable currencies,
@ -48,7 +48,7 @@ namespace BTCPayServer.Services.Apps
StoreRepository storeRepository, StoreRepository storeRepository,
HtmlSanitizer htmlSanitizer) HtmlSanitizer htmlSanitizer)
{ {
_apps = apps; _appTypes = apps.ToDictionary(a => a.Type, a=> a);
_ContextFactory = contextFactory; _ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository; _InvoiceRepository = invoiceRepository;
_Currencies = currencies; _Currencies = currencies;
@ -56,40 +56,33 @@ namespace BTCPayServer.Services.Apps
_HtmlSanitizer = htmlSanitizer; _HtmlSanitizer = htmlSanitizer;
_displayFormatter = displayFormatter; _displayFormatter = displayFormatter;
} }
#nullable enable
public Dictionary<string, string> GetAvailableAppTypes() public Dictionary<string, string> GetAvailableAppTypes()
{ {
return _apps.ToDictionary(app => app.Type, app => app.Description); return _appTypes.ToDictionary(app => app.Key, app => app.Value.Description);
} }
public Task<string> ConfigureLink(AppData app, string vmSelectedAppType) public AppBaseType? GetAppType(string appType)
{ {
return GetAppForType(vmSelectedAppType).ConfigureLink(app); _appTypes.TryGetValue(appType, out var a);
return a;
} }
private IApp GetAppForType(string appType) public async Task<object?> GetInfo(string appId)
{
return _apps.First(app => app.Type == appType);
}
public async Task<object> GetInfo(string appId)
{ {
var appData = await GetApp(appId, null); var appData = await GetApp(appId, null);
if (appData is null) if (appData is null)
{
return null; return null;
} var appType = GetAppType(appData.AppType);
var app = GetAppForType(appData.AppType); if (appType is null)
if (app is null)
{
return null; return null;
} return appType.GetInfo(appData);
return app.GetInfo(appData);
} }
public async Task<IEnumerable<ItemStats>> GetItemStats(AppData 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, var paidInvoices = await GetInvoicesForApp(_InvoiceRepository,appData,
null, new [] null, new []
{ {
@ -97,7 +90,7 @@ namespace BTCPayServer.Services.Apps
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed), InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
InvoiceState.ToString(InvoiceStatusLegacy.Complete) 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, 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) 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), var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays),
new [] new []
{ {
@ -143,12 +138,13 @@ namespace BTCPayServer.Services.Apps
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed), InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
InvoiceState.ToString(InvoiceStatusLegacy.Complete) InvoiceState.ToString(InvoiceStatusLegacy.Complete)
}); });
return await GetAppForType(app.AppType).GetSalesStats(app, paidInvoices, numberOfDays);
return await salesType.GetSalesStats(app, paidInvoices, numberOfDays);
} }
public class InvoiceStatsItem public class InvoiceStatsItem
{ {
public string ItemCode { get; set; } public string ItemCode { get; set; } = string.Empty;
public decimal FiatPrice { get; set; } public decimal FiatPrice { get; set; }
public DateTime Date { get; set; } public DateTime Date { get; set; }
} }
@ -204,8 +200,8 @@ namespace BTCPayServer.Services.Apps
public static string GetAppOrderId(string appType, string appId) => public static string GetAppOrderId(string appType, string appId) =>
appType switch appType switch
{ {
CrowdfundApp.AppType => $"crowdfund-app_{appId}", CrowdfundAppType.AppType => $"crowdfund-app_{appId}",
PointOfSaleApp.AppType => $"pos-app_{appId}", PointOfSaleAppType.AppType => $"pos-app_{appId}",
_ => $"{appType}_{appId}" _ => $"{appType}_{appId}"
}; };
@ -215,7 +211,7 @@ namespace BTCPayServer.Services.Apps
return invoice.GetInternalTags("APP#"); 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 var invoices = await invoiceRepository.GetInvoices(new InvoiceQuery
{ {
@ -252,7 +248,7 @@ 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<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null)
{ {
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
var listApps = await ctx.UserStore var listApps = await ctx.UserStore
@ -294,7 +290,7 @@ namespace BTCPayServer.Services.Apps
string style; string style;
switch (appType) switch (appType)
{ {
case PointOfSaleApp.AppType: case PointOfSaleAppType.AppType:
var settings = app.GetSettings<PointOfSaleSettings>(); var settings = app.GetSettings<PointOfSaleSettings>();
string posViewStyle = (settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView).ToString(); string posViewStyle = (settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView).ToString();
style = typeof(PosViewType).DisplayName(posViewStyle); style = typeof(PosViewType).DisplayName(posViewStyle);
@ -328,7 +324,7 @@ 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)
{ {
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
var query = ctx.Apps var query = ctx.Apps
@ -342,7 +338,7 @@ namespace BTCPayServer.Services.Apps
return await query.FirstOrDefaultAsync(); return await query.FirstOrDefaultAsync();
} }
public Task<StoreData> GetStore(AppData app) public Task<StoreData?> GetStore(AppData app)
{ {
return _storeRepository.FindStore(app.StoreDataId); return _storeRepository.FindStore(app.StoreDataId);
} }
@ -354,7 +350,7 @@ namespace BTCPayServer.Services.Apps
{ {
var itemNode = new YamlMappingNode(); var itemNode = new YamlMappingNode();
itemNode.Add("title", new YamlScalarNode(item.Title)); 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())); itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant()));
if (!string.IsNullOrEmpty(item.Description)) if (!string.IsNullOrEmpty(item.Description))
{ {
@ -436,8 +432,11 @@ namespace BTCPayServer.Services.Apps
case "false": case "false":
case null: case null:
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed; price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed;
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture); if (pValue?.Value.Value is not null)
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol); {
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
}
break; break;
} }
@ -464,13 +463,13 @@ namespace BTCPayServer.Services.Apps
{ {
return Parse(htmlSanitizer, displayFormatter, template, currency).Where(c => !c.Disabled).ToArray(); return Parse(htmlSanitizer, displayFormatter, template, currency).Where(c => !c.Disabled).ToArray();
} }
#nullable restore
private class PosHolder private class PosHolder
{ {
private readonly HtmlSanitizer _htmlSanitizer; private readonly HtmlSanitizer _htmlSanitizer;
public PosHolder(HtmlSanitizer htmlSanitizer) public PosHolder(
HtmlSanitizer htmlSanitizer)
{ {
_htmlSanitizer = htmlSanitizer; _htmlSanitizer = htmlSanitizer;
} }
@ -506,8 +505,8 @@ namespace BTCPayServer.Services.Apps
public string Key { get; set; } public string Key { get; set; }
public YamlScalarNode Value { get; set; } public YamlScalarNode Value { get; set; }
} }
#nullable enable
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, string type = null) public async Task<AppData?> GetAppDataIfOwner(string userId, string appId, string? type = null)
{ {
if (userId == null || appId == null) if (userId == null || appId == null)
return null; return null;
@ -542,7 +541,7 @@ namespace BTCPayServer.Services.Apps
await ctx.SaveChangesAsync(); 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; result = null;
try try
@ -584,7 +583,7 @@ namespace BTCPayServer.Services.Apps
public async Task SetDefaultSettings(AppData appData, string defaultCurrency) public async Task SetDefaultSettings(AppData appData, string defaultCurrency)
{ {
var app = GetAppForType(appData.AppType); var app = GetAppType(appData.AppType);
if (app is null) if (app is null)
{ {
appData.SetSettings(null); appData.SetSettings(null);
@ -597,19 +596,10 @@ namespace BTCPayServer.Services.Apps
public async Task<string?> ViewLink(AppData app) public async Task<string?> ViewLink(AppData app)
{ {
var appType = GetAppForType(app.AppType); var appType = GetAppType(app.AppType);
return await appType?.ViewLink(app)!; return await appType?.ViewLink(app)!;
} }
#nullable restore #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 public class ItemStats

View file

@ -1,3 +1,4 @@
#nullable enable
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -6,18 +7,22 @@ using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Apps 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); Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays);
}
public interface IHasItemStatsAppType
{
Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities); Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] invoiceEntities);
Task<object> GetInfo(AppData appData);
} }
public enum RequiresRefundEmail public enum RequiresRefundEmail

View file

@ -8,7 +8,7 @@
@{ var store = Context.GetStoreData(); } @{ 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"> <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}")"> <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(); } @{ 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"> <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}")"> <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)) @if (!string.IsNullOrEmpty(viewStyle))
{ {
<span>-</span> <span>-</span>
@Safe.Raw(viewStyle) <span>@viewStyle</span>
} }
</td> </td>
<td class="text-end"> <td class="text-end">

View file

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