diff --git a/BTCPayServer.Client/BTCPayServerClient.Apps.cs b/BTCPayServer.Client/BTCPayServerClient.Apps.cs index ca7e4b7bc..438619c50 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Apps.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Apps.cs @@ -9,41 +9,41 @@ namespace BTCPayServer.Client; public partial class BTCPayServerClient { public virtual async Task CreatePointOfSaleApp(string storeId, - CreatePointOfSaleAppRequest request, CancellationToken token = default) + PointOfSaleAppRequest request, CancellationToken token = default) { if (request == null) throw new ArgumentNullException(nameof(request)); return await SendHttpRequest($"api/v1/stores/{storeId}/apps/pos", request, HttpMethod.Post, token); } public virtual async Task CreateCrowdfundApp(string storeId, - CreateCrowdfundAppRequest request, CancellationToken token = default) + CrowdfundAppRequest request, CancellationToken token = default) { if (request == null) throw new ArgumentNullException(nameof(request)); return await SendHttpRequest($"api/v1/stores/{storeId}/apps/crowdfund", request, HttpMethod.Post, token); } public virtual async Task UpdatePointOfSaleApp(string appId, - CreatePointOfSaleAppRequest request, CancellationToken token = default) + PointOfSaleAppRequest request, CancellationToken token = default) { if (request == null) throw new ArgumentNullException(nameof(request)); return await SendHttpRequest($"api/v1/apps/pos/{appId}", request, HttpMethod.Put, token); } - public virtual async Task GetApp(string appId, CancellationToken token = default) + public virtual async Task GetApp(string appId, CancellationToken token = default) { if (appId == null) throw new ArgumentNullException(nameof(appId)); - return await SendHttpRequest($"api/v1/apps/{appId}", null, HttpMethod.Get, token); + return await SendHttpRequest($"api/v1/apps/{appId}", null, HttpMethod.Get, token); } - public virtual async Task GetAllApps(string storeId, CancellationToken token = default) + public virtual async Task GetAllApps(string storeId, CancellationToken token = default) { if (storeId == null) throw new ArgumentNullException(nameof(storeId)); - return await SendHttpRequest($"api/v1/stores/{storeId}/apps", null, HttpMethod.Get, token); + return await SendHttpRequest($"api/v1/stores/{storeId}/apps", null, HttpMethod.Get, token); } - public virtual async Task GetAllApps(CancellationToken token = default) + public virtual async Task GetAllApps(CancellationToken token = default) { - return await SendHttpRequest("api/v1/apps", null, HttpMethod.Get, token); + return await SendHttpRequest("api/v1/apps", null, HttpMethod.Get, token); } public virtual async Task GetPosApp(string appId, CancellationToken token = default) diff --git a/BTCPayServer.Client/Models/AppBaseData.cs b/BTCPayServer.Client/Models/AppBaseData.cs new file mode 100644 index 000000000..9bea31218 --- /dev/null +++ b/BTCPayServer.Client/Models/AppBaseData.cs @@ -0,0 +1,21 @@ +using System; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models; + +public class AppBaseData +{ + public string Id { get; set; } + public string AppType { get; set; } + public string AppName { get; set; } + public string StoreId { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public bool? Archived { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset Created { get; set; } +} + +public interface IAppRequest +{ + public string AppName { get; set; } +} diff --git a/BTCPayServer.Client/Models/CreateAppRequest.cs b/BTCPayServer.Client/Models/CreateAppRequest.cs deleted file mode 100644 index 11773a16c..000000000 --- a/BTCPayServer.Client/Models/CreateAppRequest.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace BTCPayServer.Client.Models -{ - public enum PosViewType - { - Static, - Cart, - Light, - Print - } - - public class CreateAppRequest - { - public string AppName { get; set; } - public string AppType { get; set; } - } - - public class CreatePointOfSaleAppRequest : CreateAppRequest - { - public string Currency { get; set; } = null; - public string Title { get; set; } = null; - public string Description { get; set; } = null; - public string Template { get; set; } = null; - [JsonConverter(typeof(StringEnumConverter))] - public PosViewType DefaultView { get; set; } - public bool ShowItems { get; set; } = false; - public bool ShowCustomAmount { get; set; } = false; - public bool ShowDiscount { get; set; } = false; - public bool ShowSearch { get; set; } = true; - public bool ShowCategories { get; set; } = true; - public bool EnableTips { get; set; } = false; - public string CustomAmountPayButtonText { get; set; } = null; - public string FixedAmountPayButtonText { get; set; } = null; - public string TipText { get; set; } = null; - public string NotificationUrl { get; set; } = null; - public string RedirectUrl { get; set; } = null; - public bool? RedirectAutomatically { get; set; } = null; - public bool? Archived { get; set; } = null; - public string FormId { get; set; } = null; - } - - public enum CrowdfundResetEvery - { - Never, - Hour, - Day, - Month, - Year - } - - public class CreateCrowdfundAppRequest : CreateAppRequest - { - public string Title { get; set; } = null; - public bool? Enabled { get; set; } = null; - public bool? EnforceTargetAmount { get; set; } = null; - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] - public DateTimeOffset? StartDate { get; set; } = null; - public string TargetCurrency { get; set; } = null; - public string Description { get; set; } = null; - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] - public DateTimeOffset? EndDate { get; set; } = null; - public decimal? TargetAmount { get; set; } = null; - public string MainImageUrl { get; set; } = null; - public string NotificationUrl { get; set; } = null; - public string Tagline { get; set; } = null; - public string PerksTemplate { get; set; } = null; - public bool? SoundsEnabled { get; set; } = null; - public string DisqusShortname { get; set; } = null; - public bool? AnimationsEnabled { get; set; } = null; - public int? ResetEveryAmount { get; set; } = null; - [JsonConverter(typeof(StringEnumConverter))] - public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never; - public bool? DisplayPerksValue { get; set; } = null; - public bool? DisplayPerksRanking { get; set; } = null; - public bool? SortPerksByPopularity { get; set; } = null; - public bool? Archived { get; set; } = null; - public string[] Sounds { get; set; } = null; - public string[] AnimationColors { get; set; } = null; - } -} diff --git a/BTCPayServer.Client/Models/CrowdfundAppData.cs b/BTCPayServer.Client/Models/CrowdfundAppData.cs new file mode 100644 index 000000000..792453482 --- /dev/null +++ b/BTCPayServer.Client/Models/CrowdfundAppData.cs @@ -0,0 +1,55 @@ +#nullable enable +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client.Models; + +public abstract class CrowdfundBaseData : AppBaseData +{ + public string? Title { get; set; } + public bool? Enabled { get; set; } + public bool? EnforceTargetAmount { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset? StartDate { get; set; } + public string? TargetCurrency { get; set; } + public string? Description { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset? EndDate { get; set; } + public decimal? TargetAmount { get; set; } + public string? MainImageUrl { get; set; } + public string? NotificationUrl { get; set; } + public string? Tagline { get; set; } + public bool? DisqusEnabled { get; set; } + public string? DisqusShortname { get; set; } + public bool? SoundsEnabled { get; set; } + public bool? AnimationsEnabled { get; set; } + public int? ResetEveryAmount { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public CrowdfundResetEvery? ResetEvery { get; set; } + public bool? DisplayPerksValue { get; set; } + public bool? DisplayPerksRanking { get; set; } + public bool? SortPerksByPopularity { get; set; } + public string[]? Sounds { get; set; } + public string[]? AnimationColors { get; set; } + public string? FormId { get; set; } +} + +public class CrowdfundAppData : CrowdfundBaseData +{ + public object? Perks { get; set; } +} + +public class CrowdfundAppRequest : CrowdfundBaseData, IAppRequest +{ + public string? PerksTemplate { get; set; } +} + +public enum CrowdfundResetEvery +{ + Never, + Hour, + Day, + Month, + Year +} diff --git a/BTCPayServer.Client/Models/PointOfSaleAppData.cs b/BTCPayServer.Client/Models/PointOfSaleAppData.cs index ebc7d79c7..b2a4ad878 100644 --- a/BTCPayServer.Client/Models/PointOfSaleAppData.cs +++ b/BTCPayServer.Client/Models/PointOfSaleAppData.cs @@ -1,67 +1,46 @@ -using System; +#nullable enable using Newtonsoft.Json; +using Newtonsoft.Json.Converters; -namespace BTCPayServer.Client.Models +namespace BTCPayServer.Client.Models; + +public abstract class PointOfSaleBaseData : AppBaseData { - public class AppDataBase - { - public string Id { get; set; } - public string AppType { get; set; } - public string Name { get; set; } - public string StoreId { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public bool? Archived { get; set; } - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] - public DateTimeOffset Created { get; set; } - } - - public class PointOfSaleAppData : AppDataBase - { - public string Title { get; set; } - public string DefaultView { get; set; } - public bool ShowItems { get; set; } - public bool ShowCustomAmount { get; set; } - public bool ShowDiscount { get; set; } - public bool ShowSearch { get; set; } - public bool ShowCategories { get; set; } - public bool EnableTips { get; set; } - public string Currency { get; set; } - public object Items { get; set; } - public string FixedAmountPayButtonText { get; set; } - public string CustomAmountPayButtonText { get; set; } - public string TipText { get; set; } - public string NotificationUrl { get; set; } - public string RedirectUrl { get; set; } - public string Description { get; set; } - public bool? RedirectAutomatically { get; set; } - } - - public class CrowdfundAppData : AppDataBase - { - public string Title { get; set; } - public bool Enabled { get; set; } - public bool EnforceTargetAmount { get; set; } - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] - public DateTimeOffset? StartDate { get; set; } - public string TargetCurrency { get; set; } - public string Description { get; set; } - [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] - public DateTimeOffset? EndDate { get; set; } - public decimal? TargetAmount { get; set; } - public string MainImageUrl { get; set; } - public string NotificationUrl { get; set; } - public string Tagline { get; set; } - public object Perks { get; set; } - public bool DisqusEnabled { get; set; } - public string DisqusShortname { get; set; } - public bool SoundsEnabled { get; set; } - public bool AnimationsEnabled { get; set; } - public int ResetEveryAmount { get; set; } - public string ResetEvery { get; set; } - public bool DisplayPerksValue { get; set; } - public bool DisplayPerksRanking { get; set; } - public bool SortPerksByPopularity { get; set; } - public string[] Sounds { get; set; } - public string[] AnimationColors { get; set; } - } + public string? Title { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public PosViewType? DefaultView { get; set; } + public bool? ShowItems { get; set; } + public bool? ShowCustomAmount { get; set; } + public bool? ShowDiscount { get; set; } + public bool? ShowSearch { get; set; } + public bool? ShowCategories { get; set; } + public bool? EnableTips { get; set; } + public string? Currency { get; set; } + public string? FixedAmountPayButtonText { get; set; } + public string? CustomAmountPayButtonText { get; set; } + public string? TipText { get; set; } + public string? NotificationUrl { get; set; } + public string? RedirectUrl { get; set; } + public string? Description { get; set; } + public bool? RedirectAutomatically { get; set; } + public int[]? CustomTipPercentages { get; set; } + public string? FormId { get; set; } +} + +public class PointOfSaleAppData : PointOfSaleBaseData +{ + public object? Items { get; set; } +} + +public class PointOfSaleAppRequest : PointOfSaleBaseData, IAppRequest +{ + public string? Template { get; set; } +} + +public enum PosViewType +{ + Static, + Cart, + Light, + Print } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index ed5540005..124acf21a 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -253,11 +253,11 @@ namespace BTCPayServer.Tests // Test validation for creating the app await AssertValidationError(new[] { "AppName" }, - async () => await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() { })); + async () => await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest())); await AssertValidationError(new[] { "AppName" }, async () => await client.CreatePointOfSaleApp( user.StoreId, - new CreatePointOfSaleAppRequest() + new PointOfSaleAppRequest { AppName = "this is a really long app name this is a really long app name this is a really long app name", } @@ -266,7 +266,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "Currency" }, async () => await client.CreatePointOfSaleApp( user.StoreId, - new CreatePointOfSaleAppRequest() + new PointOfSaleAppRequest { AppName = "good name", Currency = "fake currency" @@ -276,7 +276,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "Template" }, async () => await client.CreatePointOfSaleApp( user.StoreId, - new CreatePointOfSaleAppRequest() + new PointOfSaleAppRequest { AppName = "good name", Template = "lol invalid template" @@ -286,7 +286,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "AppName", "Currency", "Template" }, async () => await client.CreatePointOfSaleApp( user.StoreId, - new CreatePointOfSaleAppRequest() + new PointOfSaleAppRequest { Currency = "fake currency", Template = "lol invalid template" @@ -297,14 +297,14 @@ namespace BTCPayServer.Tests // Test creating a POS app successfully var app = await client.CreatePointOfSaleApp( user.StoreId, - new CreatePointOfSaleAppRequest() + new PointOfSaleAppRequest { AppName = "test app from API", Currency = "JPY", Title = "test app title" } ); - Assert.Equal("test app from API", app.Name); + Assert.Equal("test app from API", app.AppName); Assert.Equal(user.StoreId, app.StoreId); Assert.Equal("PointOfSale", app.AppType); Assert.Equal("test app title", app.Title); @@ -313,12 +313,19 @@ namespace BTCPayServer.Tests // Test title falls back to name app = await client.CreatePointOfSaleApp( user.StoreId, - new CreatePointOfSaleAppRequest + new PointOfSaleAppRequest { - AppName = "test app name" + AppName = "test app name", + Description = "test description", + ShowItems = true, + ShowCategories = false } ); Assert.Equal("test app name", app.Title); + Assert.Equal("test description", app.Description); + Assert.True(app.ShowItems); + Assert.False(app.ShowCategories); + Assert.False(app.ShowDiscount); // Make sure we return a 404 if we try to get an app that doesn't exist await AssertHttpError(404, async () => @@ -332,30 +339,33 @@ namespace BTCPayServer.Tests // Test that we can retrieve the app data var retrievedApp = await client.GetApp(app.Id); - Assert.Equal(app.Name, retrievedApp.Name); + Assert.Equal(app.AppName, retrievedApp.AppName); Assert.Equal(app.StoreId, retrievedApp.StoreId); Assert.Equal(app.AppType, retrievedApp.AppType); // Test that we can update the app data - await client.UpdatePointOfSaleApp( + var retrievedPosApp = await client.UpdatePointOfSaleApp( app.Id, - new CreatePointOfSaleAppRequest() + new PointOfSaleAppRequest { AppName = "new app name", Title = "new app title", Archived = true } ); + Assert.Equal("new app name", retrievedPosApp.AppName); + Assert.Equal("new app title", retrievedPosApp.Title); + Assert.True(retrievedPosApp.Archived); + // Test generic GET app endpoint first retrievedApp = await client.GetApp(app.Id); - Assert.Equal("new app name", retrievedApp.Name); + Assert.Equal("new app name", retrievedApp.AppName); Assert.True(retrievedApp.Archived); // Test the POS-specific endpoint also - var retrievedPosApp = await client.GetPosApp(app.Id); - Assert.Equal("new app name", retrievedPosApp.Name); + retrievedPosApp = await client.GetPosApp(app.Id); + Assert.Equal("new app name", retrievedPosApp.AppName); Assert.Equal("new app title", retrievedPosApp.Title); - Assert.True(retrievedPosApp.Archived); // Make sure we return a 404 if we try to delete an app that doesn't exist await AssertHttpError(404, async () => @@ -383,11 +393,11 @@ namespace BTCPayServer.Tests // Test validation for creating the app await AssertValidationError(new[] { "AppName" }, - async () => await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() { })); + async () => await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest())); await AssertValidationError(new[] { "AppName" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "this is a really long app name this is a really long app name this is a really long app name", } @@ -396,7 +406,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "TargetCurrency" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "good name", TargetCurrency = "fake currency" @@ -406,7 +416,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "PerksTemplate" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "good name", PerksTemplate = "lol invalid template" @@ -416,7 +426,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "AppName", "TargetCurrency", "PerksTemplate" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { TargetCurrency = "fake currency", PerksTemplate = "lol invalid template" @@ -426,7 +436,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "AnimationColors" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "good name", AnimationColors = new string[] { } @@ -436,7 +446,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "AnimationColors" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "good name", AnimationColors = new string[] { " ", " " } @@ -446,7 +456,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "Sounds" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "good name", Sounds = new string[] { " " } @@ -456,7 +466,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "Sounds" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "good name", Sounds = new string[] { " ", " ", " " } @@ -466,7 +476,7 @@ namespace BTCPayServer.Tests await AssertValidationError(new[] { "EndDate" }, async () => await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "good name", StartDate = DateTime.Parse("1998-01-01"), @@ -478,13 +488,13 @@ namespace BTCPayServer.Tests // Test creating a crowdfund app var app = await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest() + new CrowdfundAppRequest { AppName = "test app from API", Title = "test app title" } ); - Assert.Equal("test app from API", app.Name); + Assert.Equal("test app from API", app.AppName); Assert.Equal(user.StoreId, app.StoreId); Assert.Equal("Crowdfund", app.AppType); Assert.False(app.Archived); @@ -492,9 +502,10 @@ namespace BTCPayServer.Tests // Test title falls back to name app = await client.CreateCrowdfundApp( user.StoreId, - new CreateCrowdfundAppRequest + new CrowdfundAppRequest { - AppName = "test app name" + AppName = "test app name", + Description = "test description" } ); Assert.Equal("test app name", app.Title); @@ -511,15 +522,16 @@ namespace BTCPayServer.Tests // Test that we can retrieve the app data var retrievedApp = await client.GetApp(app.Id); - Assert.Equal(app.Name, retrievedApp.Name); + Assert.Equal(app.AppName, retrievedApp.AppName); Assert.Equal(app.StoreId, retrievedApp.StoreId); Assert.Equal(app.AppType, retrievedApp.AppType); Assert.False(retrievedApp.Archived); // Test the crowdfund-specific endpoint also var retrievedCfApp = await client.GetCrowdfundApp(app.Id); - Assert.Equal(app.Name, retrievedCfApp.Name); + Assert.Equal(app.AppName, retrievedCfApp.AppName); Assert.Equal(app.Title, retrievedCfApp.Title); + Assert.Equal("test description", retrievedCfApp.Description); Assert.False(retrievedCfApp.Archived); // Make sure we return a 404 if we try to delete an app that doesn't exist @@ -548,17 +560,17 @@ namespace BTCPayServer.Tests var posApp = await client.CreatePointOfSaleApp( user.StoreId, - new CreatePointOfSaleAppRequest() + new PointOfSaleAppRequest { AppName = "test app from API", Currency = "JPY" } ); - var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() { AppName = "test app from API" }); + var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test app from API" }); // Create another store and one app on it so we can get all apps from all stores for the user below - var newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" }); - var newApp = await client.CreateCrowdfundApp(newStore.Id, new CreateCrowdfundAppRequest() { AppName = "new app" }); + var newStore = await client.CreateStore(new CreateStoreRequest { Name = "A" }); + var newApp = await client.CreateCrowdfundApp(newStore.Id, new CrowdfundAppRequest { AppName = "new app" }); Assert.NotEqual(newApp.Id, user.StoreId); @@ -567,12 +579,12 @@ namespace BTCPayServer.Tests Assert.Equal(2, apps.Length); - Assert.Equal(posApp.Name, apps[0].Name); + Assert.Equal(posApp.AppName, apps[0].AppName); Assert.Equal(posApp.StoreId, apps[0].StoreId); Assert.Equal(posApp.AppType, apps[0].AppType); Assert.False(apps[0].Archived); - Assert.Equal(crowdfundApp.Name, apps[1].Name); + Assert.Equal(crowdfundApp.AppName, apps[1].AppName); Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId); Assert.Equal(crowdfundApp.AppType, apps[1].AppType); Assert.False(apps[1].Archived); @@ -582,17 +594,17 @@ namespace BTCPayServer.Tests Assert.Equal(3, apps.Length); - Assert.Equal(posApp.Name, apps[0].Name); + Assert.Equal(posApp.AppName, apps[0].AppName); Assert.Equal(posApp.StoreId, apps[0].StoreId); Assert.Equal(posApp.AppType, apps[0].AppType); Assert.False(apps[0].Archived); - Assert.Equal(crowdfundApp.Name, apps[1].Name); + Assert.Equal(crowdfundApp.AppName, apps[1].AppName); Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId); Assert.Equal(crowdfundApp.AppType, apps[1].AppType); Assert.False(apps[1].Archived); - Assert.Equal(newApp.Name, apps[2].Name); + Assert.Equal(newApp.AppName, apps[2].AppName); Assert.Equal(newApp.StoreId, apps[2].StoreId); Assert.Equal(newApp.AppType, apps[2].AppType); Assert.False(apps[2].Archived); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5d6c94459..5c371963c 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -80,6 +80,7 @@ using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest; using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData; using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel; using Microsoft.Extensions.Caching.Memory; +using PosViewType = BTCPayServer.Client.Models.PosViewType; namespace BTCPayServer.Tests { @@ -3109,20 +3110,20 @@ namespace BTCPayServer.Tests var client = await acc.CreateClient(); var posController = acc.GetController(); - var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest() + var app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest { AppName = "Static", - DefaultView = Client.Models.PosViewType.Static, + DefaultView = PosViewType.Static, Template = new PointOfSaleSettings().Template }); var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea"); var invoiceId = GetInvoiceId(resp); await acc.PayOnChain(invoiceId); - app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest() + app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest { AppName = "Cart", - DefaultView = Client.Models.PosViewType.Cart, + DefaultView = PosViewType.Cart, Template = new PointOfSaleSettings().Template }); resp = await posController.ViewPointOfSale(app.Id, posData: new JObject() diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index ccc179289..b8809323a 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -17,7 +17,8 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType; +using CrowdfundResetEvery = BTCPayServer.Client.Models.CrowdfundResetEvery; +using PosViewType = BTCPayServer.Client.Models.PosViewType; namespace BTCPayServer.Controllers.Greenfield { @@ -47,19 +48,20 @@ namespace BTCPayServer.Controllers.Greenfield [HttpPost("~/api/v1/stores/{storeId}/apps/crowdfund")] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task CreateCrowdfundApp(string storeId, CreateCrowdfundAppRequest request) + public async Task CreateCrowdfundApp(string storeId, CrowdfundAppRequest request) { var store = await _storeRepository.FindStore(storeId); if (store == null) return this.CreateAPIError(404, "store-not-found", "The store was not found"); - // This is not obvious but we must have a non-null currency or else request validation may work incorrectly - request.TargetCurrency = request.TargetCurrency ?? store.GetStoreBlob().DefaultCurrency; + // This is not obvious, but we must have a non-null currency or else request validation may not work correctly + request.TargetCurrency ??= store.GetStoreBlob().DefaultCurrency; - var validationResult = ValidateCrowdfundAppRequest(request); - if (validationResult != null) + ValidateAppRequest(request); + ValidateCrowdfundAppRequest(request); + if (!ModelState.IsValid) { - return validationResult; + return this.CreateValidationError(ModelState); } var appData = new AppData @@ -70,7 +72,8 @@ namespace BTCPayServer.Controllers.Greenfield Archived = request.Archived ?? false }; - appData.SetSettings(ToCrowdfundSettings(request)); + var settings = ToCrowdfundSettings(request, new CrowdfundSettings { Title = request.Title ?? request.AppName }); + appData.SetSettings(settings); await _appService.UpdateOrCreateApp(appData); @@ -79,19 +82,20 @@ namespace BTCPayServer.Controllers.Greenfield [HttpPost("~/api/v1/stores/{storeId}/apps/pos")] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request) + public async Task CreatePointOfSaleApp(string storeId, PointOfSaleAppRequest request) { var store = await _storeRepository.FindStore(storeId); if (store == null) return this.CreateAPIError(404, "store-not-found", "The store was not found"); - // This is not obvious but we must have a non-null currency or else request validation may work incorrectly - request.Currency = request.Currency ?? store.GetStoreBlob().DefaultCurrency; + // This is not obvious, but we must have a non-null currency or else request validation may not work correctly + request.Currency ??= store.GetStoreBlob().DefaultCurrency; - var validationResult = ValidatePOSAppRequest(request); - if (validationResult != null) + ValidateAppRequest(request); + ValidatePOSAppRequest(request); + if (!ModelState.IsValid) { - return validationResult; + return this.CreateValidationError(ModelState); } var appData = new AppData @@ -102,7 +106,8 @@ namespace BTCPayServer.Controllers.Greenfield Archived = request.Archived ?? false }; - appData.SetSettings(ToPointOfSaleSettings(request)); + var settings = ToPointOfSaleSettings(request, new PointOfSaleSettings { Title = request.Title ?? request.AppName }); + appData.SetSettings(settings); await _appService.UpdateOrCreateApp(appData); @@ -111,7 +116,7 @@ namespace BTCPayServer.Controllers.Greenfield [HttpPut("~/api/v1/apps/pos/{appId}")] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request) + public async Task UpdatePointOfSaleApp(string appId, PointOfSaleAppRequest request) { var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true); if (app == null) @@ -121,21 +126,28 @@ namespace BTCPayServer.Controllers.Greenfield var settings = app.GetSettings(); - // This is not obvious but we must have a non-null currency or else request validation may work incorrectly - request.Currency = request.Currency ?? settings.Currency; + // This is not obvious, but we must have a non-null currency or else request validation may not work correctly + request.Currency ??= settings.Currency; - var validationResult = ValidatePOSAppRequest(request); - if (validationResult != null) + ValidatePOSAppRequest(request); + if (!string.IsNullOrEmpty(request.AppName)) { - return validationResult; + ValidateAppRequest(request); + } + if (!ModelState.IsValid) + { + return this.CreateValidationError(ModelState); } - app.Name = request.AppName; + if (!string.IsNullOrEmpty(request.AppName)) + { + app.Name = request.AppName; + } if (request.Archived != null) { app.Archived = request.Archived.Value; } - app.SetSettings(ToPointOfSaleSettings(request)); + app.SetSettings(ToPointOfSaleSettings(request, settings)); await _appService.UpdateOrCreateApp(app); @@ -218,11 +230,12 @@ namespace BTCPayServer.Controllers.Greenfield return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found"); } - private CrowdfundSettings ToCrowdfundSettings(CreateCrowdfundAppRequest request) + private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request, CrowdfundSettings settings) { var parsedSounds = ValidateStringArray(request.Sounds); var parsedColors = ValidateStringArray(request.AnimationColors); - + Enum.TryParse(request.ResetEvery.ToString(), true, out var resetEvery); + return new CrowdfundSettings { Title = request.Title?.Trim() ?? request.AppName, @@ -244,32 +257,36 @@ namespace BTCPayServer.Controllers.Greenfield SoundsEnabled = request.SoundsEnabled ?? parsedSounds != null, AnimationsEnabled = request.AnimationsEnabled ?? parsedColors != null, ResetEveryAmount = request.ResetEveryAmount ?? 1, - ResetEvery = (Services.Apps.CrowdfundResetEvery)request.ResetEvery, + ResetEvery = resetEvery, DisplayPerksValue = request.DisplayPerksValue ?? false, DisplayPerksRanking = request.DisplayPerksRanking ?? false, SortPerksByPopularity = request.SortPerksByPopularity ?? false, Sounds = parsedSounds ?? new CrowdfundSettings().Sounds, - AnimationColors = parsedColors ?? new CrowdfundSettings().AnimationColors + AnimationColors = parsedColors ?? new CrowdfundSettings().AnimationColors, + FormId = request.FormId }; } - private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request) + private PointOfSaleSettings ToPointOfSaleSettings(PointOfSaleAppRequest request, PointOfSaleSettings settings) { + Enum.TryParse(request.DefaultView.ToString(), true, out var defaultView); + return new PointOfSaleSettings { Title = request.Title ?? request.AppName, - DefaultView = (PosViewType)request.DefaultView, - ShowItems = request.ShowItems, - ShowCustomAmount = request.ShowCustomAmount, - ShowDiscount = request.ShowDiscount, - ShowSearch = request.ShowSearch, - ShowCategories = request.ShowCategories, - EnableTips = request.EnableTips, + DefaultView = defaultView, + ShowItems = request.ShowItems ?? false, + ShowCustomAmount = request.ShowCustomAmount ?? false, + ShowDiscount = request.ShowDiscount ?? false, + ShowSearch = request.ShowSearch ?? false, + ShowCategories = request.ShowCategories ?? false, + EnableTips = request.EnableTips ?? false, Currency = request.Currency, Template = request.Template != null ? AppService.SerializeTemplate(AppService.Parse(request.Template)) : null, ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF, CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF, CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF, + CustomTipPercentages = request.CustomTipPercentages, NotificationUrl = request.NotificationUrl, RedirectUrl = request.RedirectUrl, Description = request.Description, @@ -278,27 +295,27 @@ namespace BTCPayServer.Controllers.Greenfield }; } - private AppDataBase ToModel(AppData appData) + private AppBaseData ToModel(AppData appData) { - return new AppDataBase + return new AppBaseData { Id = appData.Id, Archived = appData.Archived, AppType = appData.AppType, - Name = appData.Name, + AppName = appData.Name, StoreId = appData.StoreDataId, Created = appData.Created, }; } - private AppDataBase ToModel(Models.AppViewModels.ListAppsViewModel.ListAppViewModel appData) + private AppBaseData ToModel(Models.AppViewModels.ListAppsViewModel.ListAppViewModel appData) { - return new AppDataBase + return new AppBaseData { Id = appData.Id, Archived = appData.Archived, AppType = appData.AppType, - Name = appData.AppName, + AppName = appData.AppName, StoreId = appData.StoreId, Created = appData.Created, }; @@ -307,17 +324,18 @@ namespace BTCPayServer.Controllers.Greenfield private PointOfSaleAppData ToPointOfSaleModel(AppData appData) { var settings = appData.GetSettings(); - + Enum.TryParse(settings.DefaultView.ToString(), true, out var defaultView); + return new PointOfSaleAppData { Id = appData.Id, Archived = appData.Archived, AppType = appData.AppType, - Name = appData.Name, + AppName = appData.Name, StoreId = appData.StoreDataId, Created = appData.Created, Title = settings.Title, - DefaultView = settings.DefaultView.ToString(), + DefaultView = defaultView, ShowItems = settings.ShowItems, ShowCustomAmount = settings.ShowCustomAmount, ShowDiscount = settings.ShowDiscount, @@ -325,28 +343,30 @@ namespace BTCPayServer.Controllers.Greenfield ShowCategories = settings.ShowCategories, EnableTips = settings.EnableTips, Currency = settings.Currency, - Items = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject( - AppService.Parse(settings.Template), - new JsonSerializerSettings - { - ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() - } - ) - ), FixedAmountPayButtonText = settings.ButtonText, CustomAmountPayButtonText = settings.CustomButtonText, TipText = settings.CustomTipText, + CustomTipPercentages = settings.CustomTipPercentages, + FormId = settings.FormId, NotificationUrl = settings.NotificationUrl, RedirectUrl = settings.RedirectUrl, Description = settings.Description, - RedirectAutomatically = settings.RedirectAutomatically ?? false, + RedirectAutomatically = settings.RedirectAutomatically, + Items = JsonConvert.DeserializeObject( + JsonConvert.SerializeObject( + AppService.Parse(settings.Template), + new JsonSerializerSettings + { + ContractResolver = + new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() + } + ) + ) }; } - private IActionResult? ValidatePOSAppRequest(CreatePointOfSaleAppRequest request) + private void ValidatePOSAppRequest(PointOfSaleAppRequest request) { - var validationResult = ValidateCreateAppRequest(request); if (request.Currency != null && _currencies.GetCurrencyData(request.Currency, false) == null) { ModelState.AddModelError(nameof(request.Currency), "Invalid currency"); @@ -364,25 +384,19 @@ namespace BTCPayServer.Controllers.Greenfield ModelState.AddModelError(nameof(request.Template), "Invalid template"); } } - - if (!ModelState.IsValid) - { - validationResult = this.CreateValidationError(ModelState); - } - - return validationResult; } private CrowdfundAppData ToCrowdfundModel(AppData appData) { var settings = appData.GetSettings(); + Enum.TryParse(settings.ResetEvery.ToString(), true, out var resetEvery); return new CrowdfundAppData { Id = appData.Id, Archived = appData.Archived, AppType = appData.AppType, - Name = appData.Name, + AppName = appData.Name, StoreId = appData.StoreDataId, Created = appData.Created, Title = settings.Title, @@ -396,6 +410,17 @@ namespace BTCPayServer.Controllers.Greenfield MainImageUrl = settings.MainImageUrl, NotificationUrl = settings.NotificationUrl, Tagline = settings.Tagline, + DisqusEnabled = settings.DisqusEnabled, + DisqusShortname = settings.DisqusShortname, + SoundsEnabled = settings.SoundsEnabled, + AnimationsEnabled = settings.AnimationsEnabled, + ResetEveryAmount = settings.ResetEveryAmount, + ResetEvery = resetEvery, + DisplayPerksValue = settings.DisplayPerksValue, + DisplayPerksRanking = settings.DisplayPerksRanking, + SortPerksByPopularity = settings.SortPerksByPopularity, + Sounds = settings.Sounds, + AnimationColors = settings.AnimationColors, Perks = JsonConvert.DeserializeObject( JsonConvert.SerializeObject( AppService.Parse(settings.PerksTemplate), @@ -404,18 +429,7 @@ namespace BTCPayServer.Controllers.Greenfield ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() } ) - ), - DisqusEnabled = settings.DisqusEnabled, - DisqusShortname = settings.DisqusShortname, - SoundsEnabled = settings.SoundsEnabled, - AnimationsEnabled = settings.AnimationsEnabled, - ResetEveryAmount = settings.ResetEveryAmount, - ResetEvery = settings.ResetEvery.ToString(), - DisplayPerksValue = settings.DisplayPerksValue, - DisplayPerksRanking = settings.DisplayPerksRanking, - SortPerksByPopularity = settings.SortPerksByPopularity, - Sounds = settings.Sounds, - AnimationColors = settings.AnimationColors + ) }; } @@ -435,34 +449,38 @@ namespace BTCPayServer.Controllers.Greenfield return arr.Select(s => s.Trim()).ToArray(); } - private IActionResult? ValidateCrowdfundAppRequest(CreateCrowdfundAppRequest request) + private void ValidateCrowdfundAppRequest(CrowdfundAppRequest request) { - var validationResult = ValidateCreateAppRequest(request); if (request.TargetCurrency != null && _currencies.GetCurrencyData(request.TargetCurrency, false) == null) { ModelState.AddModelError(nameof(request.TargetCurrency), "Invalid currency"); } - try + if (request.PerksTemplate != null) { - // Just checking if we can serialize - AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate)); - } - catch - { - ModelState.AddModelError(nameof(request.PerksTemplate), "Invalid template"); + try + { + // Just checking if we can serialize + AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate)); + } + catch + { + ModelState.AddModelError(nameof(request.PerksTemplate), "Invalid template"); + } } - if (request.ResetEvery != Client.Models.CrowdfundResetEvery.Never && request.StartDate == null) + if (request.ResetEvery.HasValue && request.ResetEvery != CrowdfundResetEvery.Never) { - ModelState.AddModelError(nameof(request.StartDate), "A start date is needed when the goal resets every X amount of time"); + if (request.StartDate == null) + { + ModelState.AddModelError(nameof(request.StartDate), "A start date is needed when the goal resets every X amount of time"); + } + if (request.ResetEveryAmount <= 0) + { + ModelState.AddModelError(nameof(request.ResetEveryAmount), "You must reset the goal at a minimum of 1"); + } } - - if (request.ResetEvery != Client.Models.CrowdfundResetEvery.Never && request.ResetEveryAmount <= 0) - { - ModelState.AddModelError(nameof(request.ResetEveryAmount), "You must reset the goal at a minimum of 1"); - } - + if (request.Sounds != null && ValidateStringArray(request.Sounds) == null) { ModelState.AddModelError(nameof(request.Sounds), "Sounds must be a non-empty array of non-empty strings"); @@ -473,36 +491,22 @@ namespace BTCPayServer.Controllers.Greenfield ModelState.AddModelError(nameof(request.AnimationColors), "Animation colors must be a non-empty array of non-empty strings"); } - if (request.StartDate != null && request.EndDate != null && DateTimeOffset.Compare((DateTimeOffset)request.StartDate, (DateTimeOffset)request.EndDate!) > 0) + if (request is { StartDate: not null, EndDate: not null } && DateTimeOffset.Compare((DateTimeOffset)request.StartDate, (DateTimeOffset)request.EndDate!) > 0) { ModelState.AddModelError(nameof(request.EndDate), "End date cannot be before start date"); } - - if (!ModelState.IsValid) - { - validationResult = this.CreateValidationError(ModelState); - } - - return validationResult; } - private IActionResult? ValidateCreateAppRequest(CreateAppRequest request) + private void ValidateAppRequest(IAppRequest? request) { - if (request is null) - { - return BadRequest(); - } - - if (string.IsNullOrEmpty(request.AppName)) + if (string.IsNullOrEmpty(request?.AppName)) { ModelState.AddModelError(nameof(request.AppName), "App name is missing"); } - else if (request.AppName.Length < 1 || request.AppName.Length > 50) + else if (request.AppName.Length is < 1 or > 50) { - ModelState.AddModelError(nameof(request.AppName), "Name can only be between 1 and 50 characters"); + ModelState.AddModelError(nameof(request.AppName), "App name can only be between 1 and 50 characters"); } - - return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null; } } } diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index f3fee719f..2d443d3dc 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -1088,7 +1088,7 @@ namespace BTCPayServer.Controllers.Greenfield public override async Task CreatePointOfSaleApp( string storeId, - CreatePointOfSaleAppRequest request, CancellationToken token = default) + PointOfSaleAppRequest request, CancellationToken token = default) { return GetFromActionResult( await GetController().CreatePointOfSaleApp(storeId, request)); @@ -1096,7 +1096,7 @@ namespace BTCPayServer.Controllers.Greenfield public override async Task UpdatePointOfSaleApp( string appId, - CreatePointOfSaleAppRequest request, CancellationToken token = default) + PointOfSaleAppRequest request, CancellationToken token = default) { return GetFromActionResult( await GetController().UpdatePointOfSaleApp(appId, request)); @@ -1104,27 +1104,27 @@ namespace BTCPayServer.Controllers.Greenfield public override async Task CreateCrowdfundApp( string storeId, - CreateCrowdfundAppRequest request, CancellationToken token = default) + CrowdfundAppRequest request, CancellationToken token = default) { return GetFromActionResult( await GetController().CreateCrowdfundApp(storeId, request)); } - public override async Task GetApp(string appId, CancellationToken token = default) + public override async Task GetApp(string appId, CancellationToken token = default) { - return GetFromActionResult( + return GetFromActionResult( await GetController().GetApp(appId)); } - public override async Task GetAllApps(string storeId, CancellationToken token = default) + public override async Task GetAllApps(string storeId, CancellationToken token = default) { - return GetFromActionResult( + return GetFromActionResult( await GetController().GetAllApps(storeId)); } - public override async Task GetAllApps(CancellationToken token = default) + public override async Task GetAllApps(CancellationToken token = default) { - return GetFromActionResult( + return GetFromActionResult( await GetController().GetAllApps()); } diff --git a/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs b/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs index 6136eaaa9..f9eb1cdf3 100644 --- a/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs +++ b/BTCPayServer/Plugins/PointOfSale/PointOfSalePlugin.cs @@ -13,7 +13,6 @@ using BTCPayServer.Plugins.PointOfSale.Controllers; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; -using Ganss.Xss; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/BTCPayServer/Services/Apps/CrowdfundSettings.cs b/BTCPayServer/Services/Apps/CrowdfundSettings.cs index 8cc9e5602..974e2e391 100644 --- a/BTCPayServer/Services/Apps/CrowdfundSettings.cs +++ b/BTCPayServer/Services/Apps/CrowdfundSettings.cs @@ -10,7 +10,6 @@ namespace BTCPayServer.Services.Apps public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public string TargetCurrency { get; set; } - decimal? _TargetAmount; public decimal? TargetAmount { @@ -41,16 +40,14 @@ namespace BTCPayServer.Services.Apps public bool DisplayPerksRanking { get; set; } public bool DisplayPerksValue { get; set; } public bool SortPerksByPopularity { get; set; } - public string FormId { get; set; } = null; - - + public string FormId { get; set; } public string[] AnimationColors { get; set; } = - { + [ "#FF6138", "#FFBE53", "#2980B9", "#282741" - }; + ]; public string[] Sounds { get; set; } = - { + [ "https://github.com/ClaudiuHKS/AdvancedQuakeSounds/tree/master/sound/AQS/dominating.wav", "https://github.com/ClaudiuHKS/AdvancedQuakeSounds/tree/master/sound/AQS/doublekill.wav", "https://github.com/ClaudiuHKS/AdvancedQuakeSounds/tree/master/sound/AQS/doublekill2.wav", @@ -78,7 +75,7 @@ namespace BTCPayServer.Services.Apps "https://github.com/ClaudiuHKS/AdvancedQuakeSounds/tree/master/sound/AQS/ultrakill.wav", "https://github.com/ClaudiuHKS/AdvancedQuakeSounds/tree/master/sound/AQS/unstoppable.wav", "https://github.com/ClaudiuHKS/AdvancedQuakeSounds/tree/master/sound/AQS/whickedsick.wav" - }; + ]; } public enum CrowdfundResetEvery { diff --git a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs index d9883896d..44f424a9d 100644 --- a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs +++ b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs @@ -1,4 +1,3 @@ -using BTCPayServer.Client.Models; using BTCPayServer.Plugins.PointOfSale.Models; using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType; @@ -89,19 +88,17 @@ namespace BTCPayServer.Services.Apps public bool ShowItems { get; set; } public bool ShowCustomAmount { get; set; } public bool ShowDiscount { get; set; } - public bool ShowSearch { get; set; } = true; - public bool ShowCategories { get; set; } = true; + public bool ShowSearch { get; set; } + public bool ShowCategories { get; set; } public bool EnableTips { get; set; } - - public string FormId { get; set; } = null; - + public string FormId { get; set; } public const string BUTTON_TEXT_DEF = "Buy for {0}"; public string ButtonText { get; set; } = BUTTON_TEXT_DEF; public const string CUSTOM_BUTTON_TEXT_DEF = "Pay"; public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF; public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?"; public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF; - public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 }; + public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = { 15, 18, 20 }; public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF; public string Description { get; set; } public string NotificationUrl { get; set; } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json index 4e25b29f5..67a9c2efc 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json @@ -21,7 +21,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreatePointOfSaleAppRequest" + "$ref": "#/components/schemas/PointOfSaleAppRequest" } } }, @@ -84,7 +84,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreatePointOfSaleAppRequest" + "$ref": "#/components/schemas/PointOfSaleAppRequest" } } }, @@ -223,7 +223,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateCrowdfundAppRequest" + "$ref": "#/components/schemas/CrowdfundAppRequest" } } }, @@ -291,7 +291,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BasicAppData" + "$ref": "#/components/schemas/AppBaseData" } } } @@ -372,7 +372,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/BasicAppData" + "$ref": "#/components/schemas/AppBaseData" } } } @@ -405,7 +405,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/BasicAppData" + "$ref": "#/components/schemas/AppBaseData" } } } @@ -425,10 +425,46 @@ }, "components": { "schemas": { - "PointOfSaleAppData": { + "AppBaseData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Id of the app", + "example": "3ki4jsAkN4u9rv1PUzj1odX4Nx7s" + }, + "appName": { + "type": "string", + "description": "Name given to the app when it was created", + "example": "my test app" + }, + "storeId": { + "type": "string", + "description": "Id of the store to which the app belongs", + "example": "9CiNzKoANXxmk5ayZngSXrHTiVvvgCrwrpFQd4m2K776" + }, + "created": { + "type": "integer", + "example": 1651554744, + "description": "UNIX timestamp for when the app was created" + }, + "appType": { + "type": "string", + "example": "PointOfSale", + "description": "Type of the app which was created" + }, + "archived": { + "type": "boolean", + "description": "If true, the app does not appear in the apps list by default.", + "default": false, + "nullable": true + } + } + }, + "PointOfSaleBaseData": { "allOf": [ { - "$ref": "#/components/schemas/BasicAppData" + "$ref": "#/components/schemas/AppBaseData" }, { "type": "object", @@ -436,12 +472,14 @@ "title": { "type": "string", "description": "Display title of the app", - "example": "My PoS app" + "example": "My PoS app", + "nullable": true }, "description": { "type": "string", "description": "App description", - "example": "This is my amazing PoS app" + "example": "This is my amazing PoS app", + "nullable": true }, "defaultView": { "type": "string", @@ -458,48 +496,132 @@ "Cart", "Light", "Print" - ] + ], + "nullable": true }, "showItems": { "type": "boolean", "default": false, "description": "Display item selection for keypad", - "example": true + "example": true, + "nullable": true }, "showCustomAmount": { "type": "boolean", "description": "Whether the option to enter a custom amount is shown", - "example": true + "example": true, + "nullable": true }, "showDiscount": { "default": false, "type": "boolean", "description": "Whether the option to enter a discount is shown", - "example": false + "example": false, + "nullable": true }, "showSearch": { "type": "boolean", "description": "Display the search bar", "example": false, - "default": true + "default": true, + "nullable": true }, "showCategories": { "type": "boolean", "description": "Display the list of categories", "example": false, - "default": true + "default": true, + "nullable": true }, "enableTips": { "default": false, "type": "boolean", "description": "Whether the option to enter a tip is shown", - "example": true + "example": true, + "nullable": true }, "currency": { "type": "string", "description": "Currency used for the app", - "example": "BTC" + "example": "BTC", + "nullable": true }, + "fixedAmountPayButtonText": { + "type": "string", + "description": "Payment button text template for items with a set price", + "example": "Buy for {0}", + "nullable": true + }, + "customAmountPayButtonText": { + "type": "string", + "description": "Payment button text which appears for items which allow user to input a custom amount", + "example": "Pay", + "nullable": true + }, + "tipText": { + "type": "string", + "description": "Prompt which appears next to the tip amount field if tipping is enabled", + "example": "Do you want to leave a tip?", + "nullable": true + }, + "customTipPercentages": { + "type": "array", + "description": "Array of predefined tip percentage amounts", + "items": { + "type": "number" + }, + "default": [15,18,20], + "nullable": true + }, + "notificationUrl": { + "type": "string", + "description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations", + "nullable": true + }, + "redirectUrl": { + "type": "string", + "description": "URL user is redirected to once invoice is paid", + "nullable": true + }, + "redirectAutomatically": { + "type": "boolean", + "description": "Whether user is redirected to specified redirect URL automatically after the invoice is paid", + "example": true, + "nullable": true + }, + "formId": { + "type": "string", + "description": "Form ID to request customer data", + "nullable": true + } + } + } + ] + }, + "PointOfSaleAppRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/PointOfSaleBaseData" + }, + { + "type": "object", + "properties": { + "template": { + "type": "string", + "description": "JSON of item available in the app" + } + } + } + ] + }, + "PointOfSaleAppData": { + "allOf": [ + { + "$ref": "#/components/schemas/PointOfSaleBaseData" + }, + { + "type": "object", + "properties": { "items": { "type": "object", "description": "JSON object of app items", @@ -535,34 +657,169 @@ "disabled": false } ] - }, - "fixedAmountPayButtonText": { + } + } + } + ] + }, + "CrowdfundBaseData": { + "allOf": [ + { + "$ref": "#/components/schemas/AppBaseData" + }, + { + "type": "object", + "properties": { + "title": { "type": "string", - "description": "Payment button text template for items with a set price", - "example": "Buy for {0}" + "description": "Display title of the app", + "example": "My crowdfund app", + "nullable": true }, - "customAmountPayButtonText": { + "description": { "type": "string", - "description": "Payment button text which appears for items which allow user to input a custom amount", - "example": "Pay" + "description": "App description", + "example": "My crowdfund description", + "nullable": true }, - "tipText": { + "enabled": { + "type": "boolean", + "description": "Whether the app is enabled to be viewed by everyone", + "example": true, + "nullable": true + }, + "enforceTargetAmount": { + "type": "boolean", + "description": "Whether contributions over the set target amount are allowed", + "example": false, + "nullable": true + }, + "startDate": { + "type": "number", + "description": "UNIX timestamp for crowdfund start time (https://www.unixtimestamp.com/)", + "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}], + "example": 768658369, + "nullable": true + }, + "endDate": { + "type": "number", + "description": "UNIX timestamp for crowdfund end time (https://www.unixtimestamp.com/)", + "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}], + "example": 771336769, + "nullable": true + }, + "targetCurrency": { "type": "string", - "description": "Prompt which appears next to the tip amount field if tipping is enabled", - "example": "Do you want to leave a tip?" + "description": "Target currency for the crowdfund", + "example": "BTC", + "nullable": true + }, + "targetAmount": { + "type": "number", + "description": "Target amount for the crowdfund", + "example": 420.69, + "nullable": true + }, + "mainImageUrl": { + "type": "string", + "description": "URL for image used as a cover image for the app", + "nullable": true }, "notificationUrl": { "type": "string", - "description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations" + "description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations", + "nullable": true }, - "redirectUrl": { + "tagline": { "type": "string", - "description": "URL user is redirected to once invoice is paid" + "description": "Tagline for the app displayed to user", + "example": "I can't believe it's not butter", + "nullable": true }, - "redirectAutomatically": { + "disqusEnabled": { "type": "boolean", - "description": "Whether user is redirected to specified redirect URL automatically after the invoice is paid", - "example": true + "description": "Whether Disqus is enabled for the app", + "nullable": true + }, + "disqusShortname": { + "type": "string", + "description": "Disqus shortname to used for the app", + "nullable": true + }, + "soundsEnabled": { + "type": "boolean", + "description": "Whether sounds on new contributions are enabled", + "example": false, + "nullable": true + }, + "animationsEnabled": { + "type": "boolean", + "description": "Whether background animations on new contributions are enabled", + "example": true, + "nullable": true + }, + "resetEveryAmount": { + "type": "number", + "description": "Contribution goal reset frequency amount", + "example": 1, + "nullable": true + }, + "resetEvery": { + "type": "string", + "description": "Contribution goal reset frequency", + "example": "Day", + "nullable": true + }, + "displayPerksValue": { + "type": "boolean", + "description": "Whether perk values are displayed", + "example": false, + "nullable": true + }, + "sortPerksByPopularity": { + "type": "boolean", + "description": "Whether perks are sorted by popularity", + "default": true, + "nullable": true + }, + "sounds": { + "type": "array", + "description": "Array of custom sounds which can be used on new contributions", + "items": { + "type": "string" + }, + "example": ["https://github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/AQS/doublekill.wav"], + "nullable": true + }, + "animationColors": { + "type": "array", + "description": "Array of custom HEX colors which can be used for background animations on new contributions", + "items": { + "type": "string" + }, + "example": ["#FF0000", "#00FF00", "#0000FF"], + "nullable": true + }, + "formId": { + "type": "string", + "description": "Form ID to request customer data", + "nullable": true + } + } + } + ] + }, + "CrowdfundAppRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CrowdfundBaseData" + }, + { + "type": "object", + "properties": { + "perksTemplate": { + "type": "string", + "description": "JSON of perks available in the app" } } } @@ -571,57 +828,11 @@ "CrowdfundAppData": { "allOf": [ { - "$ref": "#/components/schemas/BasicAppData" + "$ref": "#/components/schemas/CrowdfundBaseData" }, { "type": "object", "properties": { - "title": { - "type": "string", - "description": "Display title of the app", - "example": "My crowdfund app" - }, - "description": { - "type": "string", - "description": "App description", - "example": "My crowdfund description" - }, - "enabled": { - "type": "boolean", - "description": "Whether the app is enabled to be viewed by everyone", - "example": true - }, - "enforceTargetAmount": { - "type": "boolean", - "description": "Whether contributions over the set target amount are allowed", - "example": false - }, - "startDate": { - "type": "number", - "description": "UNIX timestamp for crowdfund start time (https://www.unixtimestamp.com/)", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}], - "example": 768658369 - }, - "endDate": { - "type": "number", - "description": "UNIX timestamp for crowdfund end time (https://www.unixtimestamp.com/)", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}], - "example": 771336769 - }, - "targetCurrency": { - "type": "string", - "description": "Target currency for the crowdfund", - "example": "BTC" - }, - "targetAmount": { - "type": "number", - "description": "Target amount for the crowdfund", - "example": 420.69 - }, - "mainImageUrl": { - "type": "string", - "description": "URL for image used as a cover image for the app" - }, "perks": { "type": "object", "description": "JSON of perks available in the app", @@ -672,369 +883,10 @@ "disabled": true } ] - }, - "notificationUrl": { - "type": "string", - "description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations" - }, - "tagline": { - "type": "string", - "description": "Tagline for the app displayed to user", - "example": "I can't believe it's not butter" - }, - "disqusEnabled": { - "type": "boolean", - "description": "Whether Disqus is enabled for the app" - }, - "disqusShortname": { - "type": "string", - "description": "Disqus shortname to used for the app" - }, - "soundsEnabled": { - "type": "boolean", - "description": "Whether sounds on new contributions are enabled", - "example": false - }, - "animationsEnabled": { - "type": "boolean", - "description": "Whether background animations on new contributions are enabled", - "example": true - }, - "resetEveryAmount": { - "type": "number", - "description": "Contribution goal reset frequency amount", - "example": 1 - }, - "resetEvery": { - "type": "string", - "description": "Contribution goal reset frequency", - "example": "Day" - }, - "displayPerksValue": { - "type": "boolean", - "description": "Whether perk values are displayed", - "example": false - }, - "sortPerksByPopularity": { - "type": "boolean", - "description": "Whether perks are sorted by popularity", - "default": true - }, - "sounds": { - "type": "array", - "description": "Array of custom sounds which can be used on new contributions", - "items": { - "type": "string" - }, - "example": ["https://github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/AQS/doublekill.wav"] - }, - "animationColors": { - "type": "array", - "description": "Array of custom HEX colors which can be used for background animations on new contributions", - "items": { - "type": "string" - }, - "example": ["#FF0000", "#00FF00", "#0000FF"] } } } ] - }, - "BasicAppData": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Id of the app", - "example": "3ki4jsAkN4u9rv1PUzj1odX4Nx7s" - }, - "name": { - "type": "string", - "description": "Name given to the app when it was created", - "example": "my test app" - }, - "storeId": { - "type": "string", - "description": "Id of the store to which the app belongs", - "example": "9CiNzKoANXxmk5ayZngSXrHTiVvvgCrwrpFQd4m2K776" - }, - "created": { - "type": "integer", - "example": 1651554744, - "description": "UNIX timestamp for when the app was created" - }, - "appType": { - "type": "string", - "example": "PointOfSale", - "description": "Type of the app which was created" - }, - "archived": { - "type": "boolean", - "description": "If true, the app does not appear in the apps list by default.", - "default": false, - "nullable": true - } - } - }, - "CreatePointOfSaleAppRequest": { - "type": "object", - "properties": { - "appName": { - "type": "string", - "description": "The name of the app (shown in admin UI)", - "nullable": false - }, - "title": { - "type": "string", - "description": "The title of the app (shown to the user)", - "nullable": true - }, - "description": { - "type": "string", - "description": "The description of the app", - "nullable": true - }, - "template": { - "type": "string", - "description": "Template for items available in the app", - "nullable": true - }, - "defaultView": { - "type": "string", - "description": "Template for items available in the app", - "nullable": true, - "x-enumNames": [ - "Static", - "Cart", - "Light", - "Print" - ], - "enum": [ - "Static", - "Cart", - "Light", - "Print" - ] - }, - "currency": { - "type": "string", - "description": "Currency to use for the app. Defaults to the currency used by the store if not specified", - "example": "BTC", - "nullable": true - }, - "showCustomAmount": { - "type": "boolean", - "description": "Whether to include a special item in the store which allows user to input a custom payment amount", - "default": false, - "nullable": true - }, - "showDiscount": { - "type": "boolean", - "description": "Whether to allow user to input a discount amount. Applies to Cart view only. Not recommended for customer self-checkout", - "default": true, - "nullable": true - }, - "enableTips": { - "type": "boolean", - "description": "Whether to allow user to input a tip amount. Applies to Cart and Light views only", - "default": true, - "nullable": true - }, - "customAmountPayButtonText": { - "type": "string", - "description": "Payment button text which appears for items which allow user to input a custom amount", - "default": "Pay", - "nullable": true - }, - "fixedAmountPayButtonText": { - "type": "string", - "description": "Payment button text which appears for items which have a fixed price", - "default": "Buy for {PRICE_HERE}", - "nullable": true - }, - "tipText": { - "type": "string", - "description": "Prompt which appears next to the tip amount field if tipping is enabled", - "default": "Do you want to leave a tip?", - "nullable": true - }, - "notificationUrl": { - "type": "string", - "description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations", - "nullable": true - }, - "redirectUrl": { - "type": "string", - "description": "URL to redirect user to once invoice is paid", - "nullable": true - }, - "redirectAutomatically": { - "type": "boolean", - "description": "Whether to redirect user to redirect URL automatically once invoice is paid. Defaults to what is set in the store settings", - "nullable": true - }, - "formId": { - "type": "string", - "description": "Form ID to request customer data", - "nullable": true - } - } - }, - "CreateCrowdfundAppRequest": { - "type": "object", - "properties": { - "appName": { - "type": "string", - "description": "The name of the app (shown in admin UI)", - "example": "Kukkstarter", - "nullable": false - }, - "title": { - "type": "string", - "description": "The title of the app (shown to the user)", - "example": "My crowdfund app", - "nullable": true - }, - "description": { - "type": "string", - "description": "The description of the app (shown to the user)", - "example": "My app description", - "nullable": true - }, - "enabled": { - "type": "boolean", - "description": "Determines if the app is enabled to be viewed by everyone", - "default": true, - "nullable": true - }, - "enforceTargetAmount": { - "type": "boolean", - "description": "Will not allow contributions over the set target amount", - "default": false, - "nullable": true - }, - "startDate": { - "type": "number", - "description": "UNIX timestamp for crowdfund start time (https://www.unixtimestamp.com/)", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}], - "example": 768658369, - "nullable": true - }, - "endDate": { - "type": "number", - "description": "UNIX timestamp for crowdfund end time (https://www.unixtimestamp.com/)", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}], - "example": 771336769, - "nullable": true - }, - "targetCurrency": { - "type": "string", - "description": "Target currency for the crowdfund. Defaults to the currency used by the store if not specified", - "example": "BTC", - "nullable": true - }, - "targetAmount": { - "type": "number", - "description": "Target amount for the crowdfund", - "example": 420, - "nullable": true - }, - "mainImageUrl": { - "type": "string", - "description": "URL for image to be used as a cover image for the app", - "nullable": true - }, - "perksTemplate": { - "type": "string", - "description": "YAML template of perks available in the app", - "example": "test_perk:\r\n price: 100\r\n title: test perk\r\n price_type: \"fixed\" \r\n disabled: false", - "nullable": true - }, - "notificationUrl": { - "type": "string", - "description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations", - "nullable": true - }, - "tagline": { - "type": "string", - "description": "Tagline for the app (shown to the user)", - "example": "I can't believe it's not butter", - "nullable": true - }, - "disqusShortname": { - "type": "string", - "description": "Disqus shortname to used for the app. Enables Disqus functionality if set.", - "nullable": true - }, - "soundsEnabled": { - "type": "boolean", - "description": "Enables sounds on new contributions if set to true", - "default": false, - "nullable": true - }, - "animationsEnabled": { - "type": "boolean", - "description": "Enables background animations on new contributions if set to true", - "default": false, - "nullable": true - }, - "resetEveryAmount": { - "type": "number", - "description": "Contribution goal reset frequency amount. Must be used in conjunction with resetEvery and startDate.", - "default": 1, - "nullable": true - }, - "resetEvery": { - "type": "string", - "description": "Contribution goal reset frequency. Must be used in conjunction with resetEveryAmount and startDate.", - "nullable": true, - "default": "Never", - "x-enumNames": [ - "Never", - "Hour", - "Day", - "Month", - "Year" - ], - "enum": [ - "Never", - "Hour", - "Day", - "Month", - "Year" - ] - }, - "displayPerksValue": { - "type": "boolean", - "description": "Displays values of perks if set to true", - "default": false, - "nullable": true - }, - "sortPerksByPopularity": { - "type": "boolean", - "description": "Sorts perks by popularity if set to true", - "default": false, - "nullable": true - }, - "sounds": { - "type": "array", - "description": "Array of custom sounds to use on new contributions", - "items": { - "type": "string" - }, - "nullable": true, - "example": [ "https://github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/AQS/doublekill.wav" ] - }, - "animationColors": { - "type": "array", - "description": "Array of custom HEX colors to use for background animations on new contributions", - "items": { - "type": "string" - }, - "nullable": true, - "example": ["#FF0000", "#00FF00", "#0000FF"] - } - } } } },