Compare commits

..

4 Commits

Author SHA1 Message Date
8c6fe91c71 After successful migration from SQLite or MySql, there is an error after a restart 2023-02-11 21:01:36 +09:00
d14ce2a37f 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
2023-02-10 16:26:38 +01:00
33d272d4b0 Crowdfund: View updates (#4588)
* Crowdfund: View updates

Improve store branding and remove the card styles, because they had borders which seemed like visual clutter. Other than that I made some changes to the header section and cleaned up the markup and indentation.

* adds column spacing + details header

* Move the Featured Image input

* Center align the Last Updated

* Remove store header, update header section

* Bump description font size

* Improve perk display

* Improve details section

* Fix main image display

---------

Co-authored-by: dstrukt <gfxdsign@gmail.com>
2023-02-10 16:26:09 +01:00
57f5c15670 Merge pull request #4620 from btcpayserver/Kukks-patch-1 2023-02-10 09:45:30 +01:00
16 changed files with 596 additions and 554 deletions

View File

@ -893,7 +893,7 @@ namespace BTCPayServer.Tests
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
Assert.Equal("currently active!",
Assert.Equal("Currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
s.Driver.Close();

View File

@ -242,7 +242,10 @@ namespace BTCPayServer.Hosting
private static async Task<string?> GetMigrationState(ApplicationDbContext postgresContext)
{
return (await postgresContext.Settings.FromSqlRaw("SELECT \"Id\", \"Value\" FROM \"Settings\" WHERE \"Id\"='MigrationData'").AsNoTracking().FirstOrDefaultAsync())?.Value;
var o = (await postgresContext.Settings.FromSqlRaw("SELECT \"Id\", \"Value\" FROM \"Settings\" WHERE \"Id\"='MigrationData'").AsNoTracking().FirstOrDefaultAsync())?.Value;
if (o is null)
return null;
return JObject.Parse(o)["state"]?.Value<string>();
}
private static async Task SetMigrationState(ApplicationDbContext postgresContext, string migratingFrom, string state)
{

View File

@ -27,10 +27,20 @@
{
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>")
}
<style>
#crowdfund-main-image {
border-radius: var(--btcpay-border-radius);
object-fit: cover;
max-width: 100%;
max-height: 40vh;
}
#crowdfund-body-description {
font-size: 16px;
}
</style>
<vc:ui-extension-point location="crowdfund-head" model="@Model"></vc:ui-extension-point>
</head>
<body class="min-vh-100">
<body class="min-vh-100 p-2">
@if (!Model.Enabled)
{
<div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert">
@ -43,31 +53,15 @@
}
<div class="public-page-wrap flex-column container" id="app" @(Model.SimpleDisplay ? "" : "v-cloak")>
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
<div class="card w-100 p-0 mx-0">
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
<img v-if="srvModel.mainImageUrl" src="@Model.MainImageUrl" :src="srvModel.mainImageUrl" alt="@Model.Title" :alt="srvModel.title" class="card-img-top" id="crowdfund-main-image" asp-append-version="true"/>
<img v-if="srvModel.mainImageUrl" src="@Model.MainImageUrl" :src="srvModel.mainImageUrl" alt="@Model.Title" :alt="srvModel.title" id="crowdfund-main-image" asp-append-version="true"/>
}
<div class="d-flex flex-column justify-content-between py-4 px-3 text-center" id="crowdfund-header-container">
<h1 v-text="srvModel.title" class="my-3">@Model.Title</h1>
@if (!Model.Started && Model.StartDate.HasValue)
<div class="d-flex flex-column justify-content-between p-3 text-center" id="crowdfund-header-container">
<h1 class="mb-3">@Model.Title</h1>
@if (!string.IsNullOrEmpty(Model.Tagline))
{
<h6 class="text-muted fst-italic" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" v-text="`starts in ${startDiff}`" data-test="time-state">
starts @TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
</h6>
}
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
{
<h6 class="text-muted fst-italic" v-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" v-text="`ends in ${endDiff}`" data-test="time-state">
ends @TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
</h6>
}
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
{
<h6 class="text-muted fst-italic" v-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" data-test="time-state">
currently active!
</h6>
<h2 class="h3 mb-3 fw-semibold" v-if="srvModel.tagline" v-text="srvModel.tagline">@Model.Tagline</h2>
}
@if (Model.TargetAmount.HasValue)
{
@ -100,11 +94,29 @@
}
</span>
}
@if (!Model.Started && Model.StartDate.HasValue)
{
<h6 class="text-muted fst-italic mt-3" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" v-text="`Starts in ${startDiff}`" data-test="time-state">
Starts @TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
</h6>
}
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
{
<h6 class="text-muted fst-italic mt-3" v-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" v-text="`Ends in ${endDiff}`" data-test="time-state">
Ends @TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
</h6>
}
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
{
<h6 class="text-muted fst-italic mt-3" v-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" data-test="time-state">
Currently active!
</h6>
}
</div>
@if (Model.TargetAmount.HasValue)
{
<div class="progress rounded-pill mx-3" v-if="srvModel.targetAmount" id="crowdfund-progress-bar">
<div class="progress rounded-pill" v-if="srvModel.targetAmount" id="crowdfund-progress-bar">
<div class="progress-bar bg-primary"
role="progressbar"
style="width:@(Model.Info.ProgressPercentage + "%")"
@ -130,157 +142,124 @@
</div>
}
<div class="card-body">
<div class="row py-2 text-center crowdfund-stats">
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-raised-amount">
<h3 v-text="`${raisedAmount} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount, Model.CurrencyData.Divisibility) @Model.TargetCurrency</h3>
<h5 class="text-muted fst-italic mb-0">Raised</h5>
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0" class="only-for-js">
<ul class="p-0 text-uppercase">
<li v-for="stat of paymentStats" class="list-unstyled">
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
</li>
<div class="row py-2 text-center crowdfund-stats">
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-raised-amount">
<h3 v-text="`${raisedAmount} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount, Model.CurrencyData.Divisibility) @Model.TargetCurrency</h3>
<h5 class="text-muted fst-italic mb-0">Raised</h5>
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0" class="only-for-js">
<ul class="p-0 text-uppercase">
<li v-for="stat of paymentStats" class="list-unstyled">
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
</li>
</ul>
</b-tooltip>
</div>
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-goal-raised">
<h3 v-text="`${percentageRaisedAmount}%`">@Math.Round(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h3>
<h5 class="text-muted fst-italic mb-0">Of Goal</h5>
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'" class="only-for-js">
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
</b-tooltip>
</div>
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-total-contributors">
<h3 v-text="new Intl.NumberFormat().format(srvModel.info.totalContributors)">@Model.Info.TotalContributors</h3>
<h5 class="text-muted fst-italic mb-0">Contributors</h5>
</div>
@if (Model.StartDate.HasValue || Model.EndDate.HasValue)
{
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-campaign-dates">
@if (!Model.Started && Model.StartDate.HasValue)
{
<div v-if="startDiff">
<h3 v-text="startDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)</h3>
<h5 class="text-muted fst-italic mb-0" v-text="'Left to start'">Start Date</h5>
</div>
}
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
{
<div v-if="!startDiff && endDiff">
<h3 v-text="endDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)</h3>
<h5 class="text-muted fst-italic mb-0" v-text="'Left'">End Date</h5>
</div>
}
else if (Model.Ended)
{
<div v-if="ended">
<h3 class="mb-0">Campaign not active</h3>
</div>
}
<b-tooltip v-if="startDate || endDate" target="crowdfund-body-campaign-dates" class="only-for-js">
<ul class="p-0">
@if (Model.StartDate.HasValue)
{
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
}
@if (Model.EndDate.HasValue)
{
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
}
</ul>
</b-tooltip>
</div>
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-goal-raised">
<h3 v-text="`${percentageRaisedAmount}%`">@Math.Round(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h3>
<h5 class="text-muted fst-italic mb-0">Of Goal</h5>
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'" class="only-for-js">
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
</b-tooltip>
</div>
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-total-contributors">
<h3 v-text="new Intl.NumberFormat().format(srvModel.info.totalContributors)">@Model.Info.TotalContributors</h3>
<h5 class="text-muted fst-italic mb-0">Contributors</h5>
</div>
@if (Model.StartDate.HasValue || Model.EndDate.HasValue)
{
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-campaign-dates">
@if (!Model.Started && Model.StartDate.HasValue)
{
<div v-if="startDiff">
<h3 v-text="startDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)</h3>
<h5 class="text-muted fst-italic mb-0" v-text="'Left to start'">Start Date</h5>
</div>
}
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
{
<div v-if="!startDiff && endDiff">
<h3 v-text="endDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)</h3>
<h5 class="text-muted fst-italic mb-0" v-text="'Left'">End Date</h5>
</div>
}
else if (Model.Ended)
{
<div v-if="ended">
<h3 class="mb-0">Campaign not active</h3>
</div>
}
<b-tooltip v-if="startDate || endDate" target="crowdfund-body-campaign-dates" class="only-for-js">
<ul class="p-0">
@if (Model.StartDate.HasValue)
{
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
}
@if (Model.EndDate.HasValue)
{
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
}
</ul>
</b-tooltip>
</div>
}
</div>
<div class="card-title text-center" id="crowdfund-body-header">
@if (!string.IsNullOrEmpty(Model.Tagline))
{
<h2 class="h3 my-4 fw-normal" v-if="srvModel.tagline" v-text="srvModel.tagline">@Model.Tagline</h2>
}
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary mb-4 py-2 px-5 only-for-js" v-on:click="contributeModalOpen = true">Contribute</button>
</div>
<hr class="w-100"/>
<template v-if="srvModel.disqusEnabled && srvModel.disqusShortname">
<b-tabs>
<b-tab title="Details" active>
<div class="row mt-3">
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
@Safe.Raw(Model.Description)
</div>
</div>
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:display-perks-ranking="srvModel.displayPerksRanking"
:perks-value="srvModel.perksValue"
:active="active"
:loading="loading"
:in-modal="false"
:perks="perks">
</contribute>
</div>
</div>
</b-tab>
<b-tab title="Discussion">
<div id="disqus_thread" class="mt-3"></div>
</b-tab>
</b-tabs>
</template>
<template v-else>
<div class="row mt-2">
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
@Safe.Raw(Model.Description)
</div>
</div>
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:loading="loading"
:display-perks-ranking="srvModel.displayPerksRanking"
:perks-value="srvModel.perksValue"
:active="active"
:in-modal="false"
:perks="perks">
</contribute>
</div>
</div>
</template>
<noscript>
<div class="row">
<div class="col-md-8 col-sm-12">
<div class="card-text overflow-hidden">@Safe.Raw(Model.Description)</div>
</div>
<div class="col-md-4 col-sm-12">
<partial
name="Crowdfund/Public/ContributeForm"
model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })">
</partial>
</div>
</div>
</noscript>
}
</div>
<div class="card-footer text-muted d-flex flex-wrap align-items-center">
<div class="me-3" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</div>
<div class="form-check me-3 my-0 only-for-js" v-if="srvModel.animationsEnabled || animation">
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">
<label class="form-check-label" for="cbAnime">Animations</label>
<div class="text-center mb-4" id="crowdfund-body-header">
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary py-2 px-5 only-for-js" v-on:click="contributeModalOpen = true">Contribute</button>
</div>
<div class="row mt-4 justify-content-between gap-5">
<div class="col-lg-7 col-sm-12" id="crowdfund-body-description-container">
<template v-if="srvModel.disqusEnabled && srvModel.disqusShortname">
<b-tabs>
<b-tab title="Details" active>
<div class="overflow-hidden pt-3" v-html="srvModel.description" id="crowdfund-body-description">
@Safe.Raw(Model.Description)
</div>
</b-tab>
<b-tab title="Discussion">
<div id="disqus_thread" class="mt-4"></div>
</b-tab>
</b-tabs>
</template>
<template v-else>
<div class="overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
@Safe.Raw(Model.Description)
</div>
</template>
</div>
<div class="form-check me-3 my-0 only-for-js" v-if="srvModel.soundsEnabled|| sound">
<input class="form-check-input" type="checkbox" id="cbSounds" v-model="sound">
<label class="form-check-label" for="cbSounds">Sounds</label>
<div class="col-lg-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:loading="loading"
:display-perks-ranking="srvModel.displayPerksRanking"
:perks-value="srvModel.perksValue"
:active="active"
:in-modal="false"
:perks="perks">
</contribute>
</div>
</div>
</div>
</div>
<noscript>
<div class="row justify-content-between">
<div class="col-md-7 col-sm-12">
<div class="overflow-hidden">@Safe.Raw(Model.Description)</div>
</div>
<div class="col-md-4 col-sm-12">
<partial
name="Crowdfund/Public/ContributeForm"
model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })">
</partial>
</div>
</div>
</noscript>
<div class="text-center text-muted mt-4" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</div>
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
<contribute v-if="contributeModalOpen"
:target-currency="srvModel.targetCurrency"
@ -301,7 +280,7 @@
<template id="perks-template">
<div class="perks-container">
<perk v-if="!perks || perks.length === 0"
:perk="{title: 'Donate Custom Amount', price: { type: 0, value: null}}"
:perk="{title: 'Donate Custom Amount', price: { type: 0, value: null }}"
:target-currency="targetCurrency"
:active="active"
:loading="loading"
@ -337,15 +316,14 @@
<input type="hidden" :value="perk.id" id="choiceKey"/>
<img v-if="perk.image && perk.image != 'null'" class="card-img-top" :src="perk.image"/>
<div class="card-body">
<div class="card-title d-flex justify-content-between">
<span class="h5">{{perk.title ? perk.title : perk.id}}</span>
<div class="card-title d-flex justify-content-between" :class="{ 'mb-0': !perk.description }">
<span class="h5" :class="{ 'mb-0': !perk.description }">{{perk.title ? perk.title : perk.id}}</span>
<span class="text-muted">
<template v-if="perk.price && perk.price.value">
{{formatAmount(perk.price.value.noExponents(), srvModel.currencyData.divisibility)}}
{{targetCurrency}}
<template v-if="perk.price.type == 1">or more</template>
</template>
<template v-else-if="perk.price.type === 2 && !perk.price.value">
Free
</template>
@ -356,7 +334,6 @@
</div>
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
<div class="input-group" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
<template v-if="perk.price.type !== 0 && !(perk.price.type === 2 && !perk.price.value)">
<input

View File

@ -64,6 +64,11 @@
<input asp-for="Tagline" class="form-control" />
<span asp-validation-for="Tagline" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MainImageUrl" class="form-label"></label>
<input asp-for="MainImageUrl" class="form-control" />
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
</div>
<div class="form-group">
<div class="d-flex align-items-center">
<input asp-for="Enabled" type="checkbox" class="btcpay-toggle me-3"/>
@ -293,11 +298,6 @@
</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="MainImageUrl" class="form-label"></label>
<input asp-for="MainImageUrl" class="form-control" />
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
</div>
<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">

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": [

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.7.9</Version>
<Version>1.7.10</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,11 @@
# Changelog
## 1.7.10
### Bug fix
* After successful migration from SQLite or MySql, there is an error after a restart @NicolasDorier
## 1.7.9
### Bug fixes