diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 83fb1b320..92762f98c 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers if (choice == null) return NotFound(); title = choice.Title; - if (choice.Custom == "topup") + if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup) { price = null; } @@ -323,7 +323,7 @@ namespace BTCPayServer.Controllers return NotFound("Incorrect option provided"); title = choice.Title; - if (choice.Custom == "topup") + if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup) { price = null; } diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index e092b2e7d..650d20e58 100644 --- a/BTCPayServer/Hosting/MigrationStartupTask.cs +++ b/BTCPayServer/Hosting/MigrationStartupTask.cs @@ -6,14 +6,18 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client.Models; using BTCPayServer.Configuration; +using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Fido2; using BTCPayServer.Fido2.Models; using BTCPayServer.Logging; +using BTCPayServer.Models.AppViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services; +using BTCPayServer.Services.Apps; using BTCPayServer.Services.Stores; +using ExchangeSharp; using Fido2NetLib.Objects; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -31,6 +35,7 @@ namespace BTCPayServer.Hosting private readonly StoreRepository _StoreRepository; private readonly BTCPayNetworkProvider _NetworkProvider; private readonly SettingsRepository _Settings; + private readonly AppService _appService; private readonly UserManager _userManager; public IOptions LightningOptions { get; } @@ -41,12 +46,14 @@ namespace BTCPayServer.Hosting ApplicationDbContextFactory dbContextFactory, UserManager userManager, IOptions lightningOptions, - SettingsRepository settingsRepository) + SettingsRepository settingsRepository, + AppService appService) { _DBContextFactory = dbContextFactory; _StoreRepository = storeRepository; _NetworkProvider = networkProvider; _Settings = settingsRepository; + _appService = appService; _userManager = userManager; LightningOptions = lightningOptions; } @@ -134,6 +141,12 @@ namespace BTCPayServer.Hosting settings.MigrateHotwalletProperty = true; await _Settings.UpdateSetting(settings); } + if (!settings.MigrateAppCustomOption) + { + await MigrateAppCustomOption(); + settings.MigrateAppCustomOption = true; + await _Settings.UpdateSetting(settings); + } } catch (Exception ex) { @@ -142,6 +155,42 @@ namespace BTCPayServer.Hosting } } + private async Task MigrateAppCustomOption() + { + await using var ctx = _DBContextFactory.CreateContext(); + foreach (var app in await ctx.Apps.AsQueryable().ToArrayAsync()) + { + ViewPointOfSaleViewModel.Item[] items; + string newTemplate; + switch (app.AppType) + { + case nameof(AppType.Crowdfund): + var settings1 = app.GetSettings(); + items = _appService.Parse(settings1.PerksTemplate, settings1.TargetCurrency); + newTemplate = _appService.SerializeTemplate(items); + if (settings1.PerksTemplate != newTemplate) + { + settings1.PerksTemplate = newTemplate; + app.SetSettings(settings1); + }; + break; + + case nameof(AppType.PointOfSale): + + var settings2 = app.GetSettings(); + items = _appService.Parse(settings2.Template, settings2.Currency); + newTemplate = _appService.SerializeTemplate(items); + if (settings2.Template != newTemplate) + { + settings2.Template = newTemplate; + app.SetSettings(settings2); + }; + break; + } + } + await ctx.SaveChangesAsync(); + } + private async Task MigrateHotwalletProperty() { await using var ctx = _DBContextFactory.CreateContext(); diff --git a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs index 47718ff55..fe1121a3d 100644 --- a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs @@ -9,15 +9,22 @@ namespace BTCPayServer.Models.AppViewModels { public class ItemPrice { + public enum ItemPriceType + { + Topup, + Minimum, + Fixed + } + + public ItemPriceType Type { get; set; } public string Formatted { get; set; } - public decimal Value { get; set; } + public decimal? Value { get; set; } } public string Description { get; set; } public string Id { get; set; } public string Image { get; set; } public ItemPrice Price { get; set; } public string Title { get; set; } - public string Custom { get; set; } public string BuyButtonText { get; set; } public int? Inventory { get; set; } = null; public string[] PaymentMethods { get; set; } diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index aad704f5b..5f011c821 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -289,7 +289,7 @@ namespace BTCPayServer.Services.Apps { var itemNode = new YamlMappingNode(); itemNode.Add("title", new YamlScalarNode(item.Title)); - if(item.Custom!= "topup") + if(item.Price.Type!= ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup) itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant())); if (!string.IsNullOrEmpty(item.Description)) { @@ -302,7 +302,7 @@ namespace BTCPayServer.Services.Apps { itemNode.Add("image", new YamlScalarNode(item.Image)); } - itemNode.Add("custom", new YamlScalarNode(item.Custom.ToStringLowerInvariant())); + itemNode.Add("price_type", new YamlScalarNode(item.Price.Type.ToStringLowerInvariant())); itemNode.Add("disabled", new YamlScalarNode(item.Disabled.ToStringLowerInvariant())); if (item.Inventory.HasValue) { @@ -336,26 +336,50 @@ namespace BTCPayServer.Services.Apps .Children .Select(kv => new PosHolder(_HtmlSanitizer) { Key = _HtmlSanitizer.Sanitize((kv.Key as YamlScalarNode)?.Value), Value = kv.Value as YamlMappingNode }) .Where(kv => kv.Value != null) - .Select(c => new ViewPointOfSaleViewModel.Item() + .Select(c => { - Description = c.GetDetailString("description"), - Id = c.Key, - Image = c.GetDetailString("image"), - Title = c.GetDetailString("title") ?? c.Key, - Custom = c.GetDetailString("custom"), - Price = - c.GetDetailString("custom") == "topup" - ? null - : c.GetDetail("price") - .Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice() - { - Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture), - Formatted = Currencies.FormatCurrency(cc.Value.Value, currency) - }).Single(), - BuyButtonText = c.GetDetailString("buyButtonText"), - Inventory = string.IsNullOrEmpty(c.GetDetailString("inventory")) ? (int?)null : int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture), - PaymentMethods = c.GetDetailStringList("payment_methods"), - Disabled = c.GetDetailString("disabled") == "true" + ViewPointOfSaleViewModel.Item.ItemPrice price = new ViewPointOfSaleViewModel.Item.ItemPrice(); + var pValue = c.GetDetail("price")?.FirstOrDefault(); + + switch (c.GetDetailString("custom")??c.GetDetailString("price_type")?.ToLowerInvariant()) + { + case "topup": + case null when pValue is null: + price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup; + break; + case "true": + case "minimum": + price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum; + if (pValue != null) + { + price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture); + price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency); + } + break; + case "fixed": + case "false": + case null: + price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed; + price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture); + price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency); + break; + } + + return new ViewPointOfSaleViewModel.Item() + { + Description = c.GetDetailString("description"), + Id = c.Key, + Image = c.GetDetailString("image"), + Title = c.GetDetailString("title") ?? c.Key, + Price = price, + BuyButtonText = c.GetDetailString("buyButtonText"), + Inventory = + string.IsNullOrEmpty(c.GetDetailString("inventory")) + ? (int?)null + : int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture), + PaymentMethods = c.GetDetailStringList("payment_methods"), + Disabled = c.GetDetailString("disabled") == "true" + }; }) .ToArray(); } diff --git a/BTCPayServer/Services/MigrationSettings.cs b/BTCPayServer/Services/MigrationSettings.cs index a66bb9e48..56c9cae0e 100644 --- a/BTCPayServer/Services/MigrationSettings.cs +++ b/BTCPayServer/Services/MigrationSettings.cs @@ -25,5 +25,6 @@ namespace BTCPayServer.Services // Done in DbMigrationsHostedService public int? MigratedInvoiceTextSearchPages { get; set; } + public bool MigrateAppCustomOption { get; set; } } } diff --git a/BTCPayServer/Views/Apps/TemplateEditor.cshtml b/BTCPayServer/Views/Apps/TemplateEditor.cshtml index 24a0a26d9..e900ba39a 100644 --- a/BTCPayServer/Views/Apps/TemplateEditor.cshtml +++ b/BTCPayServer/Views/Apps/TemplateEditor.cshtml @@ -64,7 +64,8 @@
- + + 0) { itemTemplate += ' buyButtonText: ' + buyButtonText + '\n'; @@ -293,7 +294,7 @@ document.addEventListener("DOMContentLoaded", function () { editItem: function(index){ this.errors = []; if(index < 0){ - this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", custom: false, inventory: null, paymentMethods: [], disabled: false}; + this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", custom: "fixed", inventory: null, paymentMethods: [], disabled: false}; }else{ this.editingItem = {...this.items[index], index}; } diff --git a/BTCPayServer/Views/AppsPublic/Crowdfund/ContributeForm.cshtml b/BTCPayServer/Views/AppsPublic/Crowdfund/ContributeForm.cshtml index bc7f301f5..f9f1c7313 100644 --- a/BTCPayServer/Views/AppsPublic/Crowdfund/ContributeForm.cshtml +++ b/BTCPayServer/Views/AppsPublic/Crowdfund/ContributeForm.cshtml @@ -1,3 +1,4 @@ +@using BTCPayServer.Models.AppViewModels @model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund @{ var vm = Model.ViewCrowdfundViewModel; } @@ -26,20 +27,20 @@ @(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title) - @if (item.Price?.Value > 0) + @if (item.Price.Value > 0) { @item.Price.Value @vm.TargetCurrency - if (item.Custom == "true") + if (item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum) { @Safe.Raw("or more") } } - else if (item.Custom == "topup" || item.Custom == "true" ) + else if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ) { @Safe.Raw("Any amount") - }else if (item.Custom == "false") + }else if (item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed) { @Safe.Raw("Free") } @@ -54,7 +55,7 @@ { case null: break; - case int i when i <= 0: + case var i when i <= 0: Sold out break; default: diff --git a/BTCPayServer/Views/AppsPublic/PointOfSale/Cart.cshtml b/BTCPayServer/Views/AppsPublic/PointOfSale/Cart.cshtml index 9fdd30f42..46567e600 100644 --- a/BTCPayServer/Views/AppsPublic/PointOfSale/Cart.cshtml +++ b/BTCPayServer/Views/AppsPublic/PointOfSale/Cart.cshtml @@ -1,3 +1,4 @@ +@using BTCPayServer.Models.AppViewModels @model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel @{ Layout = "_LayoutPos"; @@ -232,8 +233,8 @@ @{ - var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? (item.Custom == "true" || item.Custom == "topup") ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText; - if (item.Custom != "topup") + var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText; + if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup) { buttonText = buttonText.Replace("{0}",item.Price.Formatted) ?.Replace("{Price}",item.Price.Formatted); diff --git a/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml b/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml index 15380e32b..958bd3ec3 100644 --- a/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml +++ b/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml @@ -1,3 +1,4 @@ +@using BTCPayServer.Models.AppViewModels @model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel @{ Layout = "_LayoutPos"; @@ -19,12 +20,9 @@ @for (int x = 0; x < Model.Items.Length; x++) { var item = Model.Items[x]; - var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? (item.Custom == "true" || item.Custom == "topup") ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText; - if (item.Custom != "topup") - { - buttonText = buttonText.Replace("{0}",item.Price.Formatted) - ?.Replace("{Price}",item.Price.Formatted); - } + var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText; + buttonText = buttonText.Replace("{0}",item.Price.Formatted) + ?.Replace("{Price}",item.Price.Formatted);
@if (!String.IsNullOrWhiteSpace(item.Image)) @@ -35,7 +33,7 @@