diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index d571c69c4..2f9dab501 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -378,11 +378,20 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale" + Keys.Enter); s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter); s.Driver.FindElement(By.Id("Create")).Click(); - s.Driver.FindElement(By.Id("EnableShoppingCart")).Click(); + s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart" + Keys.Enter); s.Driver.FindElement(By.Id("SaveSettings")).ForceClick(); s.Driver.FindElement(By.Id("ViewApp")).ForceClick(); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); + + var posBaseUrl = s.Driver.Url.Replace("/Cart", ""); Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS"); + Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view"); + + s.Driver.Url = posBaseUrl + "/static"; + Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view"); + + s.Driver.Url = posBaseUrl + "/cart"; + Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view"); + s.Driver.Quit(); } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 93049c656..13b67c25e 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2314,7 +2314,7 @@ donation: var publicApps = user.GetController(); var vmview = Assert.IsType(Assert - .IsType(publicApps.ViewPointOfSale(appId).Result).Model); + .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); Assert.Equal("hello", vmview.Title); Assert.Equal(3, vmview.Items.Length); Assert.Equal("good apple", vmview.Items[0].Title); @@ -2326,7 +2326,7 @@ donation: Assert.Equal("Wanna tip?", vmview.CustomTipText); Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages)); Assert.IsType(publicApps - .ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result); // var invoices = user.BitPay.GetInvoices(); @@ -2337,7 +2337,7 @@ donation: Assert.IsType(publicApps - .ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result); invoices = user.BitPay.GetInvoices(); var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple")); @@ -2347,7 +2347,7 @@ donation: // testing custom amount var action = Assert.IsType(publicApps - .ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result); Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName); invoices = user.BitPay.GetInvoices(); var donationInvoice = invoices.Single(i => i.Price == 6.6m); @@ -2388,7 +2388,7 @@ donation: Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); publicApps = user.GetController(); vmview = Assert.IsType(Assert - .IsType(publicApps.ViewPointOfSale(appId).Result).Model); + .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); Assert.Equal(test.Code, vmview.CurrencyCode); Assert.Equal(test.ExpectedSymbol, vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well); @@ -2419,17 +2419,17 @@ noninventoryitem: //inventoryitem has 1 item available Assert.IsType(publicApps - .ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); //we already bought all available stock so this should fail await Task.Delay(100); Assert.IsType(publicApps - .ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); //inventoryitem has unlimited items available Assert.IsType(publicApps - .ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); Assert.IsType(publicApps - .ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); //verify invoices where created invoices = user.BitPay.GetInvoices(); @@ -2469,9 +2469,9 @@ normal: price: 1.0"; Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); Assert.IsType(publicApps - .ViewPointOfSale(appId, 1, null, null, null, null, "btconly").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "btconly").Result); Assert.IsType(publicApps - .ViewPointOfSale(appId, 1, null, null, null, null, "normal").Result); + .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "normal").Result); invoices = user.BitPay.GetInvoices(); var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal"); var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly"); diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index 792920111..32b66ce0c 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -4,12 +4,9 @@ using System.Text; using System.Text.Encodings.Web; using System.Text.RegularExpressions; using System.Threading.Tasks; -using BTCPayServer.Data; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Services.Apps; -using BTCPayServer.Services.Mails; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; namespace BTCPayServer.Controllers { @@ -55,7 +52,7 @@ namespace BTCPayServer.Controllers " image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" + " inventory: 5\n" + " custom: true"; - EnableShoppingCart = false; + DefaultView = PosViewType.Static; ShowCustomAmount = true; ShowDiscount = true; EnableTips = true; @@ -64,6 +61,7 @@ namespace BTCPayServer.Controllers 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; } @@ -95,13 +93,15 @@ namespace BTCPayServer.Controllers if (app == null) return NotFound(); var settings = app.GetSettings(); + settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView; + settings.EnableShoppingCart = false; var vm = new UpdatePointOfSaleViewModel() { Id = appId, StoreId = app.StoreDataId, Title = settings.Title, - EnableShoppingCart = settings.EnableShoppingCart, + DefaultView = settings.DefaultView, ShowCustomAmount = settings.ShowCustomAmount, ShowDiscount = settings.ShowDiscount, EnableTips = settings.EnableTips, @@ -179,7 +179,7 @@ namespace BTCPayServer.Controllers app.SetSettings(new PointOfSaleSettings() { Title = vm.Title, - EnableShoppingCart = vm.EnableShoppingCart, + DefaultView = vm.DefaultView, ShowCustomAmount = vm.ShowCustomAmount, ShowDiscount = vm.ShowDiscount, EnableTips = vm.EnableTips, @@ -194,7 +194,6 @@ namespace BTCPayServer.Controllers Description = vm.Description, EmbeddedCSS = vm.EmbeddedCSS, RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically) - }); await _AppService.UpdateOrCreateApp(app); TempData[WellKnownTempData.SuccessMessage] = "App updated"; diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 715aa9bac..72f3fcefe 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -41,23 +41,23 @@ namespace BTCPayServer.Controllers private readonly UserManager _UserManager; [HttpGet] - [Route("/apps/{appId}/pos")] + [Route("/apps/{appId}/pos/{viewType?}")] [XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)] - public async Task ViewPointOfSale(string appId) + public async Task ViewPointOfSale(string appId, PosViewType? viewType = null) { var app = await _AppService.GetApp(appId, AppType.PointOfSale); if (app == null) return NotFound(); var settings = app.GetSettings(); - var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD"); double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits)); + viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView; - return View(new ViewPointOfSaleViewModel() + return View("PointOfSale/" + viewType, new ViewPointOfSaleViewModel() { Title = settings.Title, Step = step.ToString(CultureInfo.InvariantCulture), - EnableShoppingCart = settings.EnableShoppingCart, + ViewType = (PosViewType)viewType, ShowCustomAmount = settings.ShowCustomAmount, ShowDiscount = settings.ShowDiscount, EnableTips = settings.EnableTips, @@ -85,11 +85,12 @@ namespace BTCPayServer.Controllers } [HttpPost] - [Route("/apps/{appId}/pos")] + [Route("/apps/{appId}/pos/{viewType?}")] [XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)] [IgnoreAntiforgeryToken] [EnableCors(CorsPolicies.All)] public async Task ViewPointOfSale(string appId, + PosViewType viewType, [ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, string email, string orderId, @@ -106,9 +107,10 @@ namespace BTCPayServer.Controllers if (app == null) return NotFound(); var settings = app.GetSettings(); - if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart) + settings.DefaultView = settings.EnableShoppingCart? PosViewType.Cart : settings.DefaultView; + if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart) { - return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); + return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType }); } string title = null; var price = 0.0m; @@ -141,14 +143,14 @@ namespace BTCPayServer.Controllers } else { - if (!settings.ShowCustomAmount && !settings.EnableShoppingCart) + if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart) return NotFound(); price = amount; title = settings.Title; //if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items - if (!string.IsNullOrEmpty(posData) && - settings.EnableShoppingCart && + if (!string.IsNullOrEmpty(posData) && + settings.DefaultView == PosViewType.Cart && AppService.TryParsePosCartItems(posData, out var cartItems)) { diff --git a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs index e71910d45..3fd7f9c1a 100644 --- a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using BTCPayServer.Services.Apps; using BTCPayServer.Validation; using Microsoft.AspNetCore.Mvc.Rendering; @@ -16,8 +17,8 @@ namespace BTCPayServer.Models.AppViewModels public string Currency { get; set; } public string Template { get; set; } - [Display(Name = "Enable shopping cart")] - public bool EnableShoppingCart { get; set; } + [Display(Name = "Default view")] + public PosViewType DefaultView { get; set; } [Display(Name = "User can input custom amount")] public bool ShowCustomAmount { get; set; } [Display(Name = "User can input discount in %")] diff --git a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs index 36666f180..054309184 100644 --- a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using BTCPayServer.Services.Apps; namespace BTCPayServer.Models.AppViewModels { @@ -36,7 +33,7 @@ namespace BTCPayServer.Models.AppViewModels public CurrencyInfoData CurrencyInfo { get; set; } - public bool EnableShoppingCart { get; set; } + public PosViewType ViewType { get; set; } public bool ShowCustomAmount { get; set; } public bool ShowDiscount { get; set; } public bool EnableTips { get; set; } diff --git a/BTCPayServer/Services/Apps/AppType.cs b/BTCPayServer/Services/Apps/AppType.cs index 15fe5a3b3..328ee72b6 100644 --- a/BTCPayServer/Services/Apps/AppType.cs +++ b/BTCPayServer/Services/Apps/AppType.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace BTCPayServer.Services.Apps { public enum AppType @@ -10,4 +5,10 @@ namespace BTCPayServer.Services.Apps PointOfSale, Crowdfund } + + public enum PosViewType + { + Static, + Cart + } } diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index 2f8b13d5f..ca8bf9e01 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -1,4 +1,5 @@ -@addTagHelper *, BundlerMinifier.TagHelpers +@using BTCPayServer.Services.Apps +@addTagHelper *, BundlerMinifier.TagHelpers @model UpdatePointOfSaleViewModel @{ ViewData["Title"] = "Update Point of Sale"; @@ -35,10 +36,10 @@
-
- - - +
+ * + +
diff --git a/BTCPayServer/Views/AppsPublic/PointOfSale/Cart.cshtml b/BTCPayServer/Views/AppsPublic/PointOfSale/Cart.cshtml new file mode 100644 index 000000000..59072bfd2 --- /dev/null +++ b/BTCPayServer/Views/AppsPublic/PointOfSale/Cart.cshtml @@ -0,0 +1,314 @@ +@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel +@{ + Layout = "_LayoutPos"; + int[] customTipPercentages = Model.CustomTipPercentages; + var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue); +} + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+
+ + + + +
+
+
+ @if (!string.IsNullOrEmpty(Model.Description)) + { +
+
@Safe.Raw(Model.Description)
+
+ } +
+
+
+ + @for (var index = 0; index < Model.Items.Length; index++) + { + var item = Model.Items[index]; + var image = item.Image; + var description = item.Description; + +
+ @if (!String.IsNullOrWhiteSpace(image)) + { + @:Card image cap + } +
+
@item.Title
+ @if (!String.IsNullOrWhiteSpace(description)) + { +

@description

+ } +
+ +
+ } +
+
+
+ + + +
diff --git a/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml b/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml new file mode 100644 index 000000000..6a4c9557c --- /dev/null +++ b/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml @@ -0,0 +1,113 @@ +@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel +@{ + Layout = "_LayoutPos"; + var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue); +} + +
+
+

