mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-01 00:59:15 +01:00
Removes the current value on focus, so that the user gets to see the available options. If no selection or change is made, the value is reset to the previous value on blur. Closes #4154.
431 lines
23 KiB
Text
431 lines
23 KiB
Text
@using BTCPayServer.Services.Apps
|
|
@using BTCPayServer.Abstractions.Models
|
|
@using BTCPayServer.Views.Apps
|
|
@using BTCPayServer.Abstractions.Extensions
|
|
@model BTCPayServer.Plugins.PointOfSale.Models.UpdatePointOfSaleViewModel
|
|
@{
|
|
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
|
|
}
|
|
|
|
<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-xl-8 col-xxl-constrain">
|
|
<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 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 class="form-group">
|
|
<label asp-for="Currency" class="form-label"></label>
|
|
<input asp-for="Currency" class="form-control w-auto" currency-selection />
|
|
<small class="d-inline-block form-text text-muted">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</small>
|
|
<span asp-validation-for="Currency" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<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 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 class="row">
|
|
<div class="col-xl-8 col-xxl-constrain">
|
|
<h3 class="mt-5 mb-4">Appearance</h3>
|
|
<div class="form-group">
|
|
<label asp-for="DefaultView" class="form-label" data-required></label>
|
|
<select asp-for="DefaultView" asp-items="@Html.GetEnumSelectList<PosViewType>()" class="form-select" required></select>
|
|
<span asp-validation-for="DefaultView" class="text-danger"></span>
|
|
<div class="mt-2">
|
|
<span class="text-secondary">Choose the point of sale style for your customers.</span>
|
|
</div>
|
|
</div>
|
|
<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="RequiresRefundEmail" class="form-label"></label>
|
|
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select" required></select>
|
|
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
|
</div>
|
|
<section id="discounts" class="p-0">
|
|
<h3 class="mt-5 mb-4">Discounts</h3>
|
|
<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>
|
|
</section>
|
|
<section id="custom-payments" class="p-0">
|
|
<h3 class="mt-5 mb-4">Custom Payments</h3>
|
|
<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 mb-0 pb-3">
|
|
<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>
|
|
</section>
|
|
<section id="tips" class="p-0">
|
|
<h3 class="mt-5 mb-4">Tips</h3>
|
|
<div class="form-group d-flex align-items-center">
|
|
<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 pt-3">
|
|
<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 mb-0">
|
|
<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>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-xl-8 col-xxl-constrain">
|
|
<h3 class="mt-5 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">
|
|
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
|
</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>@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 posStyleSelector = document.getElementById('DefaultView');
|
|
posStyleSelector.addEventListener('change', function(e) {
|
|
handleStyleSelected(e.target.value);
|
|
});
|
|
|
|
function handleStyleSelected(style) {
|
|
switch (style) {
|
|
case '0': // Item list only
|
|
case '3': // Print
|
|
hideDiscountsSection();
|
|
hideButtonPriceTextSection();
|
|
showCustomPaymentAmountSection();
|
|
hideTipsSection();
|
|
break;
|
|
case '1': // Item list and cart
|
|
showDiscountsSection();
|
|
showButtonPriceTextSection();
|
|
showCustomPaymentAmountSection();
|
|
showTipsSection();
|
|
break;
|
|
case '2': // Keypad only
|
|
showDiscountsSection();
|
|
hideButtonPriceTextSection();
|
|
hideCustomPaymentAmountSection();
|
|
showTipsSection();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/** Show/hide discounts section */
|
|
|
|
const discountsSection = document.getElementById('discounts');
|
|
|
|
function hideDiscountsSection() {
|
|
hideElement(discountsSection);
|
|
}
|
|
|
|
function showDiscountsSection() {
|
|
showElement(discountsSection);
|
|
}
|
|
|
|
/***************************************/
|
|
|
|
/** Show/hide button text section */
|
|
|
|
const buttonPriceTextSection = document.getElementById('button-price-text');
|
|
|
|
function hideButtonPriceTextSection() {
|
|
hideElement(buttonPriceTextSection);
|
|
}
|
|
|
|
function showButtonPriceTextSection() {
|
|
showElement(buttonPriceTextSection);
|
|
}
|
|
|
|
/***************************************/
|
|
|
|
/** Show/hide custom payments amount seciton */
|
|
|
|
const customPaymentAmountSection = document.getElementById('custom-payments');
|
|
|
|
function hideCustomPaymentAmountSection() {
|
|
hideElement(customPaymentAmountSection);
|
|
}
|
|
|
|
function showCustomPaymentAmountSection() {
|
|
showElement(customPaymentAmountSection);
|
|
}
|
|
|
|
/***************************************/
|
|
|
|
/** Show/hide tips seciton */
|
|
|
|
const tipsSection = document.getElementById('tips');
|
|
|
|
function hideTipsSection() {
|
|
hideElement(tipsSection);
|
|
}
|
|
|
|
function showTipsSection() {
|
|
showElement(tipsSection);
|
|
}
|
|
|
|
/***************************************/
|
|
|
|
function hideElement(el) {
|
|
el.setAttribute('hidden', true);
|
|
}
|
|
|
|
function showElement(el) {
|
|
el.removeAttribute('hidden');
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
handleStyleSelected(posStyleSelector.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>
|
|
}
|