Support Topup Invoices in Apps

This commit is contained in:
Kukks 2021-10-08 13:11:00 +02:00 committed by Andrew Camilleri
parent 9df4429fc2
commit 7d2aa28e1f
12 changed files with 129 additions and 89 deletions

View file

@ -114,7 +114,7 @@ namespace BTCPayServer.Controllers
[DomainMappingConstraint(AppType.PointOfSale)] [DomainMappingConstraint(AppType.PointOfSale)]
public async Task<IActionResult> ViewPointOfSale(string appId, public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType, PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, [ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount,
string email, string email,
string orderId, string orderId,
string notificationUrl, string notificationUrl,
@ -136,7 +136,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType }); return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
} }
string title = null; string title = null;
var price = 0.0m; decimal? price = null;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null; Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null; ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey)) if (!string.IsNullOrEmpty(choiceKey))
@ -146,9 +146,17 @@ namespace BTCPayServer.Controllers
if (choice == null) if (choice == null)
return NotFound(); return NotFound();
title = choice.Title; title = choice.Title;
price = choice.Price.Value; if (choice.Custom == "topup")
if (amount > price) {
price = amount; price = null;
}
else
{
price = choice.Price.Value;
if (amount > price)
price = amount;
}
if (choice.Inventory.HasValue) if (choice.Inventory.HasValue)
{ {
@ -277,10 +285,7 @@ namespace BTCPayServer.Controllers
[DomainMappingConstraintAttribute(AppType.Crowdfund)] [DomainMappingConstraintAttribute(AppType.Crowdfund)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken) public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{ {
if (request.Amount <= 0)
{
return NotFound("Please provide an amount greater than 0");
}
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true); var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null) if (app == null)
@ -307,7 +312,7 @@ namespace BTCPayServer.Controllers
var store = await _AppService.GetStore(app); var store = await _AppService.GetStore(app);
var title = settings.Title; var title = settings.Title;
var price = request.Amount; decimal? price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null; Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null; ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey)) if (!string.IsNullOrEmpty(request.ChoiceKey))
@ -317,11 +322,17 @@ namespace BTCPayServer.Controllers
if (choice == null) if (choice == null)
return NotFound("Incorrect option provided"); return NotFound("Incorrect option provided");
title = choice.Title; title = choice.Title;
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
if (choice.Custom == "topup")
{
price = null;
}
else
{
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (choice.Inventory.HasValue) if (choice.Inventory.HasValue)
{ {
if (choice.Inventory <= 0) if (choice.Inventory <= 0)
@ -329,14 +340,21 @@ namespace BTCPayServer.Controllers
return NotFound("Option was out of stock"); return NotFound("Option was out of stock");
} }
} }
if (choice?.PaymentMethods?.Any() is true) if (choice?.PaymentMethods?.Any() is true)
{ {
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s, paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true }); s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
} }
} }
else
{
if (request.Amount < 0)
{
return NotFound("Please provide an amount greater than 0");
}
price = null;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price > if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount)))) (info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))

View file

