diff --git a/BTCPayServer.Abstractions/Form/Form.cs b/BTCPayServer.Abstractions/Form/Form.cs index 2ee6b79fa..c882fd9bb 100644 --- a/BTCPayServer.Abstractions/Form/Form.cs +++ b/BTCPayServer.Abstractions/Form/Form.cs @@ -47,10 +47,10 @@ public class Form public IEnumerable<(string FullName, List Path, Field Field)> GetAllFields() { - HashSet nameReturned = new HashSet(); + HashSet nameReturned = new(); foreach (var f in GetAllFieldsCore(new List(), Fields)) { - var fullName = String.Join('_', f.Path); + var fullName = string.Join('_', f.Path); if (!nameReturned.Add(fullName)) continue; yield return (fullName, f.Path, f.Field); @@ -60,10 +60,10 @@ public class Form public bool ValidateFieldNames(out List errors) { errors = new List(); - HashSet nameReturned = new HashSet(); + HashSet nameReturned = new(); foreach (var f in GetAllFieldsCore(new List(), Fields)) { - var fullName = String.Join('_', f.Path); + var fullName = string.Join('_', f.Path); if (!nameReturned.Add(fullName)) { errors.Add($"Form contains duplicate field names '{fullName}'"); @@ -77,7 +77,7 @@ public class Form { foreach (var field in fields) { - List thisPath = new List(path.Count + 1); + List thisPath = new(path.Count + 1); thisPath.AddRange(path); if (!string.IsNullOrEmpty(field.Name)) { diff --git a/BTCPayServer/Filters/DomainMappingConstraintAttribute.cs b/BTCPayServer/Filters/DomainMappingConstraintAttribute.cs index ff59fc96f..0f7dca3f5 100644 --- a/BTCPayServer/Filters/DomainMappingConstraintAttribute.cs +++ b/BTCPayServer/Filters/DomainMappingConstraintAttribute.cs @@ -35,9 +35,9 @@ namespace BTCPayServer.Filters var matchedDomainMapping = mapping.FirstOrDefault(item => item.AppId == appId); // App is accessed via path, redirect to canonical domain - if (matchedDomainMapping != null) + var req = context.RouteContext.HttpContext.Request; + if (matchedDomainMapping != null && req.Method != "POST" && !req.HasFormContentType) { - var req = context.RouteContext.HttpContext.Request; var uri = new UriBuilder(req.Scheme, matchedDomainMapping.Domain); if (req.Host.Port.HasValue) uri.Port = req.Host.Port.Value; context.RouteContext.HttpContext.Response.Redirect(uri.ToString()); diff --git a/BTCPayServer/Forms/FormComponentProviders.cs b/BTCPayServer/Forms/FormComponentProviders.cs index bcaa83414..32fe589dd 100644 --- a/BTCPayServer/Forms/FormComponentProviders.cs +++ b/BTCPayServer/Forms/FormComponentProviders.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using BTCPayServer.Abstractions.Form; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -9,7 +8,7 @@ public class FormComponentProviders { private readonly IEnumerable _formComponentProviders; - public Dictionary TypeToComponentProvider = new Dictionary(); + public Dictionary TypeToComponentProvider = new(); public FormComponentProviders(IEnumerable formComponentProviders) { diff --git a/BTCPayServer/Forms/Models/FormViewModel.cs b/BTCPayServer/Forms/Models/FormViewModel.cs index 1dc4cce79..77c5efc64 100644 --- a/BTCPayServer/Forms/Models/FormViewModel.cs +++ b/BTCPayServer/Forms/Models/FormViewModel.cs @@ -10,9 +10,10 @@ public class FormViewModel public string BrandColor { get; set; } public string StoreName { get; set; } public string FormName { get; set; } - public string RedirectUrl { get; set; } public Form Form { get; set; } public string AspController { get; set; } public string AspAction { get; set; } public Dictionary RouteParameters { get; set; } = new(); + public MultiValueDictionary FormParameters { get; set; } = new(); + public string FormParameterPrefix { get; set; } } diff --git a/BTCPayServer/Models/PostRedictViewModel.cs b/BTCPayServer/Models/PostRedictViewModel.cs index 5ba8d1148..c3ede2888 100644 --- a/BTCPayServer/Models/PostRedictViewModel.cs +++ b/BTCPayServer/Models/PostRedictViewModel.cs @@ -9,7 +9,7 @@ namespace BTCPayServer.Models public string FormUrl { get; set; } public bool AllowExternal { get; set; } - public MultiValueDictionary FormParameters { get; set; } = new MultiValueDictionary(); - public Dictionary RouteParameters { get; set; } = new Dictionary(); + public MultiValueDictionary FormParameters { get; set; } = new (); + public Dictionary RouteParameters { get; set; } = new (); } } diff --git a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs index 79f840c92..60ed36b64 100644 --- a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs +++ b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs @@ -26,8 +26,11 @@ using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitcoin.DataEncoders; using NBitpayClient; using Newtonsoft.Json.Linq; using NicolasDorier.RateLimits; @@ -240,13 +243,18 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers case not null: if (formResponse is null) { - return View("PostRedirect", new PostRedirectViewModel + var vm = new PostRedirectViewModel { AspAction = nameof(POSForm), + AspController = "UIPointOfSale", RouteParameters = new Dictionary { { "appId", appId } }, - AspController = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture), FormParameters = new MultiValueDictionary(Request.Form.Select(pair => new KeyValuePair>(pair.Key, pair.Value))) - }); + }; + if (viewType.HasValue) + { + vm.RouteParameters.Add("viewType", viewType.Value.ToString()); + } + return View("PostRedirect", vm); } formResponseJObject = JObject.Parse(formResponse); @@ -255,7 +263,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers if (!FormDataService.Validate(form, ModelState)) { //someone tried to bypass validation - return RedirectToAction(nameof(ViewPointOfSale), new {appId}); + return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType }); } formResponseJObject = form.GetValues(); @@ -276,7 +284,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl, RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl : !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl - : Request.GetDisplayUrl(), + : Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), "UIPointOfSale", new { appId, viewType })), FullNotifications = true, ExtendedNotifications = true, PosData = string.IsNullOrEmpty(posData) ? null : posData, @@ -294,8 +302,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers if (formResponseJObject is not null) { var meta = entity.Metadata.ToJObject(); - meta.Merge(formResponseJObject); - entity.Metadata = InvoiceMetadata.FromJObject(meta); + formResponseJObject.Merge(meta); + entity.Metadata = InvoiceMetadata.FromJObject(formResponseJObject); } }); return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id }); @@ -312,8 +320,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers } } - [HttpPost("/apps/{appId}/pos/form")] - public async Task POSForm(string appId) + [HttpPost("/apps/{appId}/pos/form/{viewType?}")] + public async Task POSForm(string appId, PosViewType? viewType = null) { var app = await _appService.GetApp(appId, AppType.PointOfSale); if (app == null) @@ -323,20 +331,18 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers var formData = await FormDataService.GetForm(settings.FormId); if (formData is null) { - return RedirectToAction(nameof(ViewPointOfSale), new { appId }); + return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType }); } - var myDictionary = Request.Form + var prefix = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)) + "_"; + var formParameters = Request.Form .Where(pair => pair.Key != "__RequestVerificationToken") - .ToDictionary(p => p.Key, p => p.Value.ToString()); - myDictionary.Add("appId", appId); - var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture); - var redirectUrl = Url.Action(nameof(ViewPointOfSale), controller, myDictionary); + .ToMultiValueDictionary(p => p.Key, p => p.Value.ToString()); + var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);; var store = await _appService.GetStore(app); var storeBlob = store.GetStoreBlob(); var form = Form.Parse(formData.Config); - - return View("Views/UIForms/View", new FormViewModel + var vm = new FormViewModel { StoreName = store.StoreName, BrandColor = storeBlob.BrandColor, @@ -344,46 +350,63 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers LogoFileId = storeBlob.LogoFileId, FormName = formData.Name, Form = form, - RedirectUrl = redirectUrl, AspController = controller, AspAction = nameof(POSFormSubmit), RouteParameters = new Dictionary { { "appId", appId } }, - }); + FormParameters = formParameters, + FormParameterPrefix = prefix + }; + if (viewType.HasValue) + { + vm.RouteParameters.Add("viewType", viewType.Value.ToString()); + } + + return View("Views/UIForms/View", vm); } - [HttpPost("/apps/{appId}/pos/form/submit")] - public async Task POSFormSubmit(string appId, FormViewModel viewModel) + [HttpPost("/apps/{appId}/pos/form/submit/{viewType?}")] + public async Task POSFormSubmit(string appId, FormViewModel viewModel, PosViewType? viewType = null) { var app = await _appService.GetApp(appId, AppType.PointOfSale); if (app == null) return NotFound(); + var settings = app.GetSettings(); var formData = await FormDataService.GetForm(settings.FormId); - if (formData is null || viewModel.RedirectUrl is null) + if (formData is null) { - return RedirectToAction(nameof(ViewPointOfSale), new {appId }); + return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType }); } - var form = Form.Parse(formData.Config); - if (Request.Method == "POST" && Request.HasFormContentType) + var formFieldNames = form.GetAllFields().Select(tuple => tuple.FullName).Distinct().ToArray(); + var formParameters = Request.Form + .Where(pair => pair.Key.StartsWith(viewModel.FormParameterPrefix)) + .ToDictionary(pair => pair.Key.Replace(viewModel.FormParameterPrefix, string.Empty), pair => pair.Value) + .ToMultiValueDictionary(p => p.Key, p => p.Value.ToString()); + + if (Request is { Method: "POST", HasFormContentType: true }) { - form.ApplyValuesFromForm(Request.Form); + form.ApplyValuesFromForm(Request.Form.Where(pair => formFieldNames.Contains(pair.Key))); + if (FormDataService.Validate(form, ModelState)) { + + var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);; + var redirectUrl = + Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), controller, new {appId, viewType})); + formParameters.Add("formResponse", form.GetValues().ToString()); return View("PostRedirect", new PostRedirectViewModel { - FormUrl = viewModel.RedirectUrl, - FormParameters = - { - { "formResponse", form.GetValues().ToString() } - } + FormUrl = redirectUrl, + FormParameters = formParameters }); } } viewModel.FormName = formData.Name; viewModel.Form = form; - + + viewModel.FormParameters = formParameters; return View("Views/UIForms/View", viewModel); } diff --git a/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml b/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml index adf264bfe..106ae001d 100644 --- a/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml +++ b/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml @@ -10,6 +10,7 @@ { if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial)) { + field.Name = $"{Model.Name}_{field.Name}"; } } diff --git a/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml b/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml index c1d608f60..5f597b8ac 100644 --- a/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml +++ b/BTCPayServer/Views/Shared/PointOfSale/Public/VueLight.cshtml @@ -2,6 +2,7 @@ @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
+
{{srvModel.currencyCode}}
{{ formatCurrency(total, false) }}
diff --git a/BTCPayServer/Views/Shared/_FormWrap.cshtml b/BTCPayServer/Views/Shared/_FormWrap.cshtml new file mode 100644 index 000000000..3df0b78aa --- /dev/null +++ b/BTCPayServer/Views/Shared/_FormWrap.cshtml @@ -0,0 +1,11 @@ +@model BTCPayServer.Forms.Models.FormViewModel + +@foreach (var o in Model.FormParameters) +{ + foreach (var v in o.Value) + { + + } +} + + diff --git a/BTCPayServer/Views/UIForms/View.cshtml b/BTCPayServer/Views/UIForms/View.cshtml index fe342c404..ddbcfa9cc 100644 --- a/BTCPayServer/Views/UIForms/View.cshtml +++ b/BTCPayServer/Views/UIForms/View.cshtml @@ -34,23 +34,13 @@ @if (string.IsNullOrEmpty(Model.AspAction)) { - @if (!string.IsNullOrEmpty(Model.RedirectUrl)) - { - - } - - + } else {
- @if (!string.IsNullOrEmpty(Model.RedirectUrl)) - { - - } - - + }
diff --git a/BTCPayServer/wwwroot/light-pos/app.js b/BTCPayServer/wwwroot/light-pos/app.js index 195d35f41..90954ead9 100644 --- a/BTCPayServer/wwwroot/light-pos/app.js +++ b/BTCPayServer/wwwroot/light-pos/app.js @@ -71,6 +71,16 @@ document.addEventListener("DOMContentLoaded",function () { }, totalNumeric () { return parseFloat(this.total); + }, + posdata () { + const data = { + subTotal: this.formatCurrency(this.amountNumeric), + total: this.formatCurrency(this.totalNumeric) + } + if (this.tipNumeric > 0) data.tip = this.formatCurrency(this.tipNumeric) + if (this.discountNumeric > 0) data.discountAmount = this.formatCurrency(this.discountNumeric) + if (this.discountPercentNumeric > 0) data.discountPercentage = this.discountPercentNumeric + return JSON.stringify(data) } }, watch: {