This commit is contained in:
Kukks
2023-12-08 11:09:23 +01:00
parent 07d5f26e7a
commit 11f9d8f379
6 changed files with 93 additions and 111 deletions

View File

@@ -11,6 +11,7 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -68,7 +69,7 @@ namespace BTCPayServer.Plugins.TicketTailor
[HttpGet("plugins/TicketTailor/{appId}")] [HttpGet("plugins/TicketTailor/{appId}")]
public async Task<IActionResult> View(string appId) public async Task<IActionResult> View(string appId)
{ {
var app = await _appService.GetApp(appId, TicketTailorApp.AppType); var app = await _appService.GetApp(appId, TicketTailorApp.AppType, true);
if (app is null) if (app is null)
return NotFound(); return NotFound();
try try
@@ -83,7 +84,11 @@ namespace BTCPayServer.Plugins.TicketTailor
return NotFound(); return NotFound();
} }
return View(new TicketTailorViewModel() {Event = evt, Settings = config}); return View(new TicketTailorViewModel()
{
Event = evt, Settings = config,
StoreBranding = new StoreBrandingViewModel(app.StoreData.GetStoreBlob())
});
} }
} }
catch (Exception e) catch (Exception e)
@@ -380,6 +385,7 @@ namespace BTCPayServer.Plugins.TicketTailor
vm.ApiKey = tt.ApiKey; vm.ApiKey = tt.ApiKey;
vm.EventId = tt.EventId; vm.EventId = tt.EventId;
vm.ShowDescription = tt.ShowDescription; vm.ShowDescription = tt.ShowDescription;
vm.SendEmail = tt.SendEmail;
vm.BypassAvailabilityCheck = tt.BypassAvailabilityCheck; vm.BypassAvailabilityCheck = tt.BypassAvailabilityCheck;
vm.CustomCSS = tt.CustomCSS; vm.CustomCSS = tt.CustomCSS;
vm.RequireFullName = tt.RequireFullName; vm.RequireFullName = tt.RequireFullName;
@@ -483,7 +489,8 @@ namespace BTCPayServer.Plugins.TicketTailor
SpecificTickets = vm.SpecificTickets, SpecificTickets = vm.SpecificTickets,
BypassAvailabilityCheck = vm.BypassAvailabilityCheck, BypassAvailabilityCheck = vm.BypassAvailabilityCheck,
RequireFullName = vm.RequireFullName, RequireFullName = vm.RequireFullName,
AllowDiscountCodes = vm.AllowDiscountCodes AllowDiscountCodes = vm.AllowDiscountCodes,
SendEmail = vm.SendEmail
}; };

View File