@ -83,7 +83,7 @@ namespace BTCPayServer.Models.AppViewModels
public class ContributeToCrowdfund public class ContributeToCrowdfund
{ {
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; } public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
[Required] public decimal Amount { get; set; } [Required] public decimal? Amount { get; set; }
public string Email { get; set; } public string Email { get; set; }
public string ChoiceKey { get; set; } public string ChoiceKey { get; set; }
public bool RedirectToCheckout { get; set; } public bool RedirectToCheckout { get; set; }

View file

@ -17,7 +17,7 @@ namespace BTCPayServer.Models.AppViewModels
public string Image { get; set; } public string Image { get; set; }
public ItemPrice Price { get; set; } public ItemPrice Price { get; set; }
public string Title { get; set; } public string Title { get; set; }
public bool Custom { get; set; } public string Custom { get; set; }
public string BuyButtonText { get; set; } public string BuyButtonText { get; set; }
public int? Inventory { get; set; } = null; public int? Inventory { get; set; } = null;
public string[] PaymentMethods { get; set; } public string[] PaymentMethods { get; set; }

View file

@ -289,7 +289,8 @@ namespace BTCPayServer.Services.Apps
{ {
var itemNode = new YamlMappingNode(); var itemNode = new YamlMappingNode();
itemNode.Add("title", new YamlScalarNode(item.Title)); itemNode.Add("title", new YamlScalarNode(item.Title));
itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant())); if(item.Custom!= "topup")
itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant()));
if (!string.IsNullOrEmpty(item.Description)) if (!string.IsNullOrEmpty(item.Description))
{ {
itemNode.Add("description", new YamlScalarNode(item.Description) itemNode.Add("description", new YamlScalarNode(item.Description)
@ -341,13 +342,16 @@ namespace BTCPayServer.Services.Apps
Id = c.Key, Id = c.Key,
Image = c.GetDetailString("image"), Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key, Title = c.GetDetailString("title") ?? c.Key,
Price = c.GetDetail("price") Custom = c.GetDetailString("custom"),
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice() Price =
{ c.GetDetailString("custom") == "topup"
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture), ? null
Formatted = Currencies.FormatCurrency(cc.Value.Value, currency) : c.GetDetail("price")
}).Single(), .Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
Custom = c.GetDetailString("custom") == "true", {
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = Currencies.FormatCurrency(cc.Value.Value, currency)
}).Single(),
BuyButtonText = c.GetDetailString("buyButtonText"), BuyButtonText = c.GetDetailString("buyButtonText"),
Inventory = string.IsNullOrEmpty(c.GetDetailString("inventory")) ? (int?)null : int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture), Inventory = string.IsNullOrEmpty(c.GetDetailString("inventory")) ? (int?)null : int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
PaymentMethods = c.GetDetailStringList("payment_methods"), PaymentMethods = c.GetDetailStringList("payment_methods"),

View file

