From bafec0d9741ce6bbe0d4235bb327c8abe8040682 Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Sat, 26 Feb 2022 21:19:02 -0800 Subject: [PATCH 1/7] Move PointOfSaleSettings class into own file --- .../UIAppsController.PointOfSale.cs | 74 ------------------ BTCPayServer/Controllers/UILNURLController.cs | 2 +- .../AppInventoryUpdaterHostedService.cs | 4 +- BTCPayServer/Hosting/MigrationStartupTask.cs | 2 +- BTCPayServer/Services/Apps/AppService.cs | 2 +- .../Services/Apps/PointOfSaleSettings.cs | 76 +++++++++++++++++++ 6 files changed, 81 insertions(+), 79 deletions(-) create mode 100644 BTCPayServer/Services/Apps/PointOfSaleSettings.cs diff --git a/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs b/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs index c0d3b9847..2bdf41f65 100644 --- a/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/UIAppsController.PointOfSale.cs @@ -15,80 +15,6 @@ namespace BTCPayServer.Controllers { public partial class UIAppsController { - public class PointOfSaleSettings - { - public PointOfSaleSettings() - { - Title = "Tea shop"; - Template = - "green tea:\n" + - " price: 1\n" + - " title: Green Tea\n" + - " description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" + - " image: ~/img/pos-sample/green-tea.jpg\n\n" + - "black tea:\n" + - " price: 1\n" + - " title: Black Tea\n" + - " description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" + - " image: ~/img/pos-sample/black-tea.jpg\n\n" + - "rooibos:\n" + - " price: 1.2\n" + - " title: Rooibos\n" + - " description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" + - " image: ~/img/pos-sample/rooibos.jpg\n\n" + - "pu erh:\n" + - " price: 2\n" + - " title: Pu Erh\n" + - " description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" + - " image: ~/img/pos-sample/pu-erh.jpg\n\n" + - "herbal tea:\n" + - " price: 1.8\n" + - " title: Herbal Tea\n" + - " description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" + - " image: ~/img/pos-sample/herbal-tea.jpg\n" + - " custom: true\n\n" + - "fruit tea:\n" + - " price: 1.5\n" + - " title: Fruit Tea\n" + - " description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" + - " image: ~/img/pos-sample/fruit-tea.jpg\n" + - " inventory: 5\n" + - " custom: true"; - DefaultView = PosViewType.Static; - ShowCustomAmount = true; - ShowDiscount = true; - EnableTips = true; - RequiresRefundEmail = RequiresRefundEmail.InheritFromStore; - } - public string Title { get; set; } - public string Currency { get; set; } - public string Template { get; set; } - public bool EnableShoppingCart { get; set; } - public PosViewType DefaultView { get; set; } - public bool ShowCustomAmount { get; set; } - public bool ShowDiscount { get; set; } - public bool EnableTips { get; set; } - public RequiresRefundEmail RequiresRefundEmail { 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 int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF; - - public string CustomCSSLink { get; set; } - - public string EmbeddedCSS { get; set; } - - public string Description { get; set; } - public string NotificationUrl { get; set; } - public string RedirectUrl { get; set; } - public bool? RedirectAutomatically { get; set; } - } - [HttpGet("{appId}/settings/pos")] public IActionResult UpdatePointOfSale(string appId) { diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index ddca7c4b5..6d1e22df7 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -115,7 +115,7 @@ namespace BTCPayServer items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency); break; case nameof(AppType.PointOfSale): - var posS = app.GetSettings(); + var posS = app.GetSettings(); currencyCode = posS.Currency; items = _appService.Parse(posS.Template, posS.Currency); break; diff --git a/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs b/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs index c767aff18..fe7b6116c 100644 --- a/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs +++ b/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs @@ -37,7 +37,7 @@ namespace BTCPayServer.HostedServices switch (Enum.Parse(data.AppType)) { case AppType.PointOfSale: - var possettings = data.GetSettings(); + var possettings = data.GetSettings(); return (Data: data, Settings: (object)possettings, Items: _appService.Parse(possettings.Template, possettings.Currency)); case AppType.Crowdfund: @@ -69,7 +69,7 @@ namespace BTCPayServer.HostedServices { case AppType.PointOfSale: - ((UIAppsController.PointOfSaleSettings)valueTuple.Settings).Template = + ((PointOfSaleSettings)valueTuple.Settings).Template = _appService.SerializeTemplate(valueTuple.Items); break; case AppType.Crowdfund: diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index 088a68c59..65d6a9759 100644 --- a/BTCPayServer/Hosting/MigrationStartupTask.cs +++ b/BTCPayServer/Hosting/MigrationStartupTask.cs @@ -350,7 +350,7 @@ WHERE cte.""Id""=p.""Id"" case nameof(AppType.PointOfSale): - var settings2 = app.GetSettings(); + var settings2 = app.GetSettings(); if (string.IsNullOrEmpty(settings2.Currency)) { settings2.Currency = app.StoreData.GetStoreBlob().DefaultCurrency; diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index ed5572720..6a27ea20b 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -347,7 +347,7 @@ namespace BTCPayServer.Services.Apps { AppType appTypeEnum = Enum.Parse(appType); AppData appData = await GetApp(appId, appTypeEnum, false); - var settings = appData.GetSettings(); + var settings = appData.GetSettings(); string style; switch (appTypeEnum) diff --git a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs new file mode 100644 index 000000000..f2b2d32ad --- /dev/null +++ b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs @@ -0,0 +1,76 @@ +namespace BTCPayServer.Services.Apps +{ + public class PointOfSaleSettings + { + public PointOfSaleSettings() + { + Title = "Tea shop"; + Template = + "green tea:\n" + + " price: 1\n" + + " title: Green Tea\n" + + " description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" + + " image: ~/img/pos-sample/green-tea.jpg\n\n" + + "black tea:\n" + + " price: 1\n" + + " title: Black Tea\n" + + " description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" + + " image: ~/img/pos-sample/black-tea.jpg\n\n" + + "rooibos:\n" + + " price: 1.2\n" + + " title: Rooibos\n" + + " description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" + + " image: ~/img/pos-sample/rooibos.jpg\n\n" + + "pu erh:\n" + + " price: 2\n" + + " title: Pu Erh\n" + + " description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" + + " image: ~/img/pos-sample/pu-erh.jpg\n\n" + + "herbal tea:\n" + + " price: 1.8\n" + + " title: Herbal Tea\n" + + " description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" + + " image: ~/img/pos-sample/herbal-tea.jpg\n" + + " custom: true\n\n" + + "fruit tea:\n" + + " price: 1.5\n" + + " title: Fruit Tea\n" + + " description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" + + " image: ~/img/pos-sample/fruit-tea.jpg\n" + + " inventory: 5\n" + + " custom: true"; + DefaultView = PosViewType.Static; + ShowCustomAmount = true; + ShowDiscount = true; + EnableTips = true; + RequiresRefundEmail = RequiresRefundEmail.InheritFromStore; + } + public string Title { get; set; } + public string Currency { get; set; } + public string Template { get; set; } + public bool EnableShoppingCart { get; set; } + public PosViewType DefaultView { get; set; } + public bool ShowCustomAmount { get; set; } + public bool ShowDiscount { get; set; } + public bool EnableTips { get; set; } + public RequiresRefundEmail RequiresRefundEmail { 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 int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF; + + public string CustomCSSLink { get; set; } + + public string EmbeddedCSS { get; set; } + + public string Description { get; set; } + public string NotificationUrl { get; set; } + public string RedirectUrl { get; set; } + public bool? RedirectAutomatically { get; set; } + } +} From aa3c0346c8e4b79deaf6078a8a138f539e7ab1d1 Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Sat, 26 Feb 2022 21:22:29 -0800 Subject: [PATCH 2/7] Add GreenfieldAppsController --- .../Models/CreateAppRequest.cs | 9 ++ .../GreenField/GreenfieldAppsController.cs | 103 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 BTCPayServer.Client/Models/CreateAppRequest.cs create mode 100644 BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs diff --git a/BTCPayServer.Client/Models/CreateAppRequest.cs b/BTCPayServer.Client/Models/CreateAppRequest.cs new file mode 100644 index 000000000..08857902c --- /dev/null +++ b/BTCPayServer.Client/Models/CreateAppRequest.cs @@ -0,0 +1,9 @@ +namespace BTCPayServer.Client.Models +{ + public class CreateAppRequest + { + public string StoreId { get; set; } + public string AppName { get; set; } + public string AppType { get; set; } + } +} diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs new file mode 100644 index 000000000..ed047856a --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -0,0 +1,103 @@ +#nullable enable +using System; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Services.Apps; +using BTCPayServer.Services.Stores; +using BTCPayServer.Abstractions.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers.Greenfield +{ + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [EnableCors(CorsPolicies.All)] + public class GreenfieldAppsController : ControllerBase + { + private readonly AppService _appService; + private readonly StoreRepository _storeRepository; + + public GreenfieldAppsController( + AppService appService, + StoreRepository storeRepository, + UserManager userManager, + BTCPayNetworkProvider btcPayNetworkProvider + ) + { + _appService = appService; + _storeRepository = storeRepository; + } + + [HttpPost("~/api/v1/apps")] + [Authorize(Policy = Policies.CanModifyStoreSettingsUnscoped, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public async Task CreateApp(CreateAppRequest request) + { + var validationResult = await Validate(request); + if (validationResult != null) + { + return validationResult; + } + + var appData = new AppData + { + StoreDataId = request.StoreId, + Name = request.AppName, + AppType = request.AppType + }; + + var defaultCurrency = (await _storeRepository.FindStore(request.StoreId)).GetStoreBlob().DefaultCurrency; + Enum.TryParse(request.AppType, out AppType appType); + switch (appType) + { + case AppType.Crowdfund: + var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency }; + appData.SetSettings(emptyCrowdfund); + break; + case AppType.PointOfSale: + var empty = new PointOfSaleSettings { Currency = defaultCurrency }; + appData.SetSettings(empty); + break; + } + + await _appService.UpdateOrCreateApp(appData); + + return Ok(appData); + } + + async private Task Validate(CreateAppRequest request) + { + if (request is null) + { + return BadRequest(); + } + + if (!Enum.TryParse(request.AppType, out AppType appType)) + { + ModelState.AddModelError(nameof(request.AppType), "Invalid app type"); + } + + if (string.IsNullOrEmpty(request.AppName)) + { + ModelState.AddModelError(nameof(request.AppName), "App name is missing"); + } + else if (request.AppName.Length < 1 || request.AppName.Length > 50) + { + ModelState.AddModelError(nameof(request.AppName), "Name can only be between 1 and 50 characters"); + } + + var store = await _storeRepository.FindStore(request.StoreId); + if (store == null) + { + ModelState.AddModelError(nameof(request.StoreId), "Store with provided ID not found"); + } + + return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null; + } + } +} From fca066fe524a987f856962e5dc669c3ea8470923 Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Sun, 1 May 2022 22:28:27 -0700 Subject: [PATCH 3/7] Add endpoint for creating POS app --- .../BTCPayServerClient.Apps.cs | 23 ++ .../Models/CreateAppRequest.cs | 22 +- .../Models/PointOfSaleAppData.cs | 20 ++ BTCPayServer.Tests/GreenfieldAPITests.cs | 16 ++ .../GreenField/GreenfieldAppsController.cs | 77 +++--- .../swagger/v1/swagger.template.apps.json | 221 ++++++++++++++++++ 6 files changed, 347 insertions(+), 32 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServerClient.Apps.cs create mode 100644 BTCPayServer.Client/Models/PointOfSaleAppData.cs create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json diff --git a/BTCPayServer.Client/BTCPayServerClient.Apps.cs b/BTCPayServer.Client/BTCPayServerClient.Apps.cs new file mode 100644 index 000000000..4f537258c --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Apps.cs @@ -0,0 +1,23 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + + public virtual async Task CreatePointOfSaleApp(string storeId, + CreatePointOfSaleAppRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/apps/pos", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/Models/CreateAppRequest.cs b/BTCPayServer.Client/Models/CreateAppRequest.cs index 08857902c..28b912cf9 100644 --- a/BTCPayServer.Client/Models/CreateAppRequest.cs +++ b/BTCPayServer.Client/Models/CreateAppRequest.cs @@ -2,8 +2,28 @@ namespace BTCPayServer.Client.Models { public class CreateAppRequest { - public string StoreId { get; set; } 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; + public string DefaultView { get; set; } = null; + public bool ShowCustomAmount { get; set; } = true; + public bool ShowDiscount { get; set; } = true; + public bool EnableTips { get; set; } = true; + public string CustomAmountPayButtonText { get; set; } = null; + public string FixedAmountPayButtonText { get; set; } = null; + public string TipText { get; set; } = null; + public string CustomCSSLink { get; set; } = null; + public string NotificationUrl { get; set; } = null; + public string RedirectUrl { get; set; } = null; + public bool? RedirectAutomatically { get; set; } = null; + public bool? RequiresRefundEmail { get; set; } = null; + public string EmbeddedCSS { get; set; } = null; + } } diff --git a/BTCPayServer.Client/Models/PointOfSaleAppData.cs b/BTCPayServer.Client/Models/PointOfSaleAppData.cs new file mode 100644 index 000000000..355672744 --- /dev/null +++ b/BTCPayServer.Client/Models/PointOfSaleAppData.cs @@ -0,0 +1,20 @@ +using System; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class AppDataBase + { + public string Id { get; set; } + public string AppType { get; set; } + public string Name { get; set; } + public string StoreId { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset Created { get; set; } + } + + public class PointOfSaleAppData : AppDataBase + { + // We can add POS specific things here later + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 6841ce3fa..7cabfeecb 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -174,6 +174,22 @@ namespace BTCPayServer.Tests await AssertAPIError("apikey-not-found", () => unrestricted.RevokeAPIKey(apiKey.ApiKey)); } + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanCreatePointOfSaleAppViaAPI() + { + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.RegisterDerivationSchemeAsync("BTC"); + var client = await user.CreateClient(); + var app = await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() { AppName = "test app from API" }); + + Assert.Equal("test app from API", app.Name); + Assert.Equal(user.StoreId, app.StoreId); + Assert.Equal("PointOfSale", app.AppType); + } + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanDeleteUsersViaApi() diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index ed047856a..dbf172ea8 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -34,54 +34,75 @@ namespace BTCPayServer.Controllers.Greenfield _storeRepository = storeRepository; } - [HttpPost("~/api/v1/apps")] - [Authorize(Policy = Policies.CanModifyStoreSettingsUnscoped, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public async Task CreateApp(CreateAppRequest request) + [HttpPost("~/api/v1/stores/{storeId}/apps/pos")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public async Task CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request) { - var validationResult = await Validate(request); + var validationResult = Validate(request); if (validationResult != null) { return validationResult; } + var defaultCurrency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency; var appData = new AppData { - StoreDataId = request.StoreId, + StoreDataId = storeId, Name = request.AppName, - AppType = request.AppType + AppType = AppType.PointOfSale.ToString() }; - var defaultCurrency = (await _storeRepository.FindStore(request.StoreId)).GetStoreBlob().DefaultCurrency; - Enum.TryParse(request.AppType, out AppType appType); - switch (appType) + Enum.TryParse(request.DefaultView, out PosViewType defaultView); + + appData.SetSettings(new PointOfSaleSettings { - case AppType.Crowdfund: - var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency }; - appData.SetSettings(emptyCrowdfund); - break; - case AppType.PointOfSale: - var empty = new PointOfSaleSettings { Currency = defaultCurrency }; - appData.SetSettings(empty); - break; - } + Title = request.Title, + DefaultView = defaultView, + ShowCustomAmount = request.ShowCustomAmount, + ShowDiscount = request.ShowDiscount, + EnableTips = request.EnableTips, + Currency = request.Currency ?? defaultCurrency, + Template = request.Template, + ButtonText = request.FixedAmountPayButtonText, + CustomButtonText = request.CustomAmountPayButtonText, + CustomTipText = request.TipText, + CustomCSSLink = request.CustomCSSLink, + NotificationUrl = request.NotificationUrl, + RedirectUrl = request.RedirectUrl, + Description = request.Description, + EmbeddedCSS = request.EmbeddedCSS, + RedirectAutomatically = request.RedirectAutomatically, + RequiresRefundEmail = request.RequiresRefundEmail == true ? + RequiresRefundEmail.On : + request.RequiresRefundEmail == false ? + RequiresRefundEmail.Off : + RequiresRefundEmail.InheritFromStore, + }); await _appService.UpdateOrCreateApp(appData); - return Ok(appData); + return Ok(ToModel(appData)); } - async private Task Validate(CreateAppRequest request) + private PointOfSaleAppData ToModel(AppData appData) + { + return new PointOfSaleAppData + { + Id = appData.Id, + AppType = appData.AppType, + Name = appData.Name, + StoreId = appData.StoreDataId, + Created = appData.Created + }; + } + + private IActionResult? Validate(CreateAppRequest request) { if (request is null) { return BadRequest(); } - if (!Enum.TryParse(request.AppType, out AppType appType)) - { - ModelState.AddModelError(nameof(request.AppType), "Invalid app type"); - } - if (string.IsNullOrEmpty(request.AppName)) { ModelState.AddModelError(nameof(request.AppName), "App name is missing"); @@ -91,12 +112,6 @@ namespace BTCPayServer.Controllers.Greenfield ModelState.AddModelError(nameof(request.AppName), "Name can only be between 1 and 50 characters"); } - var store = await _storeRepository.FindStore(request.StoreId); - if (store == null) - { - ModelState.AddModelError(nameof(request.StoreId), "Store with provided ID not found"); - } - return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null; } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json new file mode 100644 index 000000000..73f82048f --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json @@ -0,0 +1,221 @@ +{ + "paths": { + "/api/v1/stores/{storeId}/apps/pos": { + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID", + "schema": { + "type": "string" + } + } + ], + "post": { + "operationId": "Apps_CreatePointOfSaleApp", + "summary": "Create a new Point of Sale app", + "description": "Point of Sale apps allows accepting payments for items in a virtual store", + "requestBody": { + "content": { + "application/json": { + "schema": { + "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": true, + "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 + }, + "customCSSLink": { + "type": "string", + "description": "Link to a custom CSS stylesheet to be used in the app", + "nullable": true + }, + "embeddedCSS": { + "type": "string", + "description": "Custom CSS to embed into 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", + "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 + }, + "requiresRefundEmail": { + "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 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Created app details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PointOfSaleAppData" + } + } + } + }, + "422": { + "description": "Unable to validate the request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + } + }, + "tags": [ + "POS", + "Point of Sale" + ], + "security": [ + { + "API_Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + } + }, + "components": { + "schemas": { + "PointOfSaleAppData": { + "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 (will always \"PointOfSale\" in this case" + } + } + } + } + }, + "tags": [ + { + "name": "App" + }, + { + "name": "POS" + }, + { + "name": "Point of Sale" + } + ] +} From c917aec401bf858b956f97d76b637a717e7b40d9 Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Tue, 31 May 2022 22:25:04 -0700 Subject: [PATCH 4/7] Add PosViewType enum --- BTCPayServer.Client/Models/CreateAppRequest.cs | 14 +++++++++++++- .../GreenField/GreenfieldAppsController.cs | 4 +--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/BTCPayServer.Client/Models/CreateAppRequest.cs b/BTCPayServer.Client/Models/CreateAppRequest.cs index 28b912cf9..2534438bb 100644 --- a/BTCPayServer.Client/Models/CreateAppRequest.cs +++ b/BTCPayServer.Client/Models/CreateAppRequest.cs @@ -1,5 +1,16 @@ +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; } @@ -12,7 +23,8 @@ namespace BTCPayServer.Client.Models public string Title { get; set; } = null; public string Description { get; set; } = null; public string Template { get; set; } = null; - public string DefaultView { get; set; } = null; + [JsonConverter(typeof(StringEnumConverter))] + public PosViewType DefaultView { get; set; } public bool ShowCustomAmount { get; set; } = true; public bool ShowDiscount { get; set; } = true; public bool EnableTips { get; set; } = true; diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index dbf172ea8..3ae50a620 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -52,12 +52,10 @@ namespace BTCPayServer.Controllers.Greenfield AppType = AppType.PointOfSale.ToString() }; - Enum.TryParse(request.DefaultView, out PosViewType defaultView); - appData.SetSettings(new PointOfSaleSettings { Title = request.Title, - DefaultView = defaultView, + DefaultView = (Services.Apps.PosViewType)request.DefaultView, ShowCustomAmount = request.ShowCustomAmount, ShowDiscount = request.ShowDiscount, EnableTips = request.EnableTips, From d697c2ac9e6bd9b15cf86c3bf8d493f0800b3c8d Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Tue, 31 May 2022 22:27:03 -0700 Subject: [PATCH 5/7] Add fallback values for buttons --- .../Controllers/GreenField/GreenfieldAppsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index 3ae50a620..1d432679e 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -61,9 +61,9 @@ namespace BTCPayServer.Controllers.Greenfield EnableTips = request.EnableTips, Currency = request.Currency ?? defaultCurrency, Template = request.Template, - ButtonText = request.FixedAmountPayButtonText, - CustomButtonText = request.CustomAmountPayButtonText, - CustomTipText = request.TipText, + ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF, + CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF, + CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF, CustomCSSLink = request.CustomCSSLink, NotificationUrl = request.NotificationUrl, RedirectUrl = request.RedirectUrl, From 6d83a00728ea3bb5937f62c939c04dbbc52322b5 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 Jun 2022 13:07:02 +0900 Subject: [PATCH 6/7] Rename tags of PoS API in swagger --- .../wwwroot/swagger/v1/swagger.template.apps.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json index 73f82048f..f6f4c0996 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json @@ -159,8 +159,7 @@ } }, "tags": [ - "POS", - "Point of Sale" + "Apps" ], "security": [ { @@ -209,13 +208,7 @@ }, "tags": [ { - "name": "App" - }, - { - "name": "POS" - }, - { - "name": "Point of Sale" + "name": "Apps" } ] } From 02e5e1bc1ea46d1c0deb07b906c62a985d9ee12e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 Jun 2022 13:26:14 +0900 Subject: [PATCH 7/7] Fix CheckNoDeadLink which can enter in infinite loop --- BTCPayServer.Tests/ThirdPartyTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index 8086de2ba..8baeb60b5 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -203,7 +203,8 @@ namespace BTCPayServer.Tests "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0"); - var response = await httpClient.SendAsync(request); + using var cts = new CancellationTokenSource(5_000); + var response = await httpClient.SendAsync(request, cts.Token); Assert.Equal(HttpStatusCode.OK, response.StatusCode); if (uri.Fragment.Length != 0) { @@ -223,11 +224,11 @@ namespace BTCPayServer.Tests } catch (Exception) when (retryLeft > 0) { + retryLeft--; goto retry; } catch (Exception ex) { - retryLeft--; var details = ex is EqualException ? (ex as EqualException).Actual : ex.Message; TestLogs.LogInformation($"FAILED: {url} ({file}) {details}");