Greenfield: Refactor app endpoints (#6051)

* Greenfield: Refactor app endpoints

- Do not change unset data
- Clean up difference between request (template) and data (items/perks)
- Add missing properties (form id and custom tip percentage)
- Update docs

* Revert ToSettings changes in GreenfieldAppsController
This commit is contained in:
d11n 2024-06-26 10:42:22 +02:00 committed by GitHub
parent bf66b54c9a
commit 2482b9df74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 611 additions and 777 deletions

View File

@ -9,41 +9,41 @@ namespace BTCPayServer.Client;
public partial class BTCPayServerClient
{
public virtual async Task<PointOfSaleAppData> CreatePointOfSaleApp(string storeId,
CreatePointOfSaleAppRequest request, CancellationToken token = default)
PointOfSaleAppRequest request, CancellationToken token = default)
{
if (request == null) throw new ArgumentNullException(nameof(request));
return await SendHttpRequest<PointOfSaleAppData>($"api/v1/stores/{storeId}/apps/pos", request, HttpMethod.Post, token);
}
public virtual async Task<CrowdfundAppData> CreateCrowdfundApp(string storeId,
CreateCrowdfundAppRequest request, CancellationToken token = default)
CrowdfundAppRequest request, CancellationToken token = default)
{
if (request == null) throw new ArgumentNullException(nameof(request));
return await SendHttpRequest<CrowdfundAppData>($"api/v1/stores/{storeId}/apps/crowdfund", request, HttpMethod.Post, token);
}
public virtual async Task<PointOfSaleAppData> UpdatePointOfSaleApp(string appId,
CreatePointOfSaleAppRequest request, CancellationToken token = default)
PointOfSaleAppRequest request, CancellationToken token = default)
{
if (request == null) throw new ArgumentNullException(nameof(request));
return await SendHttpRequest<PointOfSaleAppData>($"api/v1/apps/pos/{appId}", request, HttpMethod.Put, token);
}
public virtual async Task<AppDataBase> GetApp(string appId, CancellationToken token = default)
public virtual async Task<AppBaseData> GetApp(string appId, CancellationToken token = default)
{
if (appId == null) throw new ArgumentNullException(nameof(appId));
return await SendHttpRequest<AppDataBase>($"api/v1/apps/{appId}", null, HttpMethod.Get, token);
return await SendHttpRequest<AppBaseData>($"api/v1/apps/{appId}", null, HttpMethod.Get, token);
}
public virtual async Task<AppDataBase[]> GetAllApps(string storeId, CancellationToken token = default)
public virtual async Task<AppBaseData[]> GetAllApps(string storeId, CancellationToken token = default)
{
if (storeId == null) throw new ArgumentNullException(nameof(storeId));
return await SendHttpRequest<AppDataBase[]>($"api/v1/stores/{storeId}/apps", null, HttpMethod.Get, token);
return await SendHttpRequest<AppBaseData[]>($"api/v1/stores/{storeId}/apps", null, HttpMethod.Get, token);
}
public virtual async Task<AppDataBase[]> GetAllApps(CancellationToken token = default)
public virtual async Task<AppBaseData[]> GetAllApps(CancellationToken token = default)
{
return await SendHttpRequest<AppDataBase[]>("api/v1/apps", null, HttpMethod.Get, token);
return await SendHttpRequest<AppBaseData[]>("api/v1/apps", null, HttpMethod.Get, token);
}
public virtual async Task<PointOfSaleAppData> GetPosApp(string appId, CancellationToken token = default)

View File

@ -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; }
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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);

View File

@ -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<UIPointOfSaleController>();
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()

View File

@ -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<IActionResult> CreateCrowdfundApp(string storeId, CreateCrowdfundAppRequest request)
public async Task<IActionResult> 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<IActionResult> CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request)
public async Task<IActionResult> 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<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
public async Task<IActionResult> 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<PointOfSaleSettings>();
// 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<BTCPayServer.Services.Apps.CrowdfundResetEvery>(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<BTCPayServer.Plugins.PointOfSale.PosViewType>(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<PointOfSaleSettings>();
Enum.TryParse<PosViewType>(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<CrowdfundSettings>();
Enum.TryParse<CrowdfundResetEvery>(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;
}
}
}

View File

@ -1088,7 +1088,7 @@ namespace BTCPayServer.Controllers.Greenfield
public override async Task<PointOfSaleAppData> CreatePointOfSaleApp(
string storeId,
CreatePointOfSaleAppRequest request, CancellationToken token = default)
PointOfSaleAppRequest request, CancellationToken token = default)
{
return GetFromActionResult<PointOfSaleAppData>(
await GetController<GreenfieldAppsController>().CreatePointOfSaleApp(storeId, request));
@ -1096,7 +1096,7 @@ namespace BTCPayServer.Controllers.Greenfield
public override async Task<PointOfSaleAppData> UpdatePointOfSaleApp(
string appId,
CreatePointOfSaleAppRequest request, CancellationToken token = default)
PointOfSaleAppRequest request, CancellationToken token = default)
{
return GetFromActionResult<PointOfSaleAppData>(
await GetController<GreenfieldAppsController>().UpdatePointOfSaleApp(appId, request));
@ -1104,27 +1104,27 @@ namespace BTCPayServer.Controllers.Greenfield
public override async Task<CrowdfundAppData> CreateCrowdfundApp(
string storeId,
CreateCrowdfundAppRequest request, CancellationToken token = default)
CrowdfundAppRequest request, CancellationToken token = default)
{
return GetFromActionResult<CrowdfundAppData>(
await GetController<GreenfieldAppsController>().CreateCrowdfundApp(storeId, request));
}
public override async Task<AppDataBase> GetApp(string appId, CancellationToken token = default)
public override async Task<AppBaseData> GetApp(string appId, CancellationToken token = default)
{
return GetFromActionResult<AppDataBase>(
return GetFromActionResult<AppBaseData>(
await GetController<GreenfieldAppsController>().GetApp(appId));
}
public override async Task<AppDataBase[]> GetAllApps(string storeId, CancellationToken token = default)
public override async Task<AppBaseData[]> GetAllApps(string storeId, CancellationToken token = default)
{
return GetFromActionResult<AppDataBase[]>(
return GetFromActionResult<AppBaseData[]>(
await GetController<GreenfieldAppsController>().GetAllApps(storeId));
}
public override async Task<AppDataBase[]> GetAllApps(CancellationToken token = default)
public override async Task<AppBaseData[]> GetAllApps(CancellationToken token = default)
{
return GetFromActionResult<AppDataBase[]>(
return GetFromActionResult<AppBaseData[]>(
await GetController<GreenfieldAppsController>().GetAllApps());
}

View File

@ -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;

View File

@ -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
{

View File

@ -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; }

View File

@ -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"]
}
}
}
}
},