@Model.Title

+ @if (!string.IsNullOrEmpty(Model.Description)) + { +
+
@Safe.Raw(Model.Description)
+
+ } +
+ @for (int x = 0; x < Model.Items.Length; x++) + { + var item = Model.Items[x]; + +
+ @if (!String.IsNullOrWhiteSpace(item.Image)) + { + Card image cap + } +
+
@item.Title
+ @if (!String.IsNullOrWhiteSpace(item.Description)) + { +

@System.Net.WebUtility.HtmlDecode(item.Description)

+ } + +
+ +
+ } + @if (Model.ShowCustomAmount) + { +
+
+
Custom Amount
+

Create invoice to pay custom amount

+ +
+ +
+ } +
+
+
diff --git a/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml b/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml deleted file mode 100644 index 11ee61421..000000000 --- a/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml +++ /dev/null @@ -1,509 +0,0 @@ -@addTagHelper *, BundlerMinifier.TagHelpers -@inject BTCPayServer.HostedServices.CssThemeManager themeManager - -@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel -@{ - ViewData["Title"] = Model.Title; - Layout = null; - int[] CustomTipPercentages = Model.CustomTipPercentages; - var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue); -} - - - - - @Model.Title - - - - - - - - - - - @if (Model.CustomCSSLink != null) - { - - } - - - @if (Model.EnableShoppingCart) - { - - - - } - - @if (!string.IsNullOrEmpty(Model.EmbeddedCSS)) - { - @Safe.Raw($""); - } - - - - - - - - - - - - - - - - @if (this.TempData.HasStatusMessage()) - { - - } - - @if (Model.EnableShoppingCart) - { - - -
- -
-
-
- -
-
- - - - -
-
-
- @if (!string.IsNullOrEmpty(Model.Description)) - { -
-
@Safe.Raw(Model.Description)
-
- } -
-
-
- - @for (var index = 0; index < Model.Items.Length; index++) - { - var item = Model.Items[index]; - var image = item.Image; - var description = item.Description; - -
- @if (!String.IsNullOrWhiteSpace(image)) - { - @:Card image cap - } -
-
@item.Title
- @if (!String.IsNullOrWhiteSpace(description)) - { -

@description

- } -
- -
- } -
-
-
- - - -
- } - else - { -
-
-

@Model.Title

- @if (!string.IsNullOrEmpty(Model.Description)) - { -
-
@Safe.Raw(Model.Description)
-
- } -
- @for (int x = 0; x < Model.Items.Length; x++) - { - var item = Model.Items[x]; - -
- @if (!String.IsNullOrWhiteSpace(item.Image)) - { - Card image cap - } -
-
@item.Title
- @if (!String.IsNullOrWhiteSpace(item.Description)) - { -

@System.Net.WebUtility.HtmlDecode(item.Description)

- } - -
- -
- } - @if (Model.ShowCustomAmount) - { -
-
-
Custom Amount
-

Create invoice to pay custom amount

- -
- -
- } -
-
-
- } - - diff --git a/BTCPayServer/Views/Shared/_LayoutPos.cshtml b/BTCPayServer/Views/Shared/_LayoutPos.cshtml new file mode 100644 index 000000000..8443b68c5 --- /dev/null +++ b/BTCPayServer/Views/Shared/_LayoutPos.cshtml @@ -0,0 +1,90 @@ +@addTagHelper *, BundlerMinifier.TagHelpers +@inject BTCPayServer.HostedServices.CssThemeManager themeManager + +@using BTCPayServer.Services.Apps +@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel +@{ + ViewData["Title"] = Model.Title; + Layout = null; +} + + + + + @Model.Title + + + + + + + + + + + @if (Model.CustomCSSLink != null) + { + + } + + + @if (Model.ViewType == PosViewType.Cart) + { + + + + } + + @if (!string.IsNullOrEmpty(Model.EmbeddedCSS)) + { + @Safe.Raw($""); + } + + + + + @if (this.TempData.HasStatusMessage()) + { + + } + + @RenderBody() + +