Merge pull request #1605 from dennisreimann/pos-refactoring

Point of Sale Refactoring
This commit is contained in:
Nicolas Dorier 2020-05-30 13:14:50 +09:00 committed by GitHub
commit a70934938a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 575 additions and 557 deletions

View file

@ -378,11 +378,20 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale" + Keys.Enter);
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.Id("EnableShoppingCart")).Click();
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart" + Keys.Enter);
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
s.Driver.Url = posBaseUrl + "/static";
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
s.Driver.Url = posBaseUrl + "/cart";
Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view");
s.Driver.Quit();
}
}

View file

@ -2314,7 +2314,7 @@ donation:
var publicApps = user.GetController<AppsPublicController>();
var vmview =
Assert.IsType<ViewPointOfSaleViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model);
Assert.Equal("hello", vmview.Title);
Assert.Equal(3, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
@ -2326,7 +2326,7 @@ donation:
Assert.Equal("Wanna tip?", vmview.CustomTipText);
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
//
var invoices = user.BitPay.GetInvoices();
@ -2337,7 +2337,7 @@ donation:
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
invoices = user.BitPay.GetInvoices();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
@ -2347,7 +2347,7 @@ donation:
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices();
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
@ -2388,7 +2388,7 @@ donation:
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
publicApps = user.GetController<AppsPublicController>();
vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model);
Assert.Equal(test.Code, vmview.CurrencyCode);
Assert.Equal(test.ExpectedSymbol,
vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well);
@ -2419,17 +2419,17 @@ noninventoryitem:
//inventoryitem has 1 item available
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
//we already bought all available stock so this should fail
await Task.Delay(100);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
//inventoryitem has unlimited items available
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
//verify invoices where created
invoices = user.BitPay.GetInvoices();
@ -2469,9 +2469,9 @@ normal:
price: 1.0";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "btconly").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "normal").Result);
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "normal").Result);
invoices = user.BitPay.GetInvoices();
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");

View file

@ -4,12 +4,9 @@ using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers
{
@ -55,7 +52,7 @@ namespace BTCPayServer.Controllers
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
" inventory: 5\n" +
" custom: true";
EnableShoppingCart = false;
DefaultView = PosViewType.Static;
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
@ -64,6 +61,7 @@ namespace BTCPayServer.Controllers
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public PosViewType DefaultView { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
@ -95,13 +93,15 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false;
var vm = new UpdatePointOfSaleViewModel()
{
Id = appId,
StoreId = app.StoreDataId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
DefaultView = settings.DefaultView,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
@ -179,7 +179,7 @@ namespace BTCPayServer.Controllers
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
DefaultView = vm.DefaultView,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
@ -194,7 +194,6 @@ namespace BTCPayServer.Controllers
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically)
});
await _AppService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";

View file

@ -41,23 +41,23 @@ namespace BTCPayServer.Controllers
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewPointOfSale(string appId)
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
return View(new ViewPointOfSaleViewModel()
return View("PointOfSale/" + viewType, new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ViewType = (PosViewType)viewType,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
@ -85,11 +85,12 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
string email,
string orderId,
@ -106,9 +107,10 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart)
settings.DefaultView = settings.EnableShoppingCart? PosViewType.Cart : settings.DefaultView;
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
}
string title = null;
var price = 0.0m;
@ -141,14 +143,14 @@ namespace BTCPayServer.Controllers
}
else
{
if (!settings.ShowCustomAmount && !settings.EnableShoppingCart)
if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
return NotFound();
price = amount;
title = settings.Title;
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
if (!string.IsNullOrEmpty(posData) &&
settings.EnableShoppingCart &&
if (!string.IsNullOrEmpty(posData) &&
settings.DefaultView == PosViewType.Cart &&
AppService.TryParsePosCartItems(posData, out var cartItems))
{

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.Apps;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -16,8 +17,8 @@ namespace BTCPayServer.Models.AppViewModels
public string Currency { get; set; }
public string Template { get; set; }
[Display(Name = "Enable shopping cart")]
public bool EnableShoppingCart { get; set; }
[Display(Name = "Default view")]
public PosViewType DefaultView { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
[Display(Name = "User can input discount in %")]

View file

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Models.AppViewModels
{
@ -36,7 +33,7 @@ namespace BTCPayServer.Models.AppViewModels
public CurrencyInfoData CurrencyInfo { get; set; }
public bool EnableShoppingCart { get; set; }
public PosViewType ViewType { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }

View file

@ -1,8 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Apps
{
public enum AppType
@ -10,4 +5,10 @@ namespace BTCPayServer.Services.Apps
PointOfSale,
Crowdfund
}
public enum PosViewType
{
Static,
Cart
}
}

View file

@ -1,4 +1,5 @@
@addTagHelper *, BundlerMinifier.TagHelpers
@using BTCPayServer.Services.Apps
@addTagHelper *, BundlerMinifier.TagHelpers
@model UpdatePointOfSaleViewModel
@{
ViewData["Title"] = "Update Point of Sale";
@ -35,10 +36,10 @@
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="EnableShoppingCart" type="checkbox" class="form-check-input" />
<label asp-for="EnableShoppingCart" class="form-check-label"></label>
<span asp-validation-for="EnableShoppingCart" class="text-danger"></span>
<div class="form-group">
<label asp-for="DefaultView" class="control-label"></label>*
<select asp-for="DefaultView" asp-items="@Html.GetEnumSelectList<PosViewType>()" class="form-control"></select>
<span asp-validation-for="DefaultView" class="text-danger"></span>
</div>
<div class="form-check">
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check-input" />

View file

@ -0,0 +1,314 @@
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
Layout = "_LayoutPos";
int[] customTipPercentages = Model.CustomTipPercentages;
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
}
<script id="template-cart-item" type="text/template">
<tr data-id="{id}">
{image}
<td class="align-middle pr-0 pl-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">
<div class="input-group-prepend">
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
</div>
<input class="js-cart-item-count form-control form-control-sm pull-left" type="number" step="1" name="count" placeholder="Qty" max="{inventory}" value="{count}" data-prev="{count}">
<div class="input-group-append">
<a class="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>
<td class="align-middle text-right">{price}</td>
</tr>
</script>
<script id="template-cart-item-image" type="text/template">
<td class="align-middle pr-0" width="1%"><img src="{image}" width="50" alt=""></td>
</script>
<script id="template-cart-custom-amount" type="text/template">
<tr>
<td colspan="5">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<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-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
</script>
<script id="template-cart-extra" type="text/template">
@if(Model.ShowCustomAmount){
<tr>
<th colspan="5" class="border-0 pb-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
<div class="input-group-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</th>
</tr>
}
@if (@Model.ShowDiscount)
{
<tr>
<th colspan="5" class="border-top-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
</div>
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
<div class="input-group-append">
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</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">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
</div>
<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)">
<div class="input-group-append">
<a class="js-cart-tip-remove btn btn-lg btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="row mb-1">
@if (customTipPercentages != null && customTipPercentages.Length > 0)
{
@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 btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
</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-right">
<span class="js-cart-total">{total}</span>
</th>
</tr>
</script>
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
<i class="fa fa-times fa-fw"></i>
</span>
</button>
</div>
<div class="modal-body p-0">
<table id="js-cart-summary" class="table m-0">
<tbody class="my-3">
<tr>
<td colspan="2" class="border-top-0 h5">Summary</td>
</tr>
<tr>
<td class="border-0 pb-0 h6">Total products</td>
<td align="right" class="border-0 pb-0 h6">
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
@if (Model.ShowDiscount)
{
<tr>
<td class="border-0 pb-y h6">Discount</td>
<td align="right" class="border-0 pb-y h6">
<span class="js-cart-summary-discount text-nowrap"></span>
</td>
</tr>
}
@if (Model.EnableTips)
{
<tr>
<td class="border-top-0 pt-0 h6">Tip</td>
<td align="right" class="border-top-0 pt-0 h6">
<span class="js-cart-summary-tip text-nowrap"></span>
</td>
</tr>
}
<tr>
<td class="h3 table-light">Total</td>
<td class="h3 table-light text-right">
<span class="js-cart-summary-total text-nowrap"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer bg-light">
<form
id="js-cart-pay-form"
method="post"
asp-controller="AppsPublic"
asp-action="ViewPointOfSale"
asp-route-appId="@Model.AppId"
asp-antiforgery="false"
data-buy
>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit">
<b>@Model.CustomButtonText</b>
</button>
</form>
</div>
</div>
</div>
</div>
<div class="wrapper">
<!-- Page Content -->
<div id="content">
<div class="p-2 p-sm-4">
<div class="row">
<div class="col-sm-4 col-lg-2 order-sm-last text-right mb-2">
<a class="js-cart btn btn-lg btn-outline-primary" href="#">
<i class="fa fa-shopping-basket"></i>&nbsp;
<span class="badge badge-light badge-pill">
<span id="js-cart-items">0</span>
</span>
</a>
</div>
<div class="col-sm-8 col-lg-10 mb-2">
<div class="mb-2 position-relative">
<input type="text" class="js-search form-control form-control-lg" placeholder="Find product">
<a class="js-search-reset btn btn-lg btn-link text-black" href="#">
<i class="fa fa-times-circle fa-lg"></i>
</a>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="row">
<div class="overflow-hidden col-12">@Safe.Raw(Model.Description)</div>
</div>
}
</div>
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row card-deck my-3">
@for (var index = 0; index < Model.Items.Length; index++)
{
var item = Model.Items[index];
var image = item.Image;
var description = item.Description;
<div class="js-add-cart card my-2 card-wrapper" data-index="@index">
@if (!String.IsNullOrWhiteSpace(image))
{
@:<img class="card-img-top" src="@image" alt="Card image cap" asp-append-version="true">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
</div>
<div class="card-footer pt-0 bg-transparent border-0">
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
@if (item.Inventory.HasValue)
{
<div class="w-100 pt-2 text-center text-muted">
@if (item.Inventory > 0)
{
<span>@item.Inventory left</span>
}
else
{
<span>Sold out</span>
}
</div>
} else if (anyInventoryItems)
{
<div class="w-100 pt-2">&nbsp;</div>
}
</div>
</div>
}
</div>
</div>
</div>
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark text-white">
<div class="bg-primary p-3 clearfix">
<h3 class="text-white m-0 pull-left">Cart</h3>
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#">
<i class="fa fa-times fa-lg"></i>
</a>
<a class="js-cart-destroy btn btn-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
</div>
<table id="js-cart-list" class="table table-responsive table-light mt-0 mb-0">
<thead>
<tr>
<th colspan="3" width="55%">Product</th>
<th class="text-center" width="20%">
<div style="width: 84px">Quantity</div>
</th>
<th class="text-right" width="25%">
<div style="min-width: 50px">Price</div>
</th>
</tr>
</thead>
<tbody></tbody>
</table>
<table id="js-cart-extra" class="table table-light mt-0 mb-0">
<thead></thead>
</table>
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit">
<b>Confirm</b>
</button>
<div class="text-center mb-5 pb-5">
<svg class="logo" viewBox="0 0 192 84" xmlns="http://www.w3.org/2000/svg"><g><path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/><path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/><path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/><path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/><path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/><path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" class="logo-brand-text"/></g></svg>
</div>
</nav>
</div>

View file

@ -0,0 +1,113 @@
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
Layout = "_LayoutPos";
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
}
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="row">
<div class="overflow-hidden col-12">@Safe.Raw(Model.Description)</div>
</div>
}
<div class="row card-deck my-3 mx-auto">
@for (int x = 0; x < Model.Items.Length; x++)
{
var item = Model.Items[x];
<div class="card my-2" data-id="@x">
@if (!String.IsNullOrWhiteSpace(item.Image))
{
<img class="card-img-top" src="@item.Image" alt="Card image cap" asp-append-version="true">
}
<div class="card-body pb-0">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(item.Description))
{
<p class="card-text">@System.Net.WebUtility.HtmlDecode(item.Description)</p>
}
</div>
<div class="card-footer bg-transparent border-0">
@if (!item.Inventory.HasValue || item.Inventory.Value > 0)
{
<div class="w-100">
@if (item.Custom)
{
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
}
else
{
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)
</button>
</form>
}
</div>
}
@if (item.Inventory.HasValue)
{
<div class="w-100 pt-2 text-center text-muted">
@if (item.Inventory > 0)
{
<span>@item.Inventory left</span>
}
else
{
<span>Sold out</span>
}
</div>
}else if (anyInventoryItems)
{
<div class="w-100 pt-2">&nbsp;</div>
}
</div>
</div>
}
@if (Model.ShowCustomAmount)
{
<div class="card my-2">
<div class="card-body my-auto pb-0">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
</div>
<div class="card-footer bg-transparent border-0">
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
@if (anyInventoryItems)
{
<div class="w-100 pt-2">&nbsp;</div>
}
</div>
</div>
}
</div>
</div>
</div>

View file

@ -1,509 +0,0 @@
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
int[] CustomTipPercentages = Model.CustomTipPercentages;
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
}
<!DOCTYPE html>
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<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="~/manifest.json">
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" asp-append-version="true" />
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.ThemeUri)" rel="stylesheet" asp-append-version="true" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" asp-append-version="true" />
@if (Model.EnableShoppingCart)
{
<link rel="stylesheet" href="~/cart/css/style.css" asp-append-version="true">
<script type="text/javascript">
var srvModel = @Safe.Json(Model);
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" asp-append-version="true" />
}
<style>
.card-deck {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: .5rem;
}
.card-deck .card:only-of-type {
max-width: 320px;
margin: auto;
}
.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;
}
.js-cart-item-count {
-moz-appearance:textfield;
margin: 0;
text-align: right;
}
.js-cart-item-count::-webkit-inner-spin-button,
.js-cart-item-count::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
</style>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>");
}
</head>
<body class="h-100">
<script id="template-cart-item" type="text/template">
<tr data-id="{id}">
{image}
<td class="align-middle pr-0 pl-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">
<div class="input-group-prepend">
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
</div>
<input class="js-cart-item-count form-control form-control-sm pull-left" type="number" step="1" name="count" placeholder="Qty" max="{inventory}" value="{count}" data-prev="{count}">
<div class="input-group-append">
<a class="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>
<td class="align-middle text-right">{price}</td>
</tr>
</script>
<script id="template-cart-item-image" type="text/template">
<td class="align-middle pr-0" width="1%"><img src="{image}" width="50"></td>
</script>
<script id="template-cart-custom-amount" type="text/template">
<tr>
<td colspan="5">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<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-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
</script>
<script id="template-cart-extra" type="text/template">
@if(Model.ShowCustomAmount){
<tr>
<th colspan="5" class="border-0 pb-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
<div class="input-group-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</th>
</tr>
}
@if (@Model.ShowDiscount)
{
<tr>
<th colspan="5" class="border-top-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
</div>
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
<div class="input-group-append">
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</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">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
</div>
<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)">
<div class="input-group-append">
<a class="js-cart-tip-remove btn btn-lg btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="row mb-1">
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0)
{
@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 btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
</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-right">
<span class="js-cart-total">{total}</span>
</th>
</tr>
</script>
@if (this.TempData.HasStatusMessage())
{
<partial name="_StatusMessage" />
}
@if (Model.EnableShoppingCart)
{
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
<i class="fa fa-times fa-fw"></i>
</span>
</button>
</div>
<div class="modal-body p-0">
<table id="js-cart-summary" class="table m-0">
<tbody class="my-3">
<tr>
<td colspan="2" class="border-top-0 h5">Summary</td>
</tr>
<tr>
<td class="border-0 pb-0 h6">Total products</td>
<td align="right" class="border-0 pb-0 h6">
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
@if (Model.ShowDiscount)
{
<tr>
<td class="border-0 pb-y h6">Discount</td>
<td align="right" class="border-0 pb-y h6">
<span class="js-cart-summary-discount text-nowrap"></span>
</td>
</tr>
}
@if (Model.EnableTips)
{
<tr>
<td class="border-top-0 pt-0 h6">Tip</td>
<td align="right" class="border-top-0 pt-0 h6">
<span class="js-cart-summary-tip text-nowrap"></span>
</td>
</tr>
}
<tr>
<td class="h3 table-light">Total</td>
<td class="h3 table-light text-right">
<span class="js-cart-summary-total text-nowrap"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer bg-light">
<form
id="js-cart-pay-form"
method="post"
asp-controller="AppsPublic"
asp-action="ViewPointOfSale"
asp-route-appId="@Model.AppId"
asp-antiforgery="false"
data-buy
>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit">
<b>@Model.CustomButtonText</b>
</button>
</form>
</div>
</div>
</div>
</div>
<div class="wrapper">
<!-- Page Content -->
<div id="content">
<div class="p-2 p-sm-4">
<div class="row">
<div class="col-sm-4 col-lg-2 order-sm-last text-right mb-2">
<a class="js-cart btn btn-lg btn-outline-primary" href="#">
<i class="fa fa-shopping-basket"></i>&nbsp;
<span class="badge badge-light badge-pill">
<span id="js-cart-items">0</span>
</span>
</a>
</div>
<div class="col-sm-8 col-lg-10 mb-2">
<div class="mb-2 position-relative">
<input type="text" class="js-search form-control form-control-lg" placeholder="Find product">
<a class="js-search-reset btn btn-lg btn-link text-black" href="#">
<i class="fa fa-times-circle fa-lg"></i>
</a>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="row">
<div class="overflow-hidden col-12">@Safe.Raw(Model.Description)</div>
</div>
}
</div>
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row card-deck my-3">
@for (var index = 0; index < Model.Items.Length; index++)
{
var item = Model.Items[index];
var image = item.Image;
var description = item.Description;
<div class="js-add-cart card my-2 card-wrapper" data-index="@index">
@if (!String.IsNullOrWhiteSpace(image))
{
@:<img class="card-img-top" src="@image" alt="Card image cap" asp-append-version="true">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
</div>
<div class="card-footer pt-0 bg-transparent border-0">
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
@if (item.Inventory.HasValue)
{
<div class="w-100 pt-2 text-center text-muted">
@if (item.Inventory > 0)
{
<span>@item.Inventory left</span>
}
else
{
<span>Sold out</span>
}
</div>
}else if (anyInventoryItems)
{
<div class="w-100 pt-2">&nbsp</div>
}
</div>
</div>
}
</div>
</div>
</div>
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark text-white">
<div class="bg-primary p-3 clearfix">
<h3 class="text-white m-0 pull-left">Cart</h3>
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#">
<i class="fa fa-times fa-lg"></i>
</a>
<a class="js-cart-destroy btn btn-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
</div>
<table id="js-cart-list" class="table table-responsive table-light mt-0 mb-0">
<thead>
<tr>
<th colspan="3" width="55%">Product</th>
<th class="text-center" width="20%">
<div style="width: 84px">Quantity</div>
</th>
<th class="text-right" width="25%">
<div style="min-width: 50px">Price</div>
</th>
</tr>
</thead>
<tbody></tbody>
</table>
<table id="js-cart-extra" class="table table-light mt-0 mb-0">
<thead></thead>
</table>
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit">
<b>Confirm</b>
</button>
<div class="text-center mb-5 pb-5">
<svg class="logo" viewBox="0 0 192 84" xmlns="http://www.w3.org/2000/svg"><g><path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="#CEDC21" class="logo-brand-light"/><path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="#51B13E" class="logo-brand-medium"/><path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="#CEDC21" class="logo-brand-light"/><path d="M10.066 31.725v20.553L24.01 42.006z" fill="#1E7A44" class="logo-brand-dark"/><path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="#CEDC21" class="logo-brand-light"/><path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" class="logo-brand-text"/></g></svg>
</div>
</nav>
</div>
}
else
{
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="row">
<div class="overflow-hidden col-12">@Safe.Raw(Model.Description)</div>
</div>
}
<div class="row card-deck my-3 mx-auto">
@for (int x = 0; x < Model.Items.Length; x++)
{
var item = Model.Items[x];
<div class="card my-2" data-id="@x">
@if (!String.IsNullOrWhiteSpace(item.Image))
{
<img class="card-img-top" src="@item.Image" alt="Card image cap" asp-append-version="true">
}
<div class="card-body pb-0">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(item.Description))
{
<p class="card-text">@System.Net.WebUtility.HtmlDecode(item.Description)</p>
}
</div>
<div class="card-footer bg-transparent border-0">
@if (!item.Inventory.HasValue || item.Inventory.Value > 0)
{
<div class="w-100">
@if (item.Custom)
{
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
}
else
{
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)
</button>
</form>
}
</div>
}
@if (item.Inventory.HasValue)
{
<div class="w-100 pt-2 text-center text-muted">
@if (item.Inventory > 0)
{
<span>@item.Inventory left</span>
}
else
{
<span>Sold out</span>
}
</div>
}else if (anyInventoryItems)
{
<div class="w-100 pt-2">&nbsp</div>
}
</div>
</div>
}
@if (Model.ShowCustomAmount)
{
<div class="card my-2">
<div class="card-body my-auto pb-0">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
</div>
<div class="card-footer bg-transparent border-0">
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
@if (anyInventoryItems)
{
<div class="w-100 pt-2">&nbsp</div>
}
</div>
</div>
}
</div>
</div>
</div>
}
</body>
</html>

View file

@ -0,0 +1,90 @@
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@using BTCPayServer.Services.Apps
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
}
<!DOCTYPE html>
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<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="~/manifest.json">
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" asp-append-version="true" />
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.ThemeUri)" rel="stylesheet" asp-append-version="true" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" asp-append-version="true" />
@if (Model.ViewType == PosViewType.Cart)
{
<link rel="stylesheet" href="~/cart/css/style.css" asp-append-version="true">
<script type="text/javascript">
var srvModel = @Safe.Json(Model);
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" asp-append-version="true" />
}
<style>
.card-deck {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: .5rem;
}
.card-deck .card:only-of-type {
max-width: 320px;
margin: auto;
}
.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;
}
.js-cart-item-count {
-moz-appearance:textfield;
margin: 0;
text-align: right;
}
.js-cart-item-count::-webkit-inner-spin-button,
.js-cart-item-count::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
</style>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>");
}
</head>
<body class="h-100">
@if (this.TempData.HasStatusMessage())
{
<partial name="_StatusMessage" />
}
@RenderBody()
</body>
</html>