Plugins can now build apps (#4608)

* Plugins can now build apps

* fix tests

* fixup

* pluginize existing apps

* Test fixes part 1

* Test fixes part 2

* Fix Crowdfund namespace

* Syntax

* More namespace fixes

* Markup

* Test fix

* upstream fixes

* Add plugin icon

* Fix nullable build warnings

* allow pre popualting app creation

* Fixes after merge

* Make link methods async

* Use AppData as parameter for ConfigureLink

* GetApps by AppType

* Use ConfigureLink on dashboard

* Rename method

* Add properties to indicate stats support

* Property updates

* Test fixes

* Clean up imports

* Fixes after merge

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2023-03-17 03:56:32 +01:00
committed by GitHub
parent a671632fde
commit f74ea14d8b
42 changed files with 899 additions and 652 deletions

View File

@@ -7,6 +7,8 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
namespace BTCPayServer.Controllers.Greenfield
{
@@ -63,7 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = AppType.Crowdfund.ToString()
AppType = CrowdfundApp.AppType
};
appData.SetSettings(ToCrowdfundSettings(request));
@@ -94,7 +97,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = AppType.PointOfSale.ToString()
AppType = PointOfSaleApp.AppType
};
appData.SetSettings(ToPointOfSaleSettings(request));
@@ -108,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, AppType.PointOfSale);
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
if (app == null)
{
return AppNotFound();
@@ -181,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, AppType.PointOfSale);
var app = await _appService.GetApp(appId, PointOfSaleApp.AppType);
if (app == null)
{
return AppNotFound();
@@ -194,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, AppType.Crowdfund);
var app = await _appService.GetApp(appId, CrowdfundApp.AppType);
if (app == null)
{
return AppNotFound();
@@ -264,7 +267,7 @@ namespace BTCPayServer.Controllers.Greenfield
return new PointOfSaleSettings()
{
Title = request.Title,
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
DefaultView = (PosViewType) request.DefaultView,
ShowCustomAmount = request.ShowCustomAmount,
ShowDiscount = request.ShowDiscount,
EnableTips = request.EnableTips,

View File

@@ -1,4 +1,3 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
@@ -6,8 +5,6 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@@ -57,13 +54,14 @@ namespace BTCPayServer.Controllers
var app = await _appService.GetApp(appId, null);
if (app is null)
return NotFound();
return app.AppType switch
var res = await _appService.ViewLink(app);
if (res is null)
{
nameof(AppType.Crowdfund) => RedirectToAction(nameof(UICrowdfundController.ViewCrowdfund), "UICrowdfund", new { appId }),
nameof(AppType.PointOfSale) => RedirectToAction(nameof(UIPointOfSaleController.ViewPointOfSale), "UIPointOfSale", new { appId }),
_ => NotFound()
};
return NotFound();
}
return Redirect(res);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
@@ -114,12 +112,10 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/stores/{storeId}/apps/create")]
public IActionResult CreateApp(string storeId)
public IActionResult CreateApp(string storeId, string appType = null)
{
return View(new CreateAppViewModel
{
StoreId = GetCurrentStore().Id
});
var vm = new CreateAppViewModel (_appService){StoreId = GetCurrentStore().Id, SelectedAppType = appType};
return View(vm);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
@@ -128,8 +124,8 @@ namespace BTCPayServer.Controllers
{
var store = GetCurrentStore();
vm.StoreId = store.Id;
if (!Enum.TryParse(vm.SelectedAppType, out AppType appType))
var types = _appService.GetAvailableAppTypes();
if (!types.ContainsKey(vm.SelectedAppType))
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
if (!ModelState.IsValid)
@@ -141,34 +137,18 @@ namespace BTCPayServer.Controllers
{
StoreDataId = store.Id,
Name = vm.AppName,
AppType = appType.ToString()
AppType = vm.SelectedAppType
};
var defaultCurrency = await GetStoreDefaultCurrentIfEmpty(appData.StoreDataId, null);
switch (appType)
{
case AppType.Crowdfund:
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
appData.SetSettings(emptyCrowdfund);
break;
case AppType.PointOfSale:
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
appData.SetSettings(empty);
break;
default:
throw new ArgumentOutOfRangeException();
}
await _appService.SetDefaultSettings(appData, defaultCurrency);
await _appService.UpdateOrCreateApp(appData);
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
CreatedAppId = appData.Id;
return appType switch
{
AppType.PointOfSale => RedirectToAction(nameof(UIPointOfSaleController.UpdatePointOfSale), "UIPointOfSale", new { appId = appData.Id }),
AppType.Crowdfund => RedirectToAction(nameof(UICrowdfundController.UpdateCrowdfund), "UICrowdfund", new { appId = appData.Id }),
_ => throw new ArgumentOutOfRangeException()
};
var url = await _appService.ConfigureLink(appData, vm.SelectedAppType);
return Redirect(url);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]

View File

@@ -213,7 +213,8 @@ namespace BTCPayServer.Controllers
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
}
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
[NonAction]
public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
{
var storeBlob = store.GetStoreBlob();
var entity = _InvoiceRepository.CreateNewInvoice();

View File

@@ -18,6 +18,8 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
@@ -47,7 +49,6 @@ namespace BTCPayServer
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
private readonly LinkGenerator _linkGenerator;
private readonly LightningAddressService _lightningAddressService;
@@ -155,6 +156,7 @@ namespace BTCPayServer
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" });
switch (claimResponse.PayoutData.State)
{
case PayoutState.AwaitingPayment:
@@ -249,37 +251,49 @@ namespace BTCPayServer
return NotFound();
}
ViewPointOfSaleViewModel.Item[] items = null;
string currencyCode = null;
ViewPointOfSaleViewModel.Item[] items;
string currencyCode;
PointOfSaleSettings posS = null;
switch (app.AppType)
{
case nameof(AppType.Crowdfund):
case CrowdfundApp.AppType:
var cfS = app.GetSettings<CrowdfundSettings>();
currencyCode = cfS.TargetCurrency;
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
break;
case nameof(AppType.PointOfSale):
var posS = app.GetSettings<PointOfSaleSettings>();
case PointOfSaleApp.AppType:
posS = app.GetSettings<PointOfSaleSettings>();
currencyCode = posS.Currency;
items = _appService.Parse(posS.Template, posS.Currency);
break;
default:
//TODO: Allow other apps to define lnurl support
return NotFound();
}
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
var item = items.FirstOrDefault(item1 =>
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
ViewPointOfSaleViewModel.Item item = null;
if (!string.IsNullOrEmpty(itemCode))
{
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
item = items.FirstOrDefault(item1 =>
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
if (item is null ||
item.Inventory <= 0 ||
(item.PaymentMethods?.Any() is true &&
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
if (item is null ||
item.Inventory <= 0 ||
(item.PaymentMethods?.Any() is true &&
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
{
return NotFound();
}
}
else if (app.AppType == PointOfSaleApp.AppType && posS?.ShowCustomAmount is not true)
{
return NotFound();
}
return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null,
() => (null, app, item, new List<string> { AppService.GetAppInternalTag(appId) }, item.Price.Value, true));
() => (null, app, item, new List<string> { AppService.GetAppInternalTag(appId) }, item?.Price.Value, true));
}
public class EditLightningAddressVM
@@ -311,11 +325,8 @@ namespace BTCPayServer
public decimal? Max { get; set; }
}
public ConcurrentDictionary<string, LightningAddressItem> Items { get; set; } =
new ConcurrentDictionary<string, LightningAddressItem>();
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; set; } =
new ConcurrentDictionary<string, string[]>();
public ConcurrentDictionary<string, LightningAddressItem> Items { get; } = new ();
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; } = new ();
public override string ToString()
{
@@ -389,7 +400,7 @@ namespace BTCPayServer
var redirectUrl = app?.AppType switch
{
nameof(AppType.PointOfSale) => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
PointOfSaleApp.AppType => app.GetSettings<PointOfSaleSettings>().RedirectUrl ??
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
_ => null
};

View File

@@ -5,7 +5,6 @@ using System.Web;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Plugins.PayButton.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;

View File

@@ -347,7 +347,7 @@ namespace BTCPayServer.Controllers
if (appIdsToFetch.Any())
{
var apps = (await _AppService.GetApps(appIdsToFetch.ToArray()))
.ToDictionary(data => data.Id, data => Enum.Parse<AppType>(data.AppType));
.ToDictionary(data => data.Id, data => data.AppType);
;
if (!string.IsNullOrEmpty(settings.RootAppId))
{
@@ -422,8 +422,10 @@ namespace BTCPayServer.Controllers
private async Task<List<SelectListItem>> GetAppSelectList()
{
var types = _AppService.GetAvailableAppTypes();
var apps = (await _AppService.GetAllApps(null, true))
.Select(a => new SelectListItem($"{typeof(AppType).DisplayName(a.AppType)} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
.Select(a =>
new SelectListItem($"{types[a.AppType]} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
apps.Insert(0, new SelectListItem("(None)", null));
return apps;
}