POS improvements (#4668)

This commit is contained in:
d11n
2023-02-23 09:52:37 +01:00
committed by GitHub
parent ddb125f458
commit 66e1eee010
11 changed files with 92 additions and 56 deletions

View File

@@ -47,10 +47,10 @@ public class Form
public IEnumerable<(string FullName, List<string> Path, Field Field)> GetAllFields()
{
HashSet<string> nameReturned = new HashSet<string>();
HashSet<string> nameReturned = new();
foreach (var f in GetAllFieldsCore(new List<string>(), 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<string> errors)
{
errors = new List<string>();
HashSet<string> nameReturned = new HashSet<string>();
HashSet<string> nameReturned = new();
foreach (var f in GetAllFieldsCore(new List<string>(), 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<string> thisPath = new List<string>(path.Count + 1);
List<string> thisPath = new(path.Count + 1);
thisPath.AddRange(path);
if (!string.IsNullOrEmpty(field.Name))
{

View File

@@ -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());

View File

@@ -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<IFormComponentProvider> _formComponentProviders;
public Dictionary<string, IFormComponentProvider> TypeToComponentProvider = new Dictionary<string, IFormComponentProvider>();
public Dictionary<string, IFormComponentProvider> TypeToComponentProvider = new();
public FormComponentProviders(IEnumerable<IFormComponentProvider> formComponentProviders)
{

View File

@@ -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<string, string> RouteParameters { get; set; } = new();
public MultiValueDictionary<string, string> FormParameters { get; set; } = new();
public string FormParameterPrefix { get; set; }
}

View File

@@ -9,7 +9,7 @@ namespace BTCPayServer.Models
public string FormUrl { get; set; }
public bool AllowExternal { get; set; }
public MultiValueDictionary<string, string> FormParameters { get; set; } = new MultiValueDictionary<string, string>();
public Dictionary<string, string> RouteParameters { get; set; } = new Dictionary<string, string>();
public MultiValueDictionary<string, string> FormParameters { get; set; } = new ();
public Dictionary<string, string> RouteParameters { get; set; } = new ();
}
}

View File

@@ -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<string, string> { { "appId", appId } },
AspController = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture),
FormParameters = new MultiValueDictionary<string, string>(Request.Form.Select(pair => new KeyValuePair<string, IReadOnlyCollection<string>>(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<IActionResult> POSForm(string appId)
[HttpPost("/apps/{appId}/pos/form/{viewType?}")]
public async Task<IActionResult> 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<string, string> { { "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<IActionResult> POSFormSubmit(string appId, FormViewModel viewModel)
[HttpPost("/apps/{appId}/pos/form/submit/{viewType?}")]
public async Task<IActionResult> 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<PointOfSaleSettings>();
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);
}

View File

@@ -10,6 +10,7 @@
{
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
{
field.Name = $"{Model.Name}_{field.Name}";
<partial name="@partial.View" for="@field"></partial>
}
}

View File

@@ -2,6 +2,7 @@
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
<form id="app" method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy v-on:submit="handleFormSubmit" class="d-flex flex-column gap-4 flex-fill" v-cloak>
<input id="posdata" type="hidden" name="posdata" v-model="posdata">
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
<div class="fw-semibold text-muted">{{srvModel.currencyCode}}</div>
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }">{{ formatCurrency(total, false) }}</div>

View File

@@ -0,0 +1,11 @@
@model BTCPayServer.Forms.Models.FormViewModel
<input type="hidden" asp-for="FormParameterPrefix"/>
@foreach (var o in Model.FormParameters)
{
foreach (var v in o.Value)
{
<input type="hidden" name="@(Model.FormParameterPrefix??string.Empty)@o.Key" value="@v" />
}
}
<partial name="_Form" model="@Model.Form" />
<input type="submit" class="btn btn-primary" name="command" value="Submit" />

View File

@@ -34,23 +34,13 @@
@if (string.IsNullOrEmpty(Model.AspAction))
{
<form method="post" novalidate="novalidate">
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
{
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl" />
}
<partial name="_Form" model="@Model.Form" />
<input type="submit" class="btn btn-primary" name="command" value="Submit" />
<partial name="_FormWrap" model="@Model" />
</form>
}
else
{
<form method="post" asp-action="@Model.AspAction" asp-controller="@Model.AspController" asp-all-route-data="Model.RouteParameters">
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
{
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl" />
}
<partial name="_Form" model="@Model.Form" />
<input type="submit" class="btn btn-primary" name="command" value="Submit" />
<partial name="_FormWrap" model="@Model" />
</form>
}
</div>

View File

@@ -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: {