mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
POS improvements (#4668)
This commit is contained in:
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
11
BTCPayServer/Views/Shared/_FormWrap.cshtml
Normal file
11
BTCPayServer/Views/Shared/_FormWrap.cshtml
Normal 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" />
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user