@ -7,7 +7,7 @@
{ {
foreach (var error in errors.Errors) foreach (var error in errors.Errors)
{ {
<br /> <br/>
<span class="text-danger">@error.ErrorMessage</span> <span class="text-danger">@error.ErrorMessage</span>
} }
} }
@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div v-if="!items || items.length === 0" class="col-12 text-center"> <div v-if="!items || items.length === 0" class="col-12 text-center">
No items.<br /> No items.<br/>
<button type="button" class="btn btn-link" v-on:click="editItem(-1)" id="btn-add-first"> <button type="button" class="btn btn-link" v-on:click="editItem(-1)" id="btn-add-first">
Add your first item Add your first item
</button> </button>
@ -46,7 +46,7 @@
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" v-if="editingItem">{{editingItem.index>=0? "Edit" : "Create"}} item</h5> <h5 class="modal-title" v-if="editingItem">{{editingItem.index>=0? "Edit" : "Create"}} item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" ref="close"> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" ref="close">
<vc:icon symbol="close" /> <vc:icon symbol="close"/>
</button> </button>
</div> </div>
<div class="modal-body" v-if="editingItem"> <div class="modal-body" v-if="editingItem">
@ -55,9 +55,15 @@
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-6"> <div class="col-sm-6">
<label class="form-label" data-required>Title</label> <label class="form-label" data-required>Title</label>
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.title" autofocus ref="txtTitle" /> <input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.title" autofocus ref="txtTitle"/>
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<label class="form-label">Price</label>
<select class="form-select" v-model="editingItem.custom">
<option v-for="option in customPriceOptions" :value="option.value">{{option.text}}</option>
</select>
</div>
<div class="col-sm-3" v-show="editingItem.custom !== 'topup'">
<label class="form-label" data-required>Price</label> <label class="form-label" data-required>Price</label>
<input class="form-control mb-2" <input class="form-control mb-2"
inputmode="numeric" inputmode="numeric"
@ -66,18 +72,13 @@
min="0" min="0"
type="number" type="number"
required required
v-model="editingItem.price" ref="txtPrice" /> v-model="editingItem.price" ref="txtPrice"/>
</div>
<div class="col-sm-3">
<label class="form-label">Custom price</label>
<select class="form-select" v-model="editingItem.custom">
<option v-for="option in customPriceOptions" :value="option.value">{{option.text}}</option>
</select>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Image</label> <label class="form-label">Image</label>
<input type="text" class="form-control mb-2" pattern="[^\*#]+" v-model="editingItem.image" ref="txtImage" /> <input type="text" class="form-control mb-2" pattern="[^\*#]+" v-model="editingItem.image" ref="txtImage"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Description</label> <label class="form-label">Description</label>
@ -85,18 +86,18 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Inventory (leave blank to not use inventory feature)</label> <label class="form-label">Inventory (leave blank to not use inventory feature)</label>
<input type="number" min="0" step="1" class="form-control mb-2" v-model="editingItem.inventory" ref="txtInventory" /> <input type="number" min="0" step="1" class="form-control mb-2" v-model="editingItem.inventory" ref="txtInventory"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Id (leave blank to generate from title)</label> <label class="form-label">Id (leave blank to generate from title)</label>
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.id" ref="txtId" /> <input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.id" ref="txtId"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Buy Button Text</label> <label class="form-label">Buy Button Text</label>
<input type="text" id="BuyButtonText" class="form-control mb-2" v-model="editingItem.buyButtonText" ref="txtBuyButtonText" /> <input type="text" id="BuyButtonText" class="form-control mb-2" v-model="editingItem.buyButtonText" ref="txtBuyButtonText"/>
</div> </div>
<div class="form-group d-flex align-items-center"> <div class="form-group d-flex align-items-center">
<input type="checkbox" id="Disabled" class="btcpay-toggle me-2" v-model="editingItem.disabled" /> <input type="checkbox" id="Disabled" class="btcpay-toggle me-2" v-model="editingItem.disabled"/>
<label class="form-label mb-0">Disabled</label> <label class="form-label mb-0">Disabled</label>
</div> </div>
</div> </div>
@ -120,8 +121,9 @@ document.addEventListener("DOMContentLoaded", function () {
items: [], items: [],
editingItem: null, editingItem: null,
customPriceOptions: [ customPriceOptions: [
{ text: 'No', value: false }, { text: 'Fixed', value: false },
{ text: 'Yes', value: true }, { text: 'Minimum', value: true },
{ text: 'Topup', value: 'topup' },
], ],
elementId: "@Model.templateId" elementId: "@Model.templateId"
}, },
@ -222,7 +224,7 @@ document.addEventListener("DOMContentLoaded", function () {
} }
} }
if (price != null || title != null) { if (title != null) {
// Add product to the list // Add product to the list
result.push({ result.push({
id: id, id: id,
@ -230,7 +232,7 @@ document.addEventListener("DOMContentLoaded", function () {
price: price, price: price,
image: image || null, image: image || null,
description: description || '', description: description || '',
custom: custom === "true", custom: custom === "topup"? "topup": custom === "true",
buyButtonText: buyButtonText, buyButtonText: buyButtonText,
inventory: isNaN(inventory)? null: inventory, inventory: isNaN(inventory)? null: inventory,
paymentMethods: paymentMethods, paymentMethods: paymentMethods,
@ -241,13 +243,13 @@ document.addEventListener("DOMContentLoaded", function () {
this.items = result; this.items = result;
}, },
toYml: function(){ toYml: function(){
var template = ''; let template = '';
// Construct template from the product list // Construct template from the product list
for (var key in this.items) { for (const key in this.items) {
var product = this.items[key], const product = this.items[key],
id = product.id, id = product.id,
title = product.title, title = product.title,
price = product.price? product.price : 0, price = product.custom === 'topup'? null : product.price??0,
image = product.image, image = product.image,
description = product.description, description = product.description,
custom = product.custom, custom = product.custom,
@ -255,36 +257,36 @@ document.addEventListener("DOMContentLoaded", function () {
inventory = product.inventory, inventory = product.inventory,
paymentMethods = product.paymentMethods, paymentMethods = product.paymentMethods,
disabled = product.disabled; disabled = product.disabled;
let itemTemplate = id+":\n";
template += id + ':\n' + itemTemplate += ( product.custom === 'topup'? '' : (' price: ' + parseFloat(price).noExponents() + '\n'));
' price: ' + parseFloat(price).noExponents() + '\n' + itemTemplate+= ' title: ' + title + '\n';
' title: ' + title + '\n';
if (description) { if (description) {
template += ' description: "' + description.replaceAll("\n", "<br/>").replaceAll('"', '\\"') + '"\n'; itemTemplate += ' description: "' + description.replaceAll("\n", "<br/>").replaceAll('"', '\\"') + '"\n';
} }
if (image) { if (image) {
template += ' image: ' + image + '\n'; itemTemplate += ' image: ' + image + '\n';
} }
if (inventory) { if (inventory) {
template += ' inventory: ' + inventory + '\n'; itemTemplate += ' inventory: ' + inventory + '\n';
} }
if (custom != null) { if (custom != null) {
template += ' custom: ' + custom + '\n'; itemTemplate += ' custom: ' + (custom === "topup"? '"topup"': custom) + '\n';
} }
if (buyButtonText != null && buyButtonText.length > 0) { if (buyButtonText != null && buyButtonText.length > 0) {
template += ' buyButtonText: ' + buyButtonText + '\n'; itemTemplate += ' buyButtonText: ' + buyButtonText + '\n';
} }
if (disabled != null) { if (disabled != null) {
template += ' disabled: ' + disabled.toString() + '\n'; itemTemplate += ' disabled: ' + disabled.toString() + '\n';
} }
if(paymentMethods != null && paymentMethods.length > 0){ if(paymentMethods != null && paymentMethods.length > 0){
template+= ' payment_methods:\n'; itemTemplate+= ' payment_methods:\n';
for (var method of paymentMethods){ for (var method of paymentMethods){
template+= ' - '+method+'\n'; itemTemplate+= ' - '+method+'\n';
} }
} }
template += '\n'; itemTemplate += '\n';
template+=itemTemplate;
} }
this.getInputElement().val(template); this.getInputElement().val(template);
}, },
@ -337,7 +339,7 @@ document.addEventListener("DOMContentLoaded", function () {
this.errors.push("Image cannot start with \"- \""); this.errors.push("Image cannot start with \"- \"");
} }
if (!this.$refs.txtPrice.checkValidity()) { if (this.editingItem.custom !== "topup" && !this.$refs.txtPrice.checkValidity()) {
this.errors.push("Price must be a valid number"); this.errors.push("Price must be a valid number");
} }
if (!this.$refs.txtTitle.checkValidity()) { if (!this.$refs.txtTitle.checkValidity()) {

View file

@ -19,26 +19,29 @@
<div class="card-body"> <div class="card-body">
<div class="card-title d-flex align-items-center justify-content-between mb-1"> <div class="card-title d-flex align-items-center justify-content-between mb-1">
<label class="h5 d-flex align-items-center"> <label class="h5 d-flex align-items-center">
@if (vm.Started && !vm.Ended && (item.Price.Value > 0 || item.Custom)) @if (vm.Started && !vm.Ended )
{ {
<input type="radio" asp-for="ChoiceKey" value="@item.Id" class="form-check-input mt-0 me-2"/> <input type="radio" asp-for="ChoiceKey" value="@item.Id" class="form-check-input mt-0 me-2"/>
} }
@(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title) @(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
</label> </label>
<span class="text-muted"> <span class="text-muted">
@if (item.Price.Value > 0) @if (item.Price?.Value > 0)
{ {
<span>@item.Price.Value</span> <span>@item.Price.Value</span>
<span>@vm.TargetCurrency</span> <span>@vm.TargetCurrency</span>
if (item.Custom) if (item.Custom == "true")
{ {
@Safe.Raw("or more") @Safe.Raw("or more")
} }
} }
else if (item.Custom) else if (item.Custom == "topup" || item.Custom == "true" )
{ {
@Safe.Raw("Any amount") @Safe.Raw("Any amount")
}else if (item.Custom == "false")
{
@Safe.Raw("Free")
} }
</span> </span>
</div> </div>

View file

@ -230,7 +230,17 @@
</div> </div>
<div class="card-footer bg-transparent border-0 pt-0 pb-3"> <div class="card-footer bg-transparent border-0 pt-0 pb-3">
<span class="text-muted small">@((item.BuyButtonText ?? Model.ButtonText).Replace("{0}",item.Price.Formatted).Replace("{Price}",item.Price.Formatted))</span> <span class="text-muted small">
@{
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Custom != "false" ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
if (item.Custom != "topup")
{
buttonText = buttonText.Replace("{0}",item.Price.Formatted)
?.Replace("{Price}",item.Price.Formatted);
}
}
@Safe.Raw(buttonText)
</span>
@if (item.Inventory.HasValue) @if (item.Inventory.HasValue)
{ {

View file

@ -19,13 +19,13 @@
@for (int x = 0; x < Model.Items.Length; x++) @for (int x = 0; x < Model.Items.Length; x++)
{ {
var item = Model.Items[x]; var item = Model.Items[x];
var buttonText = (string.IsNullOrEmpty(item.BuyButtonText) ? var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Custom != "false" ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
item.Custom? if (item.Custom != "topup")
Model.CustomButtonText : {
Model.ButtonText buttonText = buttonText.Replace("{0}",item.Price.Formatted)
: item.BuyButtonText) ?.Replace("{Price}",item.Price.Formatted);
.Replace("{0}",item.Price.Formatted) }
.Replace("{Price}",item.Price.Formatted);
<div class="card px-0" data-id="@x"> <div class="card px-0" data-id="@x">
@if (!String.IsNullOrWhiteSpace(item.Image)) @if (!String.IsNullOrWhiteSpace(item.Image))
{ {
@ -35,7 +35,7 @@
<div class="card-footer bg-transparent border-0 pb-3"> <div class="card-footer bg-transparent border-0 pb-3">
@if (!item.Inventory.HasValue || item.Inventory.Value > 0) @if (!item.Inventory.HasValue || item.Inventory.Value > 0)
{ {
@if (item.Custom) @if (item.Custom == "true")
{ {
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy> <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"/> <input type="hidden" name="choicekey" value="@item.Id"/>

View file

@ -326,12 +326,12 @@
<div class="card-title d-flex justify-content-between"> <div class="card-title d-flex justify-content-between">
<span class="h5">{{perk.title? perk.title : perk.id}}</span> <span class="h5">{{perk.title? perk.title : perk.id}}</span>
<span class="text-muted"> <span class="text-muted">
<template v-if="perk.price.value"> <template v-if="perk.price && perk.price.value">
{{perk.price.value.noExponents()}} {{perk.price.value.noExponents()}}
{{targetCurrency}} {{targetCurrency}}
<template v-if="perk.custom">or more</template> <template v-if="perk.custom === 'true'">or more</template>
</template> </template>
<template v-else-if="!perk.price.value && perk.custom"> <template v-else-if="perk.custom === 'topup' || (!perk.price.value && perk.custom === 'true')">
Any amount Any amount
</template> </template>
</span> </span>
@ -340,8 +340,9 @@
<div class="input-group" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id"> <div class="input-group" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
<input <input
v-if="perk.custom !== 'topup'"
:disabled="!active" :disabled="!active"
:readonly="!perk.custom" :readonly="perk.custom !== 'true'"
class="form-control hide-number-spin" class="form-control hide-number-spin"
type="number" type="number"
v-model="amount" v-model="amount"
@ -349,7 +350,7 @@
step="any" step="any"
placeholder="Contribution Amount" placeholder="Contribution Amount"
required> required>
<span class="input-group-text">{{targetCurrency}}</span> <span class="input-group-text" v-if="perk.custom !== 'topup'">{{targetCurrency}}</span>
<button <button
class="btn btn-primary d-flex align-items-center" class="btn btn-primary d-flex align-items-center"
v-bind:class="{ 'btn-disabled': loading}" v-bind:class="{ 'btn-disabled': loading}"

View file

@ -1,6 +1,6 @@
@model PaymentModel @model PaymentModel
<div> <div>
<p>To complete payment, please send <b>@Model.BtcDue @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p> <p>To complete payment, please send <b>@Safe.Raw(Model.IsUnsetTopUp? "any amount of": Model.BtcDue) @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
<p>Time remaining: @Model.TimeLeft</p> <p>Time remaining: @Model.TimeLeft</p>
<p> <p>
<a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;" rel="noreferrer noopener">@Model.InvoiceBitcoinUrl</a> <a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;" rel="noreferrer noopener">@Model.InvoiceBitcoinUrl</a>

View file

@ -112,7 +112,7 @@ Cart.prototype.getTotalProducts = function() {
typeof this.content[key] != 'undefined' && typeof this.content[key] != 'undefined' &&
!this.content[key].disabled !this.content[key].disabled
) { ) {
var price = this.toCents(this.content[key].price.value); var price = this.toCents(this.content[key].price?.value??0);
amount += (this.content[key].count * price); amount += (this.content[key].count * price);
} }
} }
@ -439,7 +439,7 @@ Cart.prototype.listItems = function() {
'title': this.escape(item.title), 'title': this.escape(item.title),
'count': this.escape(item.count), 'count': this.escape(item.count),
'inventory': this.escape(item.inventory < 0? 99999: item.inventory), 'inventory': this.escape(item.inventory < 0? 99999: item.inventory),
'price': this.escape(item.price.formatted) 'price': this.escape(item.price?.formatted??0)
}); });
list.push($(tableTemplate)); list.push($(tableTemplate));
} }

View file

@ -38,7 +38,7 @@ addLoadEvent(function (ev) {
}, },
computed: { computed: {
canExpand: function(){ canExpand: function(){
return !this.expanded && this.active && (this.perk.price.value || this.perk.custom) && (this.perk.inventory==null || this.perk.inventory > 0) return !this.expanded && this.active && (this.perk.custom=="topup" || this.perk.price.value || this.perk.custom == "true") && (this.perk.inventory==null || this.perk.inventory > 0)
} }
}, },
methods: { methods: {
@ -58,18 +58,20 @@ addLoadEvent(function (ev) {
} }
}, },
setAmount: function (amount) { setAmount: function (amount) {
this.amount = (amount || 0).noExponents(); this.amount = this.perk.custom == "topup"? null : (amount || 0).noExponents();
this.expanded = false; this.expanded = false;
} }
}, },
mounted: function () { mounted: function () {
this.setAmount(this.perk.price.value); this.setAmount(this.perk.price?.value);
}, },
watch: { watch: {
perk: function (newValue, oldValue) { perk: function (newValue, oldValue) {
if (newValue.price.value != oldValue.price.value) { if(newValue.custom === "topup"){
this.setAmount();
}else if (newValue.price.value != oldValue.price.value) {
this.setAmount(newValue.price.value); this.setAmount(newValue.price.value);
} }
} }