@@ -238,6 +238,10 @@ public class TicketTailorService : EventHostedServiceBase
} }
await _invoiceRepository.UpdateInvoiceMetadata(invoice.Id, invoice.StoreId, invoice.Metadata.ToJObject()); await _invoiceRepository.UpdateInvoiceMetadata(invoice.Id, invoice.StoreId, invoice.Metadata.ToJObject());
await _invoiceRepository.AddInvoiceLogs(invoice.Id, invLogs); await _invoiceRepository.AddInvoiceLogs(invoice.Id, invLogs);
if (settings.SendEmail)
{
var uri = new Uri(btcpayUrl); var uri = new Uri(btcpayUrl);
var url = var url =
_linkGenerator.GetUriByAction("Receipt", _linkGenerator.GetUriByAction("Receipt",
@@ -257,6 +261,8 @@ public class TicketTailorService : EventHostedServiceBase
{ {
// ignored // ignored
} }
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -13,5 +13,6 @@ namespace BTCPayServer.Plugins.TicketTailor
public bool BypassAvailabilityCheck { get; set; } public bool BypassAvailabilityCheck { get; set; }
public bool RequireFullName { get; set; } public bool RequireFullName { get; set; }
public bool AllowDiscountCodes { get; set; } public bool AllowDiscountCodes { get; set; }
public bool SendEmail { get; set; } = true;
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Plugins.TicketTailor; namespace BTCPayServer.Plugins.TicketTailor;
@@ -12,6 +13,7 @@ public class UpdateTicketTailorSettingsViewModel
public SelectList Events { get; set; } public SelectList Events { get; set; }
public string EventId { get; set; } public string EventId { get; set; }
public bool ShowDescription { get; set; } public bool ShowDescription { get; set; }
public bool SendEmail { get; set; }
public string CustomCSS { get; set; } public string CustomCSS { get; set; }
public TicketTailorClient.TicketType[] TicketTypes { get; set; } public TicketTailorClient.TicketType[] TicketTypes { get; set; }
@@ -41,6 +43,7 @@ public class TicketTailorViewModel
public PurchaseRequestItem[] Items { get; set; } public PurchaseRequestItem[] Items { get; set; }
public string AccessCode { get; set; } public string AccessCode { get; set; }
public string DiscountCode { get; set; } public string DiscountCode { get; set; }
public StoreBrandingViewModel StoreBranding { get; set; }
public class PurchaseRequestItem public class PurchaseRequestItem
{ {

View File

@@ -89,6 +89,11 @@
<label asp-for="AllowDiscountCodes" class="form-check-label">Allow discount codes (due to the nature of Ticket Tailor's API, redemption limit is not applied.</label> <label asp-for="AllowDiscountCodes" class="form-check-label">Allow discount codes (due to the nature of Ticket Tailor's API, redemption limit is not applied.</label>
<span asp-validation-for="AllowDiscountCodes" class="text-danger"></span> <span asp-validation-for="AllowDiscountCodes" class="text-danger"></span>
</div> </div>
<div class="form-group form-check">
<input asp-for="SendEmail" type="checkbox" class="form-check-input"/>
<label asp-for="SendEmail" class="form-check-label">Send an email when an invoice settles and the ticket is issued. </label>
<span asp-validation-for="SendEmail" class="text-danger"></span>
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="CustomCSS" class="form-label">Additional Custom CSS</label> <label asp-for="CustomCSS" class="form-label">Additional Custom CSS</label>
@@ -162,7 +167,7 @@
<div class="d-grid d-sm-flex flex-wrap gap-3 mt-3"> <div class="d-grid d-sm-flex flex-wrap gap-3 mt-3">
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@storeId" asp-route-searchterm="@AppService.GetAppSearchTerm(TicketTailorApp.AppType, appId)">Invoices</a> <a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@storeId" asp-route-searchterm="@AppService.GetAppSearchTerm(TicketTailorApp.AppType, appId)">Invoices</a>
<form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@appId"> <form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@appId">
<button type="submit" class="w-100 btn btn-outline-secondary" id="btn-archive-toggle" > <button type="submit" class="w-100 btn btn-outline-secondary" id="btn-archive-toggle">
@if (Model.Archived) @if (Model.Archived)
{ {
<span class="text-nowrap">Unarchive this app</span> <span class="text-nowrap">Unarchive this app</span>
@@ -176,6 +181,6 @@
<a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@appId" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app and its settings will be permanently deleted." data-confirm-input="DELETE">Delete this app</a> <a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@appId" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app and its settings will be permanently deleted." data-confirm-input="DELETE">Delete this app</a>
</div> </div>
<partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" /> <partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))"/>
<partial name="_ValidationScriptsPartial"/> <partial name="_ValidationScriptsPartial"/>

View File

@@ -4,7 +4,8 @@
@model BTCPayServer.Plugins.TicketTailor.TicketTailorViewModel @model BTCPayServer.Plugins.TicketTailor.TicketTailorViewModel
@{ @{
var appId = Context.GetRouteValue("appId"); var appId = Context.GetRouteValue("appId");
Layout = "_LayoutSimple"; ViewData["StoreBranding"] = Model.StoreBranding;
Layout = null;
var available = Model.Settings.BypassAvailabilityCheck || (Model.Event.Unavailable != "true" && Model.Event.TicketsAvailable == "true"); var available = Model.Settings.BypassAvailabilityCheck || (Model.Event.Unavailable != "true" && Model.Event.TicketsAvailable == "true");
Model.Settings.SpecificTickets ??= new List<SpecificTicket>(); Model.Settings.SpecificTickets ??= new List<SpecificTicket>();
Context.Request.Query.TryGetValue("accessCode", out var accessCode); Context.Request.Query.TryGetValue("accessCode", out var accessCode);
@@ -13,85 +14,36 @@
Model.DiscountCode = discount; Model.DiscountCode = discount;
} }
} }
<style>
<!DOCTYPE html>
<html lang="en">
<head>
<partial name="LayoutHead"/>
<style>
hr:last-child{ hr:last-child{
display: none; display: none;
} }
footer {
display: none;
}
@if (!string.IsNullOrEmpty(Model.Settings.CustomCSS)) @if (!string.IsNullOrEmpty(Model.Settings.CustomCSS))
{ {
@Safe.Raw(Model.Settings.CustomCSS) @Safe.Raw(Model.Settings.CustomCSS)
} }
</style> </style>
<script> </head>
document.addEventListener("DOMContentLoaded", ()=>{ <body class="min-vh-100">
const form = document.querySelector("form"); <div id="TicketTailor" class="public-page-wrap">
const btn = document.querySelector("button[type='submit']"); <partial name="_StoreHeader" model="(Model.Event.Title, Model.StoreBranding)"/>
const inputs = document.querySelectorAll("input"); <main>
const discountCode = document.querySelector("#DiscountCode");
inputs.forEach(value => value.addEventListener("input", (evt)=>{
let total = 0;
let totalQty = 0;
document.querySelectorAll("[data-price]").forEach(value1 => {
if (!!value1.value){
const qty = parseInt(value1.value);
if (qty > 0){
const price = parseFloat(value1.dataset.price).toPrecision(12);
total += price * qty;
totalQty += qty;
}
}
});
if (totalQty > 0){
btn.removeAttribute("disabled");
}
else{
btn.setAttribute("disabled", "disabled");
}
btn.textContent = `Purchase for ${total.toFixed(2)} @Model.Event.Currency.ToUpperInvariant()`
if (discountCode && discountCode.value && totalQty > 0){
const data = new FormData(form);
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const response = JSON.parse(this.responseText);
if (response.discountedAmount){
btn.innerHTML = `Purchase for ${response.total.toFixed(2)} @Model.Event.Currency.ToUpperInvariant()<br/><span class="">${response.discountedAmount.toFixed(2)} @Model.Event.Currency.ToUpperInvariant() discount</span>`
} else{
btn.textContent = `Purchase for ${response.total.toFixed(2)} @Model.Event.Currency.ToUpperInvariant()`
}
}
}
xhttp.open("POST", "@Url.Action("Purchase", new {appId, preview = true})", true);
xhttp.send(data);
}
}))
form.addEventListener("submit", ()=>{
btn.setAttribute("disabled", "disabled");
inputs.forEach(value => value.setAttribute("readonly", "readonly"));
})
})
</script>
<div class="container d-flex h-100">
<div class="justify-content-center mx-auto px-2 py-3 w-100 m-auto">
<partial name="_StatusMessage"/> <partial name="_StatusMessage"/>
<div class="text-muted mb-4 text-center lead ">@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted</div>
<h1 class="text-center ">@Model.Event.Title</h1>
<h2 class="text-muted mb-4 text-center ">@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted</h2>
@if (Model.Settings.ShowDescription && !string.IsNullOrEmpty(Model.Event.Description)) @if (Model.Settings.ShowDescription && !string.IsNullOrEmpty(Model.Event.Description))
{ {
<div class="row" id="ticket-tailor-description text-center "> <div class="ticket-tailor-description lead">@Safe.Raw(Model.Event.Description)</div>
<div class="overflow-hidden col-12 ">@Safe.Raw(Model.Event.Description)</div>
</div>
} }
<form method="post" asp-controller="TicketTailor" asp-action="Purchase" asp-antiforgery="false" asp-route-appId="@appId"> <form method="post" asp-controller="TicketTailor" asp-action="Purchase" asp-antiforgery="false" asp-route-appId="@appId">
<input type="hidden" asp-for="AccessCode" value="@accessCode"/> <input type="hidden" asp-for="AccessCode" value="@accessCode"/>
<div class="row g-2 justify-content-center mb-4" id="ticket-form-container"> <div class="row g-2 justify-content-center mb-4" id="ticket-form-container">
@@ -122,7 +74,6 @@ document.addEventListener("DOMContentLoaded", ()=>{
<div class="bg-tile w-100 p-4 mb-2"> <div class="bg-tile w-100 p-4 mb-2">
@if (!string.IsNullOrEmpty(groupedTickets.Key)) @if (!string.IsNullOrEmpty(groupedTickets.Key))
{ {
var group = Model.Event.TicketGroups.First(ticketGroup => ticketGroup.Id == groupedTickets.Key); var group = Model.Event.TicketGroups.First(ticketGroup => ticketGroup.Id == groupedTickets.Key);
@@ -140,25 +91,31 @@ document.addEventListener("DOMContentLoaded", ()=>{
{ {
continue; continue;
} }
if (matched.Price is not null) if (matched.Price is not null)
{ {
item.Price = matched.Price.Value; item.Price = matched.Price.Value;
} }
if (!string.IsNullOrEmpty(matched.Name)) if (!string.IsNullOrEmpty(matched.Name))
{ {
item.Name = matched.Name; item.Name = matched.Name;
} }
if (!string.IsNullOrEmpty(matched.Description)) if (!string.IsNullOrEmpty(matched.Description))
{ {
item.Description = matched.Description; item.Description = matched.Description;
} }
availableToShow = true; availableToShow = true;
specific = true; specific = true;
} }
if (!availableToShow) if (!availableToShow)
{ {
continue; continue;
} }
index++; index++;
<input type="hidden" asp-for="Items[index].TicketTypeId" value="@item.Id"/> <input type="hidden" asp-for="Items[index].TicketTypeId" value="@item.Id"/>
@@ -200,7 +157,6 @@ document.addEventListener("DOMContentLoaded", ()=>{
</div> </div>
} }
} }
@@ -221,14 +177,18 @@ document.addEventListener("DOMContentLoaded", ()=>{
</div> </div>
</form> </form>
</main>
<div class="row text-center"> <footer class="store-footer">
<div class="col-12" id="fiat-page-link"> <p >
<a href="@Model.Event.Url">Back to fiat ticket page</a> <a href="@Model.Event.Url">Back to fiat ticket page</a>
</div> </p>
<div class="powered__by__btcpayserver col-12"> <a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver" rel="noreferrer noopener">BTCPay Server</a> Powered by <partial name="_StoreFooterLogo"/>
</div> </a>
</div> </footer>
</div>
</div> </div>
<partial name="LayoutFoot"/>
</body>
</html>