POS: Improve Keypad view (#4596)

* UI updates

* Updates modes and calculation

* Unify tip buttons

* White caret

* Add top margin to calculation

* Add space between mode buttons and keypad

* Discount updates
This commit is contained in:
d11n
2023-02-10 16:26:38 +01:00
committed by GitHub
parent 33d272d4b0
commit d14ce2a37f
10 changed files with 429 additions and 373 deletions

View File

@@ -5,118 +5,138 @@
var customTipPercentages = Model.CustomTipPercentages;
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
}
<script id="template-cart-item" type="text/template">
<tr data-id="{id}">
<td class="align-middle pe-0" width="1%">{image}</td>
<td class="align-middle pe-0 ps-2"><b>{title}</b></td>
<td class="align-middle px-0">
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
</td>
<td class="align-middle px-0">
<div class="input-group align-items-center">
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
<input class="js-cart-item-count form-control form-control-sm pull-left hide-number-spin text-end" type="number" step="1" name="count" placeholder="Qty" max="{inventory}" value="{count}" data-prev="{count}">
<a class="input-group-text js-cart-item-plus btn btn-link px-2" href="#">
<i class="fa fa-plus-circle fa-fw text-success"></i>
</a>
</div>
</td>
<td class="align-middle text-end">{price}</td>
</tr>
</script>
<script id="template-cart-item-image" type="text/template">
<img class="cart-item-image" src="{image}" alt="">
</script>
<script id="template-cart-custom-amount" type="text/template">
<tr>
<td colspan="5">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
<div class="input-group-text">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
@section PageHeadContent {
<link rel="stylesheet" href="~/cart/css/style.css" asp-append-version="true">
<style>
.js-cart-item-minus .fa,
.js-cart-item-plus .fa {
background: #fff;
border-radius: 50%;
width: 17px;
height: 17px;
display: inline-flex;
justify-content: center;
align-items: center;
}
</style>
}
@section PageFootContent {
<script>var srvModel = @Safe.Json(Model);</script>
<script src="~/cart/js/cart.js" asp-append-version="true"></script>
<script src="~/cart/js/cart.jquery.js" asp-append-version="true"></script>
<script id="template-cart-item" type="text/template">
<tr data-id="{id}">
<td class="align-middle pe-0" width="1%">{image}</td>
<td class="align-middle pe-0 ps-2"><b>{title}</b></td>
<td class="align-middle px-0">
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
</td>
<td class="align-middle px-0">
<div class="input-group align-items-center">
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
<input class="js-cart-item-count form-control form-control-sm pull-left hide-number-spin text-end" type="number" step="1" name="count" placeholder="Qty" max="{inventory}" value="{count}" data-prev="{count}">
<a class="input-group-text js-cart-item-plus btn btn-link px-2" href="#">
<i class="fa fa-plus-circle fa-fw text-success"></i>
</a>
</div>
</div>
</td>
</tr>
</script>
</td>
<td class="align-middle text-end">{price}</td>
</tr>
</script>
<script id="template-cart-extra" type="text/template">
@if(Model.ShowCustomAmount){
<script id="template-cart-item-image" type="text/template">
<img class="cart-item-image" src="{image}" alt="">
</script>
<script id="template-cart-custom-amount" type="text/template">
<tr>
<th colspan="5" class="border-0 pb-0">
<td colspan="5">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
<div class="input-group-text">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</th>
</td>
</tr>
}
@if (Model.ShowDiscount)
{
<tr>
<th colspan="5" class="border-top-0">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</th>
</tr>
}
</script>
</script>
<script id="template-cart-tip" type="text/template">
@if (Model.EnableTips)
{
<tr>
<th colspan="5" class="border-top-0 pt-4 h5">@Model.CustomTipText</th>
</tr>
<tr>
<th colspan="5" class="border-0">
<div class="input-group mb-2">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
<input
class="js-cart-tip form-control form-control-lg"
type="number"
min="0"
step="@Model.Step"
value="{tip}"
name="tip"
placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)"
/>
<a class="js-cart-tip-remove btn btn-lg btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
<div class="row mb-1">
@if (customTipPercentages != null && customTipPercentages.Length > 0)
{
@for (int i = 0; i < customTipPercentages.Length; i++)
<script id="template-cart-extra" type="text/template">
@if (Model.ShowCustomAmount)
{
<tr>
<th colspan="5" class="border-0 pb-0">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</th>
</tr>
}
@if (Model.ShowDiscount)
{
<tr>
<th colspan="5" class="border-top-0">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</th>
</tr>
}
</script>
<script id="template-cart-tip" type="text/template">
@if (Model.EnableTips)
{
<tr>
<th colspan="5" class="border-top-0 pt-4 h5">@Model.CustomTipText</th>
</tr>
<tr>
<th colspan="5" class="border-0">
<div class="input-group mb-2">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
<input
class="js-cart-tip form-control form-control-lg"
type="number"
min="0"
step="@Model.Step"
value="{tip}"
name="tip"
placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)"
/>
<a class="js-cart-tip-remove btn btn-lg btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
<div class="row mb-1">
@if (customTipPercentages != null && customTipPercentages.Length > 0)
{
var percentage = customTipPercentages[i];
<div class="col">
<a class="js-cart-tip-btn btn btn-lg btn-light w-100 border mb-2" href="#" data-tip="@percentage">@percentage%</a>
</div>
@for (int i = 0; i < customTipPercentages.Length; i++)
{
var percentage = customTipPercentages[i];
<div class="col">
<a class="js-cart-tip-btn btn btn-lg btn-light w-100 border mb-2" href="#" data-tip="@percentage">@percentage%</a>
</div>
}
}
}
</div>
</div>
</th>
</tr>
}
</script>
<script id="template-cart-total" type="text/template">
<tr>
<th colspan="1" class="pb-4 h4">Total</th>
<th colspan="4" class="pb-4 h4 text-end">
<span class="js-cart-total">{total}</span>
</th>
</tr>}
</script>
<script id="template-cart-total" type="text/template">
<tr>
<th colspan="1" class="pb-4 h4">Total</th>
<th colspan="4" class="pb-4 h4 text-end">
<span class="js-cart-total">{total}</span>
</th>
</tr>
</script>
</tr>
</script>
}
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">

View File

@@ -2,11 +2,104 @@
@{
Layout = "PointOfSale/Public/_Layout";
}
@section PageHeadContent {
<style>
.public-page-wrap {
max-width: 560px;
overflow: hidden;
}
.keypad {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.keypad .btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
position: relative;
border-radius: 0;
font-weight: var(--btcpay-font-weight-semibold);
font-size: 24px;
min-height: 3.5rem;
height: 8vh;
max-height: 6rem;
color: var(--btcpay-body-text);
}
.keypad .btn[data-key="del"] svg {
--btn-icon-size: 2.25rem;
transform: rotate(180deg);
}
.btcpay-pills label,
.btn-secondary.rounded-pill {
padding-left: 1rem;
padding-right: 1rem;
}
/* make borders collapse by shifting rows and columns by 1px */
/* second column */
.keypad .btn:nth-child(3n-1) {
margin-left: -1px;
}
/* third column */
.keypad .btn:nth-child(3n) {
margin-left: -1px;
}
/* from second row downwards */
.keypad .btn:nth-child(n+4) {
margin-top: -1px;
}
/* ensure highlighted button is topmost */
.keypad .btn:hover,
.keypad .btn:focus,
.keypad .btn:active {
z-index: 1;
}
.actions {
display: flex;
align-items: center;
justify-content: center;
}
.actions .btn {
flex: 1 1 50%;
}
@@media (max-height: 700px) {
.store-header {
display: none !important;
}
}
@@media (max-width: 575px) {
.public-page-wrap {
padding-right: 0;
padding-left: 0;
}
.keypad {
margin-left: -1px;
margin-right: -1px;
}
.store-footer {
display: none !important;
}
}
/* fix sticky hover effect on mobile browsers */
@@media (hover: none) {
.keypad .btn-secondary:hover,
.actions .btn-secondary:hover {
border-color: var(--btcpay-secondary-border-active) !important;
}
}
</style>
}
@section PageFootContent {
<script>var srvModel = @Safe.Json(Model);</script>
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
<script src="~/light-pos/app.js" asp-append-version="true"></script>
}
<div class="public-page-wrap flex-column">
<partial name="_StatusMessage" />
<partial name="_StoreHeader" model="(string.IsNullOrEmpty(Model.Title) ? Model.StoreName : Model.Title, Model.LogoFileId)" />
@if (Context.Request.Query.ContainsKey("simple"))
{
<partial name="PointOfSale/Public/MinimalLight" model="Model" />

View File

@@ -6,15 +6,6 @@
@inject StoreRepository StoreRepository
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
<style>
/* This hides unwanted metadata such as url, date, etc from appearing on a printed page. */
@@media print {
@@page {
margin-top: 0;
margin-bottom: 0;
}
}
</style>
@{
var store = await StoreRepository.FindStore(Model.StoreId);
Layout = "PointOfSale/Public/_Layout";
@@ -26,6 +17,17 @@
supported = null;
}
}
@section PageHeadContent {
<style>
/* This hides unwanted metadata such as url, date, etc from appearing on a printed page. */
@@media print {
@@page {
margin-top: 0;
margin-bottom: 0;
}
}
</style>
}
@if (supported is null)
{

View File

@@ -1,90 +1,64 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
<div id="app" class="l-pos-wrapper" v-cloak>
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy v-on:submit="handleFormSubmit">
<div ref="display" class="l-pos-display pb-3 px-1"><div class="text-muted">{{srvModel.currencyCode}}</div><span ref="amount" v-bind:style="{fontSize: fontSize + 'px'}">{{ payTotal }}</span></div>
<div class="l-pos-keypad">
<template
v-for="(key, index) in keys"
:key="index">
<div v-if="key !== ''" class="btn"
v-bind:class="{ 'btn-primary' : (isNaN(key) === false) || key === '.', 'btn-dark' : isNaN(key) && key !== '.' }"
v-on:click="buttonClicked(key)">{{ key }}</div>
<div v-else class="btn btn-empty"></div>
</template>
<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>
<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>
<div class="text-muted text-center mt-2" v-if="calculation">{{ calculation }}</div>
</div>
<div id="ModeTabs" class="tab-content mb-n2">
<div id="Mode-Discount" class="tab-pane fade px-2" :class="{ show: mode === 'discount', active: mode === 'discount' }" role="tabpanel" aria-labelledby="ModeTablist-Discount" v-if="srvModel.showDiscount">
<div class="h4 fw-semibold text-muted text-center">
<span class="h3 text-body me-1">{{discountPercent || 0}}%</span> discount
</div>
</div>
<div class="d-flex align-items-center justify-content-center mt-4 gap-3">
<div class="btn btn-outline-secondary btn-lg flex-fill" v-on:click="clearTotal">Clear</div>
<button class="btn btn-primary btn-lg flex-fill" id="pay-button" type="submit" v-bind:disabled="payButtonLoading">
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
<span class="sr-only">Loading...</span>
</div>
Pay
</button>
</div>
<input class="form-control" type="hidden" name="amount" v-model="payTotalNumeric">
</form>
@if (Model.ShowDiscount)
{
<div class="input-group mt-4">
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
<input
class="js-cart-discount form-control"
type="number"
min="0"
max="100"
step="1"
v-model="discountPercent"
v-on:change="onDiscountChange"
name="discount"
placeholder="Discount in %"
>
<a
class="js-cart-discount-remove btn btn-danger"
href="#"
v-on:click="removeDiscount"
><i class="fa fa-times"></i></a>
</div>
}
@if (Model.EnableTips)
{
<p class="pt-5 h5">@Model.CustomTipText</p>
<div class="input-group mb-2">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
<input
class="js-cart-tip form-control form-control-lg"
disabled
type="number"
min="0"
step="@Model.Step"
v-model="tipTotal"
name="tip"
placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol ?? Model.CurrencyCode)"
>
<a
class="js-cart-tip-remove btn btn-lg btn-danger"
href="#"
v-on:click="removeTip"
><i class="fa fa-times"></i></a>
</div>
<div class="d-flex align-items-center justify-content-center mt-2 gap-3">
@if (Model.CustomTipPercentages != null && Model.CustomTipPercentages.Length > 0)
{
@foreach (var percentage in Model.CustomTipPercentages)
{
<div id="Mode-Tip" class="tab-pane fade px-2" :class="{ show: mode === 'tip', active: mode === 'tip' }" role="tabpanel" aria-labelledby="ModeTablist-Tip" v-if="srvModel.enableTips">
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2">
<template v-if="srvModel.customTipPercentages">
<button
class="js-cart-tip-btn btn btn-lg btn-light w-100 border mb-2"
data-tip="@percentage"
v-on:click="tipClicked(@percentage)"
>
@percentage%
type="button"
class="btcpay-pill"
:class="{ active: !tipPercent }"
v-on:click.prevent="tipPercent = null">
<template v-if="tip && tip > 0">{{formatCurrency(tip, true)}}</template>
<template v-else>Custom</template>
</button>
}
}
<button
v-for="percentage in srvModel.customTipPercentages"
type="button"
class="btcpay-pill"
:class="{ active: tipPercent == percentage }"
v-on:click.prevent="tipPercentage(percentage)">
{{ percentage }}%
</button>
</template>
<div v-else class="h5 fw-semibold text-muted text-center">
Amount<template v-if="tip">: {{formatCurrency(tip, true)}}</template>
</div>
</div>
</div>
}
</div>
</div>
<div id="ModeTablist" class="nav btcpay-pills align-items-center justify-content-center mb-n2 pb-1" role="tablist" v-if="modes.length > 1">
<template v-for="m in modes" :key="m.value">
<input :id="`ModeTablist-${m.type}`" name="mode" :value="m.type" type="radio" role="tab" data-bs-toggle="pill" :data-bs-target="`#Mode-${m.type}`" :disabled="m.type != 'amount' && amountNumeric == 0" :aria-controls="`Mode-${m.type}`" :aria-selected="mode === m.type" :checked="mode === m.type" v-on:click="mode = m.type">
<label :for="`ModeTablist-${m.type}`">{{ m.title }}</label>
</template>
</div>
<div class="keypad">
<button v-for="k in keys" :key="k" v-on:click.prevent="keyPressed(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">
<template v-if="k === 'del'"><vc:icon symbol="caret-right"/></template>
<template v-else>{{ k }}</template>
</button>
</div>
<div class="actions px-4 gap-4">
<button class="btn btn-lg btn-secondary" type="reset" v-on:click.prevent="clear">Clear</button>
<button class="btn btn-lg btn-primary" type="submit" :disabled="payButtonLoading" id="pay-button">
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
<span class="sr-only">Loading...</span>
</div>
<template v-else>Charge</template>
</button>
</div>
<input class="form-control" type="hidden" name="amount" v-model="totalNumeric">
</form>

View File

@@ -1,6 +1,4 @@
@using BTCPayServer.Services.Apps
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.TagHelpers
@using Microsoft.AspNetCore.Hosting
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Newtonsoft.Json.Linq
@@ -40,28 +38,6 @@
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
<link rel="apple-touch-startup-image" href="~/img/splash.png">
<link rel="manifest" href="@(await GetDynamicManifest(ViewData["Title"]!.ToString()))">
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
}
@if (Model.ViewType == PosViewType.Cart)
{
<link rel="stylesheet" href="~/cart/css/style.css" asp-append-version="true">
<script>var srvModel = @Safe.Json(Model);</script>
<script src="~/vendor/jquery/jquery.min.js" asp-append-version="true"></script>
<script src="~/vendor/bootstrap/bootstrap.bundle.min.js" asp-append-version="true"></script>
<script src="~/cart/js/cart.js" asp-append-version="true"></script>
<script src="~/cart/js/cart.jquery.js" asp-append-version="true"></script>
}
@if (Model.ViewType == PosViewType.Light)
{
<link href="~/light-pos/styles/main.css" asp-append-version="true" rel="stylesheet" />
<script>var srvModel = @Safe.Json(Model);</script>
<script src="~/vendor/jquery/jquery.min.js" asp-append-version="true"></script>
<script src="~/vendor/bootstrap/bootstrap.bundle.min.js" asp-append-version="true"></script>
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
<script src="~/light-pos/app.js" asp-append-version="true"></script>
}
<style>
.lead :last-child {
margin-bottom: 0;
@@ -78,23 +54,12 @@
max-width: 320px;
margin: auto !important;
}
.js-cart-item-minus .fa,
.js-cart-item-plus .fa {
background: #fff;
border-radius: 50%;
width: 17px;
height: 17px;
display: inline-flex;
justify-content: center;
align-items: center;
}
</style>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>");
}
@await RenderSectionAsync("PageHeadContent", false)
</head>
<body class="min-vh-100">
@RenderBody()
<partial name="LayoutFoot"/>
@await RenderSectionAsync("PageFootContent", false)
</body>
</html>

View File

@@ -1,40 +1,91 @@
let app = null;
document.addEventListener("DOMContentLoaded",function () {
const displayFontSize = 80;
app = new Vue({
const displayFontSize = 64;
new Vue({
el: '#app',
data: function () {
data () {
return {
srvModel: window.srvModel,
payTotal: '0',
payTotalNumeric: 0,
tipTotal: null,
tipTotalNumeric: 0,
mode: 'amount',
amount: null,
tip: null,
tipPercent: null,
discount: null,
discountPercent: null,
discountTotalNumeric: 0,
fontSize: displayFontSize,
defaultFontSize: displayFontSize,
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', 'C'],
payButtonLoading: false,
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', 'del'],
payButtonLoading: false
}
},
created: function() {
/** We need to unset state in case user clicks the browser back button */
window.addEventListener('pagehide', this.unsetPayButtonLoading);
},
destroyed: function() {
window.removeEventListener('pagehide', this.unsetPayButtonLoading);
},
computed: {
Currency: function(){
return this.srvModel.Currency.toUpperCase();
modes () {
const modes = [{ title: 'Amount', type: 'amount' }]
if (this.srvModel.showDiscount) modes.push({ title: 'Discount', type: 'discount' })
if (this.srvModel.enableTips) modes.push({ title: 'Tip', type: 'tip'})
return modes
},
keypadTarget () {
switch (this.mode) {
case 'amount':
return 'amount';
case 'discount':
return 'discountPercent';
case 'tip':
return 'tip';
}
},
calculation () {
if (!this.tipNumeric && !this.discountNumeric) return null
let calc = this.formatCurrency(this.amountNumeric, true)
if (this.discountNumeric > 0) calc += ` - ${this.formatCurrency(this.discountNumeric, true)} (${this.discountPercent}%)`
if (this.tipNumeric > 0) calc += ` + ${this.formatCurrency(this.tipNumeric, true)}`
if (this.tipPercent) calc += ` (${this.tipPercent}%)`
return calc
},
amountNumeric () {
const value = parseFloat(this.amount)
return isNaN(value) ? 0.0 : value
},
discountPercentNumeric () {
const value = parseFloat(this.discountPercent)
return isNaN(value) ? 0.0 : value;
},
discountNumeric () {
return this.amountNumeric && this.discountPercentNumeric
? this.amountNumeric * (this.discountPercentNumeric / 100)
: 0.0;
},
amountMinusDiscountNumeric () {
return this.amountNumeric - this.discountNumeric;
},
tipNumeric () {
if (this.tipPercent) {
return this.amountMinusDiscountNumeric * (this.tipPercent / 100);
} else {
const value = parseFloat(this.tip)
return isNaN(value) ? 0.0 : value;
}
},
total () {
return (this.amountNumeric - this.discountNumeric + this.tipNumeric);
},
totalNumeric () {
return parseFloat(this.total);
}
},
watch: {
payTotal: function() {
// This must be timeouted because the updated width is not available yet
this.$nextTick(function(){
discountPercent (val) {
const value = parseFloat(val)
if (isNaN(value)) this.discountPercent = null
else if (value > 100) this.discountPercent = '100'
else this.discountPercent = value.toString();
},
tip (val) {
this.tipPercent = null;
},
total () {
// This must be timed out because the updated width is not available yet
this.$nextTick(function () {
const displayWidth = this.getWidth(this.$refs.display),
amountWidth = this.getWidth(this.$refs.amount),
gamma = displayWidth / amountWidth || 0,
@@ -51,96 +102,81 @@ document.addEventListener("DOMContentLoaded",function () {
}
},
methods: {
getWidth: function(el) {
getWidth (el) {
const styles = window.getComputedStyle(el),
width = parseFloat(el.clientWidth),
padL = parseFloat(styles.paddingLeft),
padR = parseFloat(styles.paddingRight);
return width - padL - padR;
},
clearTotal: function() {
this.payTotal = '0';
this.payTotalNumeric = 0;
this.tipTotal = null;
this.tipTotalNumeric = 0;
this.discountPercent = null;
this.discountTotalNumeric = 0;
clear () {
this.amount = this.tip = this.discount = this.tipPercent = this.discountPercent = null;
this.mode = 'amount';
},
handleFormSubmit: function() {
handleFormSubmit () {
this.payButtonLoading = true;
},
unsetPayButtonLoading: function() {
unsetPayButtonLoading () {
this.payButtonLoading = false;
},
buttonClicked: function(key) {
let payTotal = this.payTotal;
if (key === 'C') {
payTotal = payTotal.substring(0, payTotal.length - 1);
payTotal = payTotal === '' ? '0' : payTotal;
formatCrypto (value, withSymbol) {
const symbol = withSymbol ? ` ${this.srvModel.currencySymbol || this.srvModel.currencyCode}` : '';
const divisibility = this.srvModel.currencyInfo.divisibility;
return parseFloat(value).toFixed(divisibility) + symbol;
},
formatCurrency (value, withSymbol) {
const currency = this.srvModel.currencyCode;
if (currency === 'BTC' || currency === 'SATS') return this.formatCrypto(value, withSymbol);
const divisibility = this.srvModel.currencyInfo.divisibility;
const locale = currency === 'USD' ? 'en-US' : navigator.language;
const style = withSymbol ? 'currency' : 'decimal';
const opts = { currency, style, maximumFractionDigits: divisibility, minimumFractionDigits: divisibility };
try {
return new Intl.NumberFormat(locale, opts).format(value);
} catch (err) {
return this.formatCrypto(value, withSymbol);
}
},
applyKeyToValue (key, value) {
if (!value) value = '';
if (key === 'del') {
value = value.substring(0, value.length - 1);
value = value === '' ? '0' : value;
} else if (key === '.') {
// Only add decimal point if it doesn't exist yet
if (payTotal.indexOf('.') === -1) {
payTotal += key;
if (value.indexOf('.') === -1) {
value += key;
}
} else { // Is a digit
if (payTotal === '0') {
payTotal = '';
if (!value || value === '0') {
value = '';
}
payTotal += key;
value += key;
const { divisibility } = this.srvModel.currencyInfo;
const decimalIndex = payTotal.indexOf('.')
if (decimalIndex !== -1 && (payTotal.length - decimalIndex - 1 > divisibility)) {
payTotal = payTotal.replace(".", "");
payTotal = payTotal.substr(0, payTotal.length - divisibility) + "." +
payTotal.substr(payTotal.length - divisibility);
const decimalIndex = value.indexOf('.')
if (decimalIndex !== -1 && (value.length - decimalIndex - 1 > divisibility)) {
value = value.replace('.', '');
value = value.substr(0, value.length - divisibility) + '.' +
value.substr(value.length - divisibility);
}
}
this.payTotal = payTotal;
this.payTotalNumeric = parseFloat(payTotal);
this.tipTotalNumeric = 0;
this.tipTotal = null;
this.discountTotalNumeric = 0;
this.discountPercent = null;
return value;
},
tipClicked: function(percentage) {
const { divisibility } = this.srvModel.currencyInfo;
this.payTotalNumeric -= this.tipTotalNumeric;
this.tipTotalNumeric = parseFloat((this.payTotalNumeric * (percentage / 100)).toFixed(divisibility));
this.payTotalNumeric = parseFloat((this.payTotalNumeric + this.tipTotalNumeric).toFixed(divisibility));
this.payTotal = this.payTotalNumeric.toString(10);
this.tipTotal = this.tipTotalNumeric === 0 ? null : this.tipTotalNumeric.toFixed(divisibility);
},
removeTip: function() {
this.payTotalNumeric -= this.tipTotalNumeric;
this.payTotal = this.payTotalNumeric.toString(10);
this.tipTotalNumeric = 0;
this.tipTotal = null;
},
removeDiscount: function() {
this.payTotalNumeric += this.discountTotalNumeric;
this.payTotal = this.payTotalNumeric.toString(10);
this.discountTotalNumeric = 0;
this.discountPercent = null;
// Remove the tips as well as it won't be the right number anymore after discount is removed
this.removeTip();
},
onDiscountChange: function (e){
// Remove tip if we are changing discount % as it won't be the right number anymore
this.removeTip();
const discountPercent = parseFloat(e.target.value);
const { divisibility } = this.srvModel.currencyInfo;
this.payTotalNumeric += this.discountTotalNumeric;
this.discountTotalNumeric = parseFloat((this.payTotalNumeric * (discountPercent / 100)).toFixed(divisibility));
this.payTotalNumeric = parseFloat((this.payTotalNumeric - this.discountTotalNumeric).toFixed(divisibility));
this.payTotal = this.payTotalNumeric.toString(10);
keyPressed (key) {
this[this.keypadTarget] = this.applyKeyToValue(key, this[this.keypadTarget]);
},
tipPercentage (percentage) {
this.tipPercent = this.tipPercent !== percentage
? percentage
: null;
}
},
created () {
/** We need to unset state in case user clicks the browser back button */
window.addEventListener('pagehide', this.unsetPayButtonLoading);
},
destroyed () {
window.removeEventListener('pagehide', this.unsetPayButtonLoading);
}
});
});

View File

@@ -1,41 +0,0 @@
[v-cloak] > * {
display: none
}
[v-cloak]::before {
content: "loading…"
}
.l-pos-wrapper {
max-width: 450px;
margin: auto;
}
.l-pos-header {
color: #fff;
}
.l-pos-display {
font-size: 1.4rem;
overflow: hidden;
}
.l-pos-display span {
display: inline-block;
height: 80px;
line-height: 80px;
}
.l-pos-keypad .btn {
width: 32%;
margin-right: 1%;
margin-bottom: 1%;
border-radius: 0;
padding-top: 4%;
padding-bottom: 4%;
font-weight: bold;
font-size: 1.3rem;
}
.logo {
height: 40px;
}

View File

@@ -11168,16 +11168,16 @@ ul:not([class]) li {
/* Button */
.btn-outline-secondary {
color: var(--btcpay-secondary-text);
--btcpay-btn-color: var(--btcpay-secondary-text);
}
.btn-outline-secondary:hover {
color: var(--btcpay-secondary-text-hover);
border-color: var(--btcpay-secondary-border-hover);
--btcpay-btn-color: var(--btcpay-secondary-text-hover);
--btcpay-btn-border-color: var(--btcpay-secondary-border-hover);
}
.btn-outline-secondary:active {
color: var(--btcpay-secondary-text-active);
--btcpay-btn-color: var(--btcpay-secondary-text-active);
}
.btn .icon {
@@ -11695,8 +11695,9 @@ html[data-devenv]:before {
color: var(--btcpay-secondary-text);
opacity: .7;
padding: 4px 5px 3px 7px;
font-size: 10px;
border-top-left-radius: 4px;
font-size: var(--btcpay-font-size-xs);
font-family: var(--btcpay-font-family-monospace);
border-top-left-radius: var(--btcpay-border-radius);
}
@media (max-width: 575px) { html[data-devenv]:before { content: 'XS'; } }

View File

@@ -608,10 +608,17 @@ input:checked + .btcpay-list-select-item {
.public-page-wrap {
display: flex;
gap: 1.5rem;
min-height: 100vh;
margin: 0 auto;
padding: var(--btcpay-space-l) var(--btcpay-space-m);
}
/* gradually try to set better but less supported values and units */
.min-vh-100,
.public-page-wrap {
min-height: -webkit-fill-available !important;
min-height: 100dvh !important;
}
@media (max-width: 400px) {
.public-page-wrap {
padding-left: 0;

View File

@@ -1,7 +1,6 @@
{
"name": "BTCPay Server Point of Sale",
"short_name": "BTCPay POS",
"theme_color": "green",
"background_color": "white",
"display": "standalone",
"icons": [