mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 22:46:49 +01:00
* Custom Forms * Update BTCPayServer.Data/Migrations/20230125085242_AddForms.cs * Cleanups * Explain public form * Add store branding * Add form name to POS form * add tests * fix migration * Minor cleanups * Code improvements * Add form validation Closes #4317. * Adapt form validation for Bootstrap 5 * update logic for forms * pr changes * Minor code cleanup * Remove unused parameters * Refactor Form data handling to avoid O(n3) issues * Rename Hidden to Constant * Pre-populate FormView from the query string params * Fix test --------- Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
408 lines
23 KiB
Text
408 lines
23 KiB
Text
@using BTCPayServer.Services.Apps
|
|
@using BTCPayServer.Abstractions.Models
|
|
@using BTCPayServer.Views.Apps
|
|
@using BTCPayServer.Abstractions.Extensions
|
|
@using BTCPayServer.Forms
|
|
@using BTCPayServer.Services.Stores
|
|
@inject FormDataService FormDataService
|
|
@model BTCPayServer.Plugins.PointOfSale.Models.UpdatePointOfSaleViewModel
|
|
@{
|
|
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
|
|
|
|
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
|
|
}
|
|
|
|
<form method="post">
|
|
<div class="sticky-header-setup"></div>
|
|
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
|
|
<h2 class="mb-0">@ViewData["Title"]</h2>
|
|
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
|
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
|
|
<a class="btn btn-secondary" asp-action="ViewPointOfSale" asp-route-appId="@Model.Id" id="ViewApp" target="_blank">View</a>
|
|
</div>
|
|
</div>
|
|
|
|
<partial name="_StatusMessage" />
|
|
|
|
<input type="hidden" asp-for="StoreId" />
|
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
|
|
|
<div class="row">
|
|
<div class="col-sm-10 col-md-9 col-xl-7 col-xxl-6">
|
|
<div class="row">
|
|
<div class="col-sm-6">
|
|
<div class="form-group">
|
|
<label asp-for="AppName" class="form-label" data-required></label>
|
|
<input asp-for="AppName" class="form-control" required />
|
|
<span asp-validation-for="AppName" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<div class="form-group">
|
|
<label asp-for="Title" class="form-label" data-required></label>
|
|
<input asp-for="Title" class="form-control" required />
|
|
<span asp-validation-for="Title" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label asp-for="DefaultView" class="form-label" data-required>Choose Point of Sale Style</label>
|
|
<div class="btcpay-list-select">
|
|
@foreach (var type in Enum.GetValues<PosViewType>())
|
|
{
|
|
<input type="radio" asp-for="DefaultView" value="@type" id="DefaultView_@type">
|
|
<label for="DefaultView_@type" class="btcpay-list-select-item">
|
|
<vc:icon symbol="pos-@type.ToString().ToLowerInvariant()" />
|
|
@typeof(PosViewType).DisplayName(type.ToString())
|
|
</label>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="form-group mb-0">
|
|
<label asp-for="Currency" class="form-label"></label>
|
|
<input asp-for="Currency" class="form-control w-auto" currency-selection />
|
|
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
|
|
<span asp-validation-for="Currency" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="description" class="row mt-4">
|
|
<div class="col-xl-10 col-xxl-constrain">
|
|
<div class="form-group mb-0">
|
|
<label asp-for="Description" class="form-label"></label>
|
|
<textarea asp-for="Description" rows="10" cols="40" class="form-control richtext"></textarea>
|
|
<span asp-validation-for="Description" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="products">
|
|
<div class="row">
|
|
<div class="col-xxl-constrain">
|
|
<partial name="TemplateEditor" model="@(nameof(Model.Template), "Products", Model.Currency ?? Model.StoreDefaultCurrency)" />
|
|
</div>
|
|
</div>
|
|
<div class="row collapse" id="RawEditor">
|
|
<div class="col-xxl-constrain">
|
|
<div class="form-group pt-3">
|
|
<label asp-for="Template" class="form-label"></label>
|
|
<textarea asp-for="Template" rows="10" cols="40" class="form-control"></textarea>
|
|
<span asp-validation-for="Template" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-5">
|
|
<div class="col-sm-10 col-md-9 col-xl-7 col-xxl-6">
|
|
<h3 class="mb-4">Checkout</h3>
|
|
<fieldset>
|
|
<div class="form-group" id="button-price-text">
|
|
<label asp-for="ButtonText" class="form-label" data-required></label>
|
|
<input asp-for="ButtonText" class="form-control" required />
|
|
<span asp-validation-for="ButtonText" class="text-danger"></span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label asp-for="FormId" class="form-label"></label>
|
|
<select asp-for="FormId" class="form-select w-auto" asp-items="@checkoutFormOptions"></select>
|
|
<span asp-validation-for="FormId" class="text-danger"></span>
|
|
</div>
|
|
</fieldset>
|
|
<fieldset id="tips" class="mt-2">
|
|
<legend class="h5 mb-3 fw-semibold">Tips</legend>
|
|
<div class="form-group d-flex align-items-center pt-2">
|
|
<input asp-for="EnableTips" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#CustomTipsSettings" aria-expanded="@Model.EnableTips" aria-controls="CustomTipsSettings" />
|
|
<label asp-for="EnableTips" class="form-label mb-0"></label>
|
|
<span asp-validation-for="EnableTips" class="text-danger"></span>
|
|
</div>
|
|
<div class="collapse @(Model.EnableTips ? "show" : "")" id="CustomTipsSettings">
|
|
<div class="form-group">
|
|
<label asp-for="CustomTipText" class="form-label" data-required></label>
|
|
<input asp-for="CustomTipText" class="form-control" required />
|
|
<span asp-validation-for="CustomTipText" class="text-danger"></span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label asp-for="CustomTipPercentages" class="form-label"></label>
|
|
<input asp-for="CustomTipPercentages" class="form-control" />
|
|
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
<fieldset id="discounts" class="mt-2">
|
|
<legend class="h5 mb-3 fw-semibold">Discounts</legend>
|
|
<div class="form-group d-flex align-items-center">
|
|
<input asp-for="ShowDiscount" type="checkbox" class="btcpay-toggle me-3" />
|
|
<div>
|
|
<label asp-for="ShowDiscount" class="form-label mb-0"></label>
|
|
<div class="text-muted">Not recommended for customer self-checkout.</div>
|
|
</div>
|
|
<span asp-validation-for="ShowDiscount" class="text-danger"></span>
|
|
</div>
|
|
</fieldset>
|
|
<fieldset id="custom-payments" class="mt-2">
|
|
<legend class="h5 mb-3 fw-semibold">Custom Payments</legend>
|
|
<div class="form-group mb-4 d-flex align-items-center">
|
|
<input asp-for="ShowCustomAmount" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#CustomAmountSettings" aria-expanded="@Model.ShowCustomAmount" aria-controls="CustomAmountSettings"/>
|
|
<label asp-for="ShowCustomAmount" class="form-label mb-0"></label>
|
|
<span asp-validation-for="ShowCustomAmount" class="text-danger"></span>
|
|
</div>
|
|
<div class="collapse @(Model.ShowCustomAmount ? "show" : "")" id="CustomAmountSettings">
|
|
<div class="form-group">
|
|
<label asp-for="CustomButtonText" class="form-label" data-required></label>
|
|
<input asp-for="CustomButtonText" class="form-control" required />
|
|
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
<div class="row" style="margin-top:2.25rem;">
|
|
<div class="col-xl-8 col-xxl-constrain">
|
|
<h3 class="mb-2">Additional Options</h3>
|
|
<div class="form-group">
|
|
<div class="accordion" id="additional">
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="additional-embed-payment-button-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-payment-button" aria-expanded="false" aria-controls="additional-embed-payment-button">
|
|
Embed a payment button linking to POS item
|
|
<vc:icon symbol="caret-down" />
|
|
</button>
|
|
</h2>
|
|
<div id="additional-embed-payment-button" class="accordion-collapse collapse" aria-labelledby="additional-embed-payment-button-header">
|
|
<div class="accordion-body">
|
|
<p>You can host point of sale buttons in an external website with the following code.</p>
|
|
@if (Model.Example1 != null)
|
|
{
|
|
<span>For anything with a custom amount</span>
|
|
<pre class="p-3">@Model.Example1</pre>
|
|
}
|
|
@if (Model.Example2 != null)
|
|
{
|
|
<span>For a specific item of your template</span>
|
|
<pre class="p-3">@Model.Example2</pre>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="additional-embed-pos-iframe-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-pos-iframe" aria-expanded="false" aria-controls="additional-embed-pos-iframe">
|
|
Embed Point of Sale via Iframe
|
|
<vc:icon symbol="caret-down" />
|
|
</button>
|
|
</h2>
|
|
<div id="additional-embed-pos-iframe" class="accordion-collapse collapse" aria-labelledby="additional-embed-pos-iframe-header">
|
|
<div class="accordion-body">
|
|
You can embed this POS via an iframe.
|
|
@{
|
|
var iframe = $"<iframe src='{Url.Action("ViewPointOfSale", "UIPointOfSale", new { appId = Model.Id }, Context.Request.Scheme)}' style='max-width: 100%; border: 0;'></iframe>";
|
|
}
|
|
<pre class="p-3">@iframe</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="additional-redirect-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-redirect" aria-expanded="false" aria-controls="additional-redirect">
|
|
Redirects
|
|
<vc:icon symbol="caret-down" />
|
|
</button>
|
|
</h2>
|
|
<div id="additional-redirect" class="accordion-collapse collapse" aria-labelledby="additional-redirect-header">
|
|
<div class="accordion-body">
|
|
<div class="form-group">
|
|
<label asp-for="RedirectUrl" class="form-label"></label>
|
|
<input asp-for="RedirectUrl" class="form-control" />
|
|
<span asp-validation-for="RedirectUrl" class="text-danger"></span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label asp-for="RedirectAutomatically" class="form-label"></label>
|
|
<select asp-for="RedirectAutomatically" asp-items="Model.RedirectAutomaticallySelectList" class="form-select"></select>
|
|
<span asp-validation-for="RedirectAutomatically" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="additional-notification-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notification" aria-expanded="false" aria-controls="additional-notification">
|
|
Notification URL Callbacks
|
|
<vc:icon symbol="caret-down" />
|
|
</button>
|
|
</h2>
|
|
<div id="additional-notification" class="accordion-collapse collapse" aria-labelledby="additional-notification-header">
|
|
<div class="accordion-body">
|
|
<div class="form-group">
|
|
<label asp-for="NotificationUrl" class="form-label"></label>
|
|
<input asp-for="NotificationUrl" class="form-control" />
|
|
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
|
|
</div>
|
|
<p>A <code>POST</code> callback will be sent to the specified <code>notificationUrl</code> (for on-chain transactions when there are sufficient confirmations):</p>
|
|
<pre class="p-3">@Model.ExampleCallback</pre>
|
|
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
|
|
<ul>
|
|
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
|
|
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
|
|
<li>You can then ship your order</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="additional-custom-css-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
|
|
Custom CSS
|
|
<vc:icon symbol="caret-down" />
|
|
</button>
|
|
</h2>
|
|
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
|
|
<div class="accordion-body">
|
|
<div class="form-group">
|
|
<label asp-for="CustomCSSLink" class="form-label"></label>
|
|
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
|
<vc:icon symbol="info" />
|
|
</a>
|
|
<input asp-for="CustomCSSLink" class="form-control" />
|
|
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label asp-for="EmbeddedCSS" class="form-label"></label>
|
|
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
|
|
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="d-flex gap-3 mt-3">
|
|
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
|
|
<a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(Model.AppName)</strong> and its settings will be permanently deleted." data-confirm-input="DELETE">Delete this app</a>
|
|
</div>
|
|
|
|
<partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" />
|
|
|
|
@section PageHeadContent {
|
|
<link href="~/vendor/highlightjs/default.min.css" rel="stylesheet" asp-append-version="true">
|
|
<link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true" />
|
|
<link href="~/main/template-editor.css" rel="stylesheet" asp-append-version="true" />
|
|
}
|
|
|
|
@section PageFootContent {
|
|
<partial name="_ValidationScriptsPartial" />
|
|
<script id="template-product-item" type="text/template">
|
|
<div class="col-sm-4 col-md-3 mb-3">
|
|
<div class="card">
|
|
{image}
|
|
<div class="card-body">
|
|
<h6 class="card-title">{title}</h6>
|
|
<a href="#" class="js-product-edit btn btn-primary" data-bs-toggle="modal" data-bs-target="#product-modal">Edit</a>
|
|
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</script>
|
|
<script id="template-product-content" type="text/template">
|
|
<div class="mb-3">
|
|
<input class="js-product-id" type="hidden" name="id" value="{id}">
|
|
<input class="js-product-index" type="hidden" name="index" value="{index}">
|
|
<div class="form-group row">
|
|
<div class="col-sm-6">
|
|
<label class="form-label" data-required>Title</label>
|
|
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus required />
|
|
</div>
|
|
<div class="col-sm-3">
|
|
<label class="form-label" data-required>Price</label>
|
|
<input class="js-product-price form-control mb-2"
|
|
inputmode="decimal"
|
|
pattern="\d*"
|
|
type="number"
|
|
value="{price}"
|
|
required />
|
|
</div>
|
|
<div class="col-sm-3">
|
|
<label class="form-label">Custom price</label>
|
|
<select class="js-product-custom form-select">
|
|
{custom}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Image</label>
|
|
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Inventory (leave blank to not use inventory feature)</label>
|
|
<input type="number" inputmode="numeric" step="1" class="js-product-inventory form-control mb-2" value="{inventory}" />
|
|
</div>
|
|
<div class="form-group d-flex align-items-center">
|
|
<input type="checkbox" class="btcpay-toggle me-2" value="{disabled}" />
|
|
<label class="form-label mb-0">Disabled</label>
|
|
</div>
|
|
</div>
|
|
</script>
|
|
<script>
|
|
const description = document.getElementById('description');
|
|
const products = document.getElementById('products');
|
|
const tips = document.getElementById('tips');
|
|
const discounts = document.getElementById('discounts');
|
|
const buttonPriceText = document.getElementById('button-price-text');
|
|
const customPayments = document.getElementById('custom-payments');
|
|
|
|
function hide(el) {
|
|
el.setAttribute('hidden', true);
|
|
}
|
|
function show(el) {
|
|
el.removeAttribute('hidden');
|
|
}
|
|
function updateFormForDefaultView(type) {
|
|
console.log(type)
|
|
switch (type) {
|
|
case 'Static':
|
|
case 'Print':
|
|
hide(tips);
|
|
hide(discounts);
|
|
hide(buttonPriceText);
|
|
show(description);
|
|
show(products);
|
|
show(customPayments);
|
|
break;
|
|
case 'Cart':
|
|
show(tips);
|
|
show(products);
|
|
show(discounts);
|
|
show(description);
|
|
show(buttonPriceText);
|
|
show(customPayments);
|
|
break;
|
|
case 'Light':
|
|
show(tips);
|
|
show(discounts);
|
|
hide(products);
|
|
hide(description);
|
|
hide(buttonPriceText);
|
|
hide(customPayments);
|
|
break;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const defaultView = document.querySelector('input[name="DefaultView"]:checked');
|
|
if (defaultView) {
|
|
updateFormForDefaultView(defaultView.value);
|
|
}
|
|
});
|
|
|
|
delegate('change', 'input[name="DefaultView"]', e => {
|
|
updateFormForDefaultView(e.target.value);
|
|
});
|
|
</script>
|
|
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
|
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
|
|
}
|