mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-04 09:58:13 +01:00
Add crowdfund app create endpoint (#4068)
* Add crowdfund app create endpoint * replace DateTimeJsonConverter with NBitcoin.JsonConverters.DateTimeToUnixTimeConverter * Use DateTimeOffset instead of DateTime * Use array instead of CSV * update "startDate" and "endDate" docs definition * update docs
This commit is contained in:
parent
3942463ac9
commit
52af129c8b
7 changed files with 576 additions and 6 deletions
|
@ -19,6 +19,17 @@ namespace BTCPayServer.Client
|
||||||
method: HttpMethod.Post), token);
|
method: HttpMethod.Post), token);
|
||||||
return await HandleResponse<PointOfSaleAppData>(response);
|
return await HandleResponse<PointOfSaleAppData>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual async Task<CrowdfundAppData> CreateCrowdfundApp(string storeId,
|
||||||
|
CreateCrowdfundAppRequest request, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
throw new ArgumentNullException(nameof(request));
|
||||||
|
var response = await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/apps/crowdfund", bodyPayload: request,
|
||||||
|
method: HttpMethod.Post), token);
|
||||||
|
return await HandleResponse<CrowdfundAppData>(response);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual async Task<PointOfSaleAppData> UpdatePointOfSaleApp(string appId,
|
public virtual async Task<PointOfSaleAppData> UpdatePointOfSaleApp(string appId,
|
||||||
CreatePointOfSaleAppRequest request, CancellationToken token = default)
|
CreatePointOfSaleAppRequest request, CancellationToken token = default)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
@ -40,4 +41,44 @@ namespace BTCPayServer.Client.Models
|
||||||
public string EmbeddedCSS { get; set; } = null;
|
public string EmbeddedCSS { get; set; } = null;
|
||||||
public CheckoutType? CheckoutType { get; set; } = null;
|
public CheckoutType? CheckoutType { 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 CustomCSSLink { get; set; } = null;
|
||||||
|
public string MainImageUrl { get; set; } = null;
|
||||||
|
public string EmbeddedCSS { 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 string[] Sounds { get; set; } = null;
|
||||||
|
public string[] AnimationColors { get; set; } = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,9 @@ namespace BTCPayServer.Client.Models
|
||||||
{
|
{
|
||||||
// We can add POS specific things here later
|
// We can add POS specific things here later
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CrowdfundAppData : AppDataBase
|
||||||
|
{
|
||||||
|
// We can add Crowdfund specific things here later
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,6 +288,117 @@ namespace BTCPayServer.Tests
|
||||||
await client.GetApp(retrievedApp.Id);
|
await client.GetApp(retrievedApp.Id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanCreateCrowdfundApp()
|
||||||
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
await user.RegisterDerivationSchemeAsync("BTC");
|
||||||
|
var client = await user.CreateClient();
|
||||||
|
|
||||||
|
// Test validation for creating the app
|
||||||
|
await AssertValidationError(new[] { "AppName" },
|
||||||
|
async () => await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() {}));
|
||||||
|
await AssertValidationError(new[] { "AppName" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "this is a really long app name this is a really long app name this is a really long app name",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "TargetCurrency" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "good name",
|
||||||
|
TargetCurrency = "fake currency"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "PerksTemplate" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "good name",
|
||||||
|
PerksTemplate = "lol invalid template"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "AppName", "TargetCurrency", "PerksTemplate" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
TargetCurrency = "fake currency",
|
||||||
|
PerksTemplate = "lol invalid template"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "AnimationColors" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "good name",
|
||||||
|
AnimationColors = new string[] {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "AnimationColors" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "good name",
|
||||||
|
AnimationColors = new string[] { " ", " " }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "Sounds" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "good name",
|
||||||
|
Sounds = new string[] { " " }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "Sounds" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "good name",
|
||||||
|
Sounds = new string[] { " ", " ", " " }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await AssertValidationError(new[] { "EndDate" },
|
||||||
|
async () => await client.CreateCrowdfundApp(
|
||||||
|
user.StoreId,
|
||||||
|
new CreateCrowdfundAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "good name",
|
||||||
|
StartDate = DateTime.Parse("1998-01-01"),
|
||||||
|
EndDate = DateTime.Parse("1997-12-31")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test creating a crowdfund app
|
||||||
|
var app = await client.CreateCrowdfundApp(user.StoreId, new CreateCrowdfundAppRequest() { AppName = "test app from API" });
|
||||||
|
Assert.Equal("test app from API", app.Name);
|
||||||
|
Assert.Equal(user.StoreId, app.StoreId);
|
||||||
|
Assert.Equal("Crowdfund", app.AppType);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
|
@ -38,6 +39,37 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
_currencies = currencies;
|
_currencies = currencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("~/api/v1/stores/{storeId}/apps/crowdfund")]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
public async Task<IActionResult> CreateCrowdfundApp(string storeId, CreateCrowdfundAppRequest 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;
|
||||||
|
|
||||||
|
var validationResult = ValidateCrowdfundAppRequest(request);
|
||||||
|
if (validationResult != null)
|
||||||
|
{
|
||||||
|
return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var appData = new AppData
|
||||||
|
{
|
||||||
|
StoreDataId = storeId,
|
||||||
|
Name = request.AppName,
|
||||||
|
AppType = AppType.Crowdfund.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
appData.SetSettings(ToCrowdfundSettings(request));
|
||||||
|
|
||||||
|
await _appService.UpdateOrCreateApp(appData);
|
||||||
|
|
||||||
|
return Ok(ToCrowdfundModel(appData));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
|
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request)
|
public async Task<IActionResult> CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request)
|
||||||
|
@ -66,7 +98,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
|
|
||||||
await _appService.UpdateOrCreateApp(appData);
|
await _appService.UpdateOrCreateApp(appData);
|
||||||
|
|
||||||
return Ok(ToModel(appData));
|
return Ok(ToPointOfSaleModel(appData));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("~/api/v1/apps/pos/{appId}")]
|
[HttpPut("~/api/v1/apps/pos/{appId}")]
|
||||||
|
@ -95,7 +127,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
|
|
||||||
await _appService.UpdateOrCreateApp(app);
|
await _appService.UpdateOrCreateApp(app);
|
||||||
|
|
||||||
return Ok(ToModel(app));
|
return Ok(ToPointOfSaleModel(app));
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequiresRefundEmail? BoolToRequiresRefundEmail(bool? requiresRefundEmail)
|
private RequiresRefundEmail? BoolToRequiresRefundEmail(bool? requiresRefundEmail)
|
||||||
|
@ -115,7 +147,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> GetApp(string appId)
|
public async Task<IActionResult> GetApp(string appId)
|
||||||
{
|
{
|
||||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
var app = await _appService.GetApp(appId, null);
|
||||||
if (app == null)
|
if (app == null)
|
||||||
{
|
{
|
||||||
return AppNotFound();
|
return AppNotFound();
|
||||||
|
@ -143,6 +175,43 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CrowdfundSettings ToCrowdfundSettings(CreateCrowdfundAppRequest request)
|
||||||
|
{
|
||||||
|
var parsedSounds = ValidateStringArray(request.Sounds);
|
||||||
|
var parsedColors = ValidateStringArray(request.AnimationColors);
|
||||||
|
|
||||||
|
return new CrowdfundSettings
|
||||||
|
{
|
||||||
|
Title = request.Title?.Trim(),
|
||||||
|
Enabled = request.Enabled ?? true,
|
||||||
|
EnforceTargetAmount = request.EnforceTargetAmount ?? false,
|
||||||
|
StartDate = request.StartDate?.UtcDateTime,
|
||||||
|
TargetCurrency = request.TargetCurrency?.Trim(),
|
||||||
|
Description = request.Description?.Trim(),
|
||||||
|
EndDate = request.EndDate?.UtcDateTime,
|
||||||
|
TargetAmount = request.TargetAmount,
|
||||||
|
CustomCSSLink = request.CustomCSSLink?.Trim(),
|
||||||
|
MainImageUrl = request.MainImageUrl?.Trim(),
|
||||||
|
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
|
||||||
|
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||||
|
Tagline = request.Tagline?.Trim(),
|
||||||
|
PerksTemplate = request.PerksTemplate != null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate?.Trim(), request.TargetCurrency)) : null,
|
||||||
|
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
|
||||||
|
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
|
||||||
|
DisqusShortname = request.DisqusShortname?.Trim(),
|
||||||
|
// If explicit parameter is not passed for enabling sounds/animations, turn them on if custom sounds/colors are passed
|
||||||
|
SoundsEnabled = request.SoundsEnabled ?? parsedSounds != null,
|
||||||
|
AnimationsEnabled = request.AnimationsEnabled ?? parsedColors != null,
|
||||||
|
ResetEveryAmount = request.ResetEveryAmount ?? 1,
|
||||||
|
ResetEvery = (Services.Apps.CrowdfundResetEvery)request.ResetEvery,
|
||||||
|
DisplayPerksValue = request.DisplayPerksValue ?? false,
|
||||||
|
DisplayPerksRanking = request.DisplayPerksRanking ?? false,
|
||||||
|
SortPerksByPopularity = request.SortPerksByPopularity ?? false,
|
||||||
|
Sounds = parsedSounds ?? new CrowdfundSettings().Sounds,
|
||||||
|
AnimationColors = parsedColors ?? new CrowdfundSettings().AnimationColors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request)
|
private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request)
|
||||||
{
|
{
|
||||||
return new PointOfSaleSettings()
|
return new PointOfSaleSettings()
|
||||||
|
@ -169,10 +238,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private PointOfSaleAppData ToModel(AppData appData)
|
private AppDataBase ToModel(AppData appData)
|
||||||
{
|
{
|
||||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
return new AppDataBase
|
||||||
|
{
|
||||||
|
Id = appData.Id,
|
||||||
|
AppType = appData.AppType,
|
||||||
|
Name = appData.Name,
|
||||||
|
StoreId = appData.StoreDataId,
|
||||||
|
Created = appData.Created,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private PointOfSaleAppData ToPointOfSaleModel(AppData appData)
|
||||||
|
{
|
||||||
return new PointOfSaleAppData
|
return new PointOfSaleAppData
|
||||||
{
|
{
|
||||||
Id = appData.Id,
|
Id = appData.Id,
|
||||||
|
@ -211,6 +290,84 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
return validationResult;
|
return validationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CrowdfundAppData ToCrowdfundModel(AppData appData)
|
||||||
|
{
|
||||||
|
return new CrowdfundAppData
|
||||||
|
{
|
||||||
|
Id = appData.Id,
|
||||||
|
AppType = appData.AppType,
|
||||||
|
Name = appData.Name,
|
||||||
|
StoreId = appData.StoreDataId,
|
||||||
|
Created = appData.Created
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[]? ValidateStringArray(string[]? arr)
|
||||||
|
{
|
||||||
|
if (arr == null || !arr.Any())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure it's not just an array of empty strings
|
||||||
|
if (arr.All(s => string.IsNullOrEmpty(s.Trim())))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr.Select(s => s.Trim()).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult? ValidateCrowdfundAppRequest(CreateCrowdfundAppRequest request)
|
||||||
|
{
|
||||||
|
var validationResult = ValidateCreateAppRequest(request);
|
||||||
|
if (request.TargetCurrency != null && _currencies.GetCurrencyData(request.TargetCurrency, false) == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(request.TargetCurrency), "Invalid currency");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, request.TargetCurrency));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(request.PerksTemplate), "Invalid template");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ResetEvery != Client.Models.CrowdfundResetEvery.Never && request.StartDate == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(request.StartDate), "A start date is needed when the goal resets every X amount of time");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.AnimationColors != null && ValidateStringArray(request.AnimationColors) == null)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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 IActionResult? ValidateCreateAppRequest(CreateAppRequest request)
|
||||||
{
|
{
|
||||||
if (request is null)
|
if (request is null)
|
||||||
|
|
|
@ -1163,6 +1163,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
await GetController<GreenfieldAppsController>().UpdatePointOfSaleApp(appId, request));
|
await GetController<GreenfieldAppsController>().UpdatePointOfSaleApp(appId, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<CrowdfundAppData> CreateCrowdfundApp(
|
||||||
|
string storeId,
|
||||||
|
CreateCrowdfundAppRequest 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<AppDataBase> GetApp(string appId, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetFromActionResult<AppDataBase>(
|
return GetFromActionResult<AppDataBase>(
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "Apps_CreatePointOfSaleApp",
|
"operationId": "Apps_CreatePointOfSaleApp",
|
||||||
"summary": "Create a new Point of Sale app",
|
"summary": "Create a new Point of Sale app",
|
||||||
"description": "Point of Sale apps allows accepting payments for items in a virtual store",
|
"description": "Point of Sale app allows accepting payments for items in a virtual store",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"x-name": "request",
|
"x-name": "request",
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -126,6 +126,69 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/stores/{storeId}/apps/crowdfund": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store ID",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post": {
|
||||||
|
"operationId": "Apps_CreateCrowdfundApp",
|
||||||
|
"summary": "Create a new Crowdfund app",
|
||||||
|
"requestBody": {
|
||||||
|
"x-name": "request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateCrowdfundAppRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-position": 1
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Created app details",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CrowdfundAppData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Unable to validate the request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Apps",
|
||||||
|
"Crowdfund"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.store.canmodifystoresettings"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/apps/{appId}": {
|
"/api/v1/apps/{appId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -217,6 +280,15 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"CrowdfundAppData": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/BasicAppData"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"BasicAppData": {
|
"BasicAppData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -368,6 +440,171 @@
|
||||||
"nullable": true
|
"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
|
||||||
|
},
|
||||||
|
"customCSSLink": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Link to a custom CSS stylesheet to be used in the app",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"mainImageUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL for image to be used as a cover image for the app",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"embeddedCSS": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom CSS to embed into 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": "Enables background animations on new contributions 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
|
||||||
|
},
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue