mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
LNURL POS Support (#3019)
This commit is contained in:
@@ -131,7 +131,14 @@ namespace BTCPayServer.Tests
|
|||||||
var element = driver.FindElement(selector);
|
var element = driver.FindElement(selector);
|
||||||
if ((value && !element.Selected) || (!value && element.Selected))
|
if ((value && !element.Selected) || (!value && element.Selected))
|
||||||
{
|
{
|
||||||
driver.WaitForAndClick(selector);
|
try
|
||||||
|
{
|
||||||
|
driver.WaitForAndClick(selector);
|
||||||
|
}
|
||||||
|
catch (ElementClickInterceptedException)
|
||||||
|
{
|
||||||
|
element.SendKeys(" ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value != element.Selected)
|
if (value != element.Selected)
|
||||||
|
|||||||
@@ -316,7 +316,10 @@ namespace BTCPayServer.Tests
|
|||||||
Driver.FindElement(By.Id("Password")).SendKeys(password);
|
Driver.FindElement(By.Id("Password")).SendKeys(password);
|
||||||
Driver.FindElement(By.Id("LoginButton")).Click();
|
Driver.FindElement(By.Id("LoginButton")).Click();
|
||||||
}
|
}
|
||||||
|
public void GoToApps()
|
||||||
|
{
|
||||||
|
Driver.FindElement(By.Id("Apps")).Click();
|
||||||
|
}
|
||||||
public void GoToStores()
|
public void GoToStores()
|
||||||
{
|
{
|
||||||
Driver.FindElement(By.Id("Stores")).Click();
|
Driver.FindElement(By.Id("Stores")).Click();
|
||||||
|
|||||||
@@ -1218,7 +1218,50 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Contains(bolt, s.Driver.PageSource);
|
Assert.Contains(bolt, s.Driver.PageSource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Selenium", "Selenium")]
|
||||||
|
[Trait("Lightning", "Lightning")]
|
||||||
|
public async Task CanUsePOSPrint()
|
||||||
|
{
|
||||||
|
using var s = SeleniumTester.Create();
|
||||||
|
s.Server.ActivateLightning();
|
||||||
|
await s.StartAsync();
|
||||||
|
|
||||||
|
await s.Server.EnsureChannelsSetup();
|
||||||
|
|
||||||
|
s.RegisterNewUser(true);
|
||||||
|
var store = s.CreateNewStore();
|
||||||
|
var network = s.Server.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork;
|
||||||
|
s.GoToStore(store.storeId);
|
||||||
|
s.AddLightningNode("BTC", LightningConnectionType.CLightning, false);
|
||||||
|
s.Driver.FindElement(By.Id($"Modify-LightningBTC")).Click();
|
||||||
|
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
|
||||||
|
s.GoToApps();
|
||||||
|
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("SelectedAppType")).Click();
|
||||||
|
s.Driver.FindElement(By.CssSelector("option[value='PointOfSale']")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("Name")).SendKeys(Guid.NewGuid().ToString());
|
||||||
|
s.Driver.FindElement(By.Id("Create")).Click();
|
||||||
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||||
|
s.Driver.FindElement(By.Id("DefaultView")).Click();
|
||||||
|
s.Driver.FindElement(By.CssSelector("option[value='3']")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||||
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||||
|
var btns = s.Driver.FindElements(By.ClassName("lnurl"));
|
||||||
|
foreach (IWebElement webElement in btns)
|
||||||
|
{
|
||||||
|
var choice = webElement.GetAttribute("data-choice");
|
||||||
|
var lnurl = webElement.GetAttribute("href");
|
||||||
|
var parsed = LNURL.LNURL.Parse(lnurl, out _);
|
||||||
|
Assert.True(parsed.ToString().EndsWith(choice));
|
||||||
|
Assert.IsType<LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Selenium", "Selenium")]
|
[Trait("Selenium", "Selenium")]
|
||||||
[Trait("Lightning", "Lightning")]
|
[Trait("Lightning", "Lightning")]
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<PackageReference Include="Fido2" Version="2.0.1" />
|
<PackageReference Include="Fido2" Version="2.0.1" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||||
<PackageReference Include="LNURL" Version="0.0.13" />
|
<PackageReference Include="LNURL" Version="0.0.14" />
|
||||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="3.3.2">
|
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="3.3.2">
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ namespace BTCPayServer.Controllers
|
|||||||
CustomCSSLink = settings.CustomCSSLink,
|
CustomCSSLink = settings.CustomCSSLink,
|
||||||
CustomLogoLink = storeBlob.CustomLogo,
|
CustomLogoLink = storeBlob.CustomLogo,
|
||||||
AppId = appId,
|
AppId = appId,
|
||||||
|
Store = store,
|
||||||
Description = settings.Description,
|
Description = settings.Description,
|
||||||
EmbeddedCSS = settings.EmbeddedCSS,
|
EmbeddedCSS = settings.EmbeddedCSS,
|
||||||
RequiresRefundEmail = settings.RequiresRefundEmail
|
RequiresRefundEmail = settings.RequiresRefundEmail
|
||||||
|
|||||||
@@ -2,9 +2,14 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
@@ -18,7 +23,9 @@ using BTCPayServer.Services.Apps;
|
|||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using LNURL;
|
using LNURL;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Crypto;
|
using NBitcoin.Crypto;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -35,6 +42,8 @@ namespace BTCPayServer
|
|||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
private readonly AppService _appService;
|
private readonly AppService _appService;
|
||||||
private readonly InvoiceController _invoiceController;
|
private readonly InvoiceController _invoiceController;
|
||||||
|
private readonly SettingsRepository _settingsRepository;
|
||||||
|
private readonly LinkGenerator _linkGenerator;
|
||||||
|
|
||||||
public LNURLController(InvoiceRepository invoiceRepository,
|
public LNURLController(InvoiceRepository invoiceRepository,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
@@ -42,7 +51,9 @@ namespace BTCPayServer
|
|||||||
LightningLikePaymentHandler lightningLikePaymentHandler,
|
LightningLikePaymentHandler lightningLikePaymentHandler,
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
AppService appService,
|
AppService appService,
|
||||||
InvoiceController invoiceController)
|
InvoiceController invoiceController,
|
||||||
|
SettingsRepository settingsRepository,
|
||||||
|
LinkGenerator linkGenerator)
|
||||||
{
|
{
|
||||||
_invoiceRepository = invoiceRepository;
|
_invoiceRepository = invoiceRepository;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
@@ -51,8 +62,163 @@ namespace BTCPayServer
|
|||||||
_storeRepository = storeRepository;
|
_storeRepository = storeRepository;
|
||||||
_appService = appService;
|
_appService = appService;
|
||||||
_invoiceController = invoiceController;
|
_invoiceController = invoiceController;
|
||||||
|
_settingsRepository = settingsRepository;
|
||||||
|
_linkGenerator = linkGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("pay/app/{appId}/{itemCode}")]
|
||||||
|
public async Task<IActionResult> GetLNURLForApp(string cryptoCode, string appId, string itemCode = null)
|
||||||
|
{
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
|
if (network is null || !network.SupportLightning)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = await _appService.GetApp(appId, null, true);
|
||||||
|
if (app is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = app.StoreData;
|
||||||
|
if (store is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(itemCode))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
||||||
|
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||||
|
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
|
||||||
|
var lnUrlMethod =
|
||||||
|
methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod;
|
||||||
|
var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi);
|
||||||
|
if (lnUrlMethod is null || lnMethod is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewPointOfSaleViewModel.Item[] items = { };
|
||||||
|
string currencyCode = null;
|
||||||
|
switch (app.AppType)
|
||||||
|
{
|
||||||
|
case nameof(AppType.Crowdfund):
|
||||||
|
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||||
|
currencyCode = cfS.TargetCurrency;
|
||||||
|
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
|
||||||
|
break;
|
||||||
|
case nameof(AppType.PointOfSale):
|
||||||
|
var posS = app.GetSettings<AppsController.PointOfSaleSettings>();
|
||||||
|
currencyCode = posS.Currency;
|
||||||
|
items = _appService.Parse(posS.Template, posS.Currency);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = items.FirstOrDefault(item1 =>
|
||||||
|
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (item is null ||
|
||||||
|
item.Inventory <= 0 ||
|
||||||
|
(item.PaymentMethods?.Any() is true &&
|
||||||
|
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null,
|
||||||
|
() => (null, new List<string> { AppService.GetAppInternalTag(appId) }, item.Price.Value, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("pay")]
|
||||||
|
public async Task<IActionResult> GetLNURL(string cryptoCode, string storeId, string currencyCode = null,
|
||||||
|
decimal? min = null, decimal? max = null,
|
||||||
|
Func<(string username, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice)>
|
||||||
|
internalDetails = null)
|
||||||
|
{
|
||||||
|
currencyCode ??= cryptoCode;
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
|
if (network is null || !network.SupportLightning)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = await _storeRepository.FindStore(storeId);
|
||||||
|
if (store is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
||||||
|
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||||
|
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
|
||||||
|
var lnUrlMethod =
|
||||||
|
methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod;
|
||||||
|
var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi);
|
||||||
|
if (lnUrlMethod is null || lnMethod is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var blob = store.GetStoreBlob();
|
||||||
|
if (blob.GetExcludedPaymentMethods().Match(pmi) || blob.GetExcludedPaymentMethods().Match(lnpmi))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
(string username, List<string> additionalTags, decimal? invoiceAmount, bool? anyoneCanInvoice) =
|
||||||
|
(internalDetails ?? (() => (null, null, null, null)))();
|
||||||
|
|
||||||
|
if ((anyoneCanInvoice ?? blob.AnyoneCanInvoice) is false)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string[]> lnurlMetadata = new List<string[]>();
|
||||||
|
|
||||||
|
var i = await _invoiceController.CreateInvoiceCoreRaw(
|
||||||
|
new CreateInvoiceRequest
|
||||||
|
{
|
||||||
|
Amount = invoiceAmount,
|
||||||
|
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||||
|
{
|
||||||
|
PaymentMethods = new[] { pmi.ToStringNormalized() },
|
||||||
|
Expiration = blob.InvoiceExpiration < TimeSpan.FromMinutes(2)
|
||||||
|
? blob.InvoiceExpiration
|
||||||
|
: TimeSpan.FromMinutes(2)
|
||||||
|
},
|
||||||
|
Currency = currencyCode,
|
||||||
|
Type = invoiceAmount is null ? InvoiceType.TopUp : InvoiceType.Standard,
|
||||||
|
}, store, Request.GetAbsoluteUri(""), additionalTags);
|
||||||
|
if (i.Type != InvoiceType.TopUp)
|
||||||
|
{
|
||||||
|
min = i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi);
|
||||||
|
max = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
lnurlMetadata.Add(new[] { "text/plain", i.Id });
|
||||||
|
return Ok(new LNURLPayRequest
|
||||||
|
{
|
||||||
|
Tag = "payRequest",
|
||||||
|
MinSendable = new LightMoney(min ?? 1m, LightMoneyUnit.Satoshi),
|
||||||
|
MaxSendable =
|
||||||
|
max is null
|
||||||
|
? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC)
|
||||||
|
: new LightMoney(max.Value, LightMoneyUnit.Satoshi),
|
||||||
|
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0,
|
||||||
|
Metadata = JsonConvert.SerializeObject(lnurlMetadata),
|
||||||
|
Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||||
|
action: nameof(GetLNURLForInvoice),
|
||||||
|
controller: "LNURL",
|
||||||
|
values: new { cryptoCode, invoiceId = i.Id }, Request.Scheme, Request.Host, Request.PathBase))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("pay/i/{invoiceId}")]
|
[HttpGet("pay/i/{invoiceId}")]
|
||||||
public async Task<IActionResult> GetLNURLForInvoice(string invoiceId, string cryptoCode,
|
public async Task<IActionResult> GetLNURLForInvoice(string invoiceId, string cryptoCode,
|
||||||
[FromQuery] long? amount = null, string comment = null)
|
[FromQuery] long? amount = null, string comment = null)
|
||||||
@@ -137,7 +303,7 @@ namespace BTCPayServer
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return BadRequest(new LNUrlStatusResponse
|
return BadRequest(new LNUrlStatusResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.AppViewModels
|
namespace BTCPayServer.Models.AppViewModels
|
||||||
@@ -65,5 +66,7 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
[Display(Name = "Custom CSS Code")]
|
[Display(Name = "Custom CSS Code")]
|
||||||
public string EmbeddedCSS { get; set; }
|
public string EmbeddedCSS { get; set; }
|
||||||
public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore;
|
public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore;
|
||||||
|
|
||||||
|
public StoreData Store { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ namespace BTCPayServer.Services.Apps
|
|||||||
[Display(Name = "Item list and cart")]
|
[Display(Name = "Item list and cart")]
|
||||||
Cart,
|
Cart,
|
||||||
[Display(Name = "Keypad only")]
|
[Display(Name = "Keypad only")]
|
||||||
Light
|
Light,
|
||||||
|
Print
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RequiresRefundEmail
|
public enum RequiresRefundEmail
|
||||||
|
|||||||
122
BTCPayServer/Views/AppsPublic/PointOfSale/Print.cshtml
Normal file
122
BTCPayServer/Views/AppsPublic/PointOfSale/Print.cshtml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
@using BTCPayServer.Models.AppViewModels
|
||||||
|
@using BTCPayServer.Payments.Lightning
|
||||||
|
@using LNURL
|
||||||
|
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||||
|
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
Context.Request.Query.TryGetValue("cryptocode", out var cryptoCodeValues);
|
||||||
|
var cryptoCode = cryptoCodeValues.FirstOrDefault() ?? "BTC";
|
||||||
|
Layout = "_LayoutPos";
|
||||||
|
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
|
||||||
|
var supported = Model.Store.GetSupportedPaymentMethods(BTCPayNetworkProvider).OfType<LNURLPaySupportedPaymentMethod>().OrderBy(method => method.CryptoCode == cryptoCode).FirstOrDefault();
|
||||||
|
if (supported != null && !Model.Store.GetEnabledPaymentIds(BTCPayNetworkProvider).Contains(supported.PaymentId))
|
||||||
|
{
|
||||||
|
supported = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
@if (supported is null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert">
|
||||||
|
LNURL is not enabled on your store, which this print feature relies on.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<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 m-auto">
|
||||||
|
<partial name="_StatusMessage" />
|
||||||
|
|
||||||
|
<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="card-deck my-3 mx-auto">
|
||||||
|
@for (int x = 0; x < Model.Items.Length; x++)
|
||||||
|
{
|
||||||
|
var item = Model.Items[x];
|
||||||
|
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
|
||||||
|
buttonText = buttonText.Replace("{0}",item.Price.Formatted)
|
||||||
|
?.Replace("{Price}",item.Price.Formatted);
|
||||||
|
|
||||||
|
<div class="card px-0" data-id="@x">
|
||||||
|
@if (!String.IsNullOrWhiteSpace(item.Image))
|
||||||
|
{
|
||||||
|
<img class="card-img-top" src="@item.Image" asp-append-version="true">
|
||||||
|
}
|
||||||
|
@{CardBody(item.Title, item.Description);}
|
||||||
|
<div class="card-footer bg-transparent border-0 pb-3">
|
||||||
|
<div class="w-100 pt-2 text-center text-muted">
|
||||||
|
@switch (item.Price.Type)
|
||||||
|
{
|
||||||
|
case ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup:
|
||||||
|
<span>Any amount</span>
|
||||||
|
break;
|
||||||
|
case ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum:
|
||||||
|
<span>@item.Price.Formatted minimum</span>
|
||||||
|
break;
|
||||||
|
case ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed:
|
||||||
|
@item.Price.Formatted
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!item.Inventory.HasValue || item.Inventory.Value > 0)
|
||||||
|
{
|
||||||
|
if (supported != null)
|
||||||
|
{
|
||||||
|
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForApp", "LNURL", new
|
||||||
|
{
|
||||||
|
cryptoCode = supported.CryptoCode,
|
||||||
|
appid = Model.AppId,
|
||||||
|
ItemCode = item.Id
|
||||||
|
}, Context.Request.Scheme, Context.Request.Host.ToString()));
|
||||||
|
var lnUrl = LNURL.EncodeUri(lnurlEndpoint, "payRequest", supported.UseBech32Scheme);
|
||||||
|
<vc:qr-code data="@lnUrl.ToString().ToUpperInvariant()"></vc:qr-code>
|
||||||
|
|
||||||
|
<a href="@lnUrl" class="btn btn-secondary d-print-none w-100 mt-2 lnurl" data-choice="@item.Id" rel="noreferrer noopener">
|
||||||
|
Open in wallet
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@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"> </div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
private void CardBody(string title, string description)
|
||||||
|
{
|
||||||
|
<div class="card-body my-auto pb-0">
|
||||||
|
<h5 class="card-title">@title</h5>
|
||||||
|
@if (!String.IsNullOrWhiteSpace(description))
|
||||||
|
{
|
||||||
|
<p class="card-text">@Safe.Raw(description)</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user