diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index db9b8880a..5e1d7c4b5 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -988,14 +988,14 @@ namespace BTCPayServer.Tests 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"); Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view"); - Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count); + Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")).Count); var drinks = s.Driver.FindElement(By.CssSelector("label[for='Category-Drinks']")); Assert.Equal("Drinks", drinks.Text); drinks.Click(); - Assert.Single(s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)"))); + Assert.Single(s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)"))); s.Driver.FindElement(By.CssSelector("label[for='Category-*']")).Click(); - Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count); + Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")).Count); s.Driver.Url = posBaseUrl + "/static"; Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view"); @@ -2189,47 +2189,52 @@ namespace BTCPayServer.Tests Assert.Contains("App successfully created", s.FindAlertMessage().Text); s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click(); s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR"); - s.Driver.FindElement(By.Id("ShowCustomAmount")).Click(); + s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear(); + s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21"); s.Driver.FindElement(By.Id("SaveSettings")).Click(); Assert.Contains("App updated", s.FindAlertMessage().Text); s.Driver.FindElement(By.Id("ViewApp")).Click(); var windows = s.Driver.WindowHandles; Assert.Equal(2, windows.Count); s.Driver.SwitchTo().Window(windows[1]); - s.Driver.WaitForElement(By.Id("js-cart-list")); - Assert.Empty(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr"))); - Assert.Equal("0,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text); - Assert.False(s.Driver.FindElement(By.Id("CartClear")).Displayed); + s.Driver.WaitForElement(By.Id("PosItems")); + Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr"))); // Select and clear - s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(1)")).Click(); - Assert.Single(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr"))); + s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click(); + Assert.Single(s.Driver.FindElements(By.CssSelector("#CartItems tr"))); s.Driver.FindElement(By.Id("CartClear")).Click(); - Assert.Empty(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr"))); Thread.Sleep(250); + Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr"))); // Select items - s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(2)")).Click(); + s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click(); Thread.Sleep(250); - s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(1)")).Click(); + s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click(); Thread.Sleep(250); - Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")).Count); - Assert.Equal("2,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text); + s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click(); + Thread.Sleep(250); + Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count); + Assert.Equal("3,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text); - // Custom amount - s.Driver.FindElement(By.Id("CartCustomAmount")).SendKeys("1.5"); - s.Driver.FindElement(By.Id("CartTotal")).Click(); - Assert.Equal("3,50 €", s.Driver.FindElement(By.Id("CartTotal")).Text); - s.Driver.FindElement(By.Id("js-cart-confirm")).Click(); + // Discount: 10% + s.Driver.ElementDoesNotExist(By.Id("CartDiscount")); + s.Driver.FindElement(By.Id("Discount")).SendKeys("10"); + Assert.Contains("10% = 0,30 €", s.Driver.FindElement(By.Id("CartDiscount")).Text); + Assert.Equal("2,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text); + + // Tip: 10% + s.Driver.ElementDoesNotExist(By.Id("CartTip")); + s.Driver.FindElement(By.Id("Tip-10")).Click(); + Assert.Contains("10% = 0,27 €", s.Driver.FindElement(By.Id("CartTip")).Text); + Assert.Equal("2,97 €", s.Driver.FindElement(By.Id("CartTotal")).Text); // Pay - Assert.Equal("3,50 €", s.Driver.FindElement(By.Id("CartSummaryTotal")).Text); - s.Driver.FindElement(By.Id("js-cart-pay")).Click(); - + s.Driver.FindElement(By.Id("CartSubmit")).Click(); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); s.Driver.FindElement(By.Id("DetailsToggle")).Click(); s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat")); - Assert.Contains("3,50 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); + Assert.Contains("2,97 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); } [Fact] diff --git a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs index 36b4f20b6..267771156 100644 --- a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs @@ -93,7 +93,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models .ToList(); if (groups.Count == 0) return; - groups.Insert(0, new KeyValuePair("All items", "*")); + groups.Insert(0, new KeyValuePair("All", "*")); AllCategories = new SelectList(groups, "Value", "Key", "*"); } diff --git a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml index 22b44797e..294f1a964 100644 --- a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml +++ b/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml @@ -53,7 +53,7 @@ } -
+
@if (!string.IsNullOrEmpty(Model.MainImageUrl)) { diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml index cbe7b28a3..92e2cd2d6 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/Cart.cshtml @@ -1,349 +1,244 @@ @using BTCPayServer.Plugins.PointOfSale.Models @using BTCPayServer.Services @using Newtonsoft.Json.Linq; -@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel - @inject DisplayFormatter DisplayFormatter +@inject BTCPayServer.Security.ContentSecurityPolicies Csp +@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel @{ Layout = "PointOfSale/Public/_Layout"; - var customTipPercentages = Model.CustomTipPercentages; - var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue); + Csp.UnsafeEval(); } @section PageHeadContent { - - + } @section PageFootContent { - - - - - - - - - + + + + } - - -
- -
-
- - - @if (!string.IsNullOrEmpty(Model.Description)) - { -
@Safe.Raw(Model.Description)
- } - @if (Model.AllCategories != null) - { -
- @foreach (var g in Model.AllCategories) - { - - - } -
- } -
-
-
- @for (var index = 0; index < Model.Items.Length; index++) + +
+ +
+ +
+ + @if (!string.IsNullOrEmpty(Model.Description)) { - var item = Model.Items[index]; - if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) - { - continue; - } - var image = item.Image; - var description = item.Description; - -
- @if (!string.IsNullOrWhiteSpace(image)) - { - @Safe.Raw(item.Title) - } -
-
@Safe.Raw(item.Title)
- @if (!string.IsNullOrWhiteSpace(description)) - { -

@Safe.Raw(description)

- } -
- -
+
@Safe.Raw(Model.Description)
} -
+
+ @for (var index = 0; index < Model.Items.Length; index++) + { + var item = Model.Items[index]; + if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) + { + continue; + } + var formatted = GetItemPriceFormatted(item); + var inStock = item.Inventory is null or > 0; + var buttonText = string.IsNullOrEmpty(item.BuyButtonText) + ? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText + : item.BuyButtonText; + buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted); + +
+
+ @if (!string.IsNullOrWhiteSpace(item.Image)) + { + @Safe.Raw(item.Title) + } +
+
@Safe.Raw(item.Title)
+
+ @if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup || item.Price == 0) + { + @Safe.Raw(char.ToUpper(formatted[0]) + formatted[1..]) + } + else + { + @Safe.Raw(formatted) + } + @if (item.Inventory.HasValue) + { + + @(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out") + + } +
+ @if (!string.IsNullOrWhiteSpace(item.Description)) + { +

@Safe.Raw(item.Description)

+ } +
+ @if (inStock) + { + +
+ } +
+
+ } +
+ +
- - - +
diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/Light.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/Light.cshtml index 0ab0d5dae..223ae5cb4 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/Light.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/Light.cshtml @@ -5,112 +5,15 @@ Csp.UnsafeEval(); } @section PageHeadContent { - + } @section PageFootContent { - + + } -
+
@if (Context.Request.Query.ContainsKey("simple")) diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/MinimalLight.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/MinimalLight.cshtml index fe9ae4908..16e2123dc 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/MinimalLight.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/MinimalLight.cshtml @@ -1,6 +1,6 @@
-
+
@Model.CurrencySymbol diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/Print.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/Print.cshtml index 2e0ea8aa2..e1128a4c0 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/Print.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/Print.cshtml @@ -19,15 +19,7 @@ } } @section PageHeadContent { - + } @if (supported is null) @@ -50,14 +42,14 @@ else Regular version
} -
+
@if (!string.IsNullOrEmpty(Model.Description)) {
@Safe.Raw(Model.Description)
} -
+
@if (supported is not null) { if (Model.ShowCustomAmount) @@ -75,37 +67,45 @@ else } } -
+
@for (var x = 0; x < Model.Items.Length; x++) { var item = Model.Items[x]; -
-
-

@Safe.Raw(item.Title)

- @if (!string.IsNullOrEmpty(item.Description)) - { -

@Safe.Raw(item.Description)

- } -
- @{ - var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol); - } - @switch (item.PriceType) + var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol); +
+
+
+
@Safe.Raw(item.Title)
+
+ + @switch (item.PriceType) + { + case ViewPointOfSaleViewModel.ItemPriceType.Topup: + Any amount + break; + case ViewPointOfSaleViewModel.ItemPriceType.Minimum: + @formatted minimum + break; + case ViewPointOfSaleViewModel.ItemPriceType.Fixed: + @formatted + break; + default: + throw new ArgumentOutOfRangeException(); + } + + @if (item.Inventory.HasValue) + { + + @(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out") + + } +
+ @if (!string.IsNullOrWhiteSpace(item.Description)) { - case ViewPointOfSaleViewModel.ItemPriceType.Topup: - Any amount - break; - case ViewPointOfSaleViewModel.ItemPriceType.Minimum: - @formatted minimum - break; - case ViewPointOfSaleViewModel.ItemPriceType.Fixed: - @formatted - break; - default: - throw new ArgumentOutOfRangeException(); +

@Safe.Raw(item.Description)

}
- @if (!item.Inventory.HasValue || item.Inventory.Value > 0) + @if (item.Inventory is null or > 0) { if (supported != null) { @@ -116,7 +116,7 @@ else ItemCode = item.Id }, Context.Request.Scheme, Context.Request.Host.ToString())); var lnUrl = LNURL.EncodeUri(lnurlEndpoint, "payRequest", supported.UseBech32Scheme); - + } @@ -126,7 +126,7 @@ else }
-
+
Powered by diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/Static.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/Static.cshtml index 52f9ccb41..6a6949a50 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/Static.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/Static.cshtml @@ -1,93 +1,107 @@ @using BTCPayServer.Plugins.PointOfSale.Models @using BTCPayServer.Services +@using Microsoft.AspNetCore.Mvc.TagHelpers @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel @inject DisplayFormatter DisplayFormatter @{ Layout = "PointOfSale/Public/_Layout"; - var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue); +} +@functions { + private string GetItemPriceFormatted(ViewPointOfSaleViewModel.Item item) + { + if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) return "any amount"; + if (item.Price == 0) return "free"; + var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol); + return item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum ? $"{formatted} minimum" : formatted; + } } - - -
- +
- @if (!string.IsNullOrEmpty(Model.Description)) - { -
@Safe.Raw(Model.Description)
- } -
-
+
+ + @if (!string.IsNullOrEmpty(Model.Description)) + { +
@Safe.Raw(Model.Description)
+ } +
@for (var x = 0; x < Model.Items.Length; x++) { var item = Model.Items[x]; - var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol); - var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText; + var formatted = GetItemPriceFormatted(item); + var inStock = item.Inventory is null or > 0; + var buttonText = string.IsNullOrEmpty(item.BuyButtonText) + ? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText + : item.BuyButtonText; buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted); -
- @if (!string.IsNullOrWhiteSpace(item.Image)) - { - @Safe.Raw(item.Title) - } - @{CardBody(item.Title, item.Description);} -
- -@functions { - private void PayFormInputContent(string buttonText,ViewPointOfSaleViewModel.ItemPriceType itemPriceType, decimal? minPriceValue = null, decimal? priceValue = null) - { - if (itemPriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed && priceValue == 0) - { -
- - -
- } - else - { -
- @Model.CurrencySymbol - - - -
- } - } - - private void CardBody(string title, string description) - { -
-
@Safe.Raw(title)
- @if (!string.IsNullOrWhiteSpace(description)) - { -

@Safe.Raw(description)

- } -
- } -} diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml index 0fd3d8435..605cc2795 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml @@ -1,22 +1,22 @@ @using Microsoft.AspNetCore.Mvc.TagHelpers @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel -
+
-
{{srvModel.currencyCode}}
+
{{currencyCode}}
{{ formatCurrency(total, false) }}
-
{{ calculation }}
+
{{ calculation }}
-
-
+
+
{{discountPercent || 0}}% discount
-
+
-