Upgrade TicketTailor to be an app

This commit is contained in:
Kukks
2023-12-06 09:18:26 +01:00
parent a1db9ddf17
commit 7996aaede0
16 changed files with 384 additions and 263 deletions

View File

@@ -0,0 +1,65 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.TicketTailor;
public class AppMigrate : IStartupTask
{
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly ApplicationDbContextFactory _contextFactory;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public AppMigrate(StoreRepository storeRepository, AppService appService,
ApplicationDbContextFactory contextFactory, BTCPayNetworkProvider btcPayNetworkProvider)
{
_storeRepository = storeRepository;
_appService = appService;
_contextFactory = contextFactory;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
var existingSettings =
await _storeRepository.GetSettingsAsync<TicketTailorSettings>("TicketTailorSettings");
foreach (var setting in existingSettings)
{
var app = new AppData()
{
Created = DateTimeOffset.UtcNow,
Name = "Ticket Tailor",
AppType = TicketTailorApp.AppType,
StoreDataId = setting.Key,
TagAllInvoices = false,
Archived = false,
Settings = JsonConvert.SerializeObject(setting.Value)
};
await _appService.UpdateOrCreateApp(app);
await using var ctx = _contextFactory.CreateContext();
var invoices = await ctx.Invoices
.Include(data => data.InvoiceSearchData)
.Where(data => data.StoreDataId == setting.Key && data.OrderId == "tickettailor").ToListAsync(cancellationToken: cancellationToken);
foreach (var invoice in invoices)
{
var entity = invoice.GetBlob(_btcPayNetworkProvider);
entity.Metadata.SetAdditionalData("appId", app.Id);
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
InvoiceRepository.AddToTextSearch(ctx, invoice, AppService.GetAppSearchTerm(app) );
invoice.SetBlob(entity);
}
await ctx.SaveChangesAsync(cancellationToken);
await _storeRepository.UpdateSetting<TicketTailorSettings>(setting.Key, "TicketTailorSettings", null);
}
}
}

View File

@@ -9,7 +9,7 @@
<PropertyGroup>
<Product>TicketTailor</Product>
<Description>Allows you to integrate with TicketTailor.com to sell tickets for Bitcoin</Description>
<Version>1.0.14</Version>
<Version>2.0.0</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>

View File

@@ -8,7 +8,7 @@ It allows you to sell tickets for your events and accept payments in Bitcoin.
1. Install the plugin from Plugins=>Add New=> TicketTailor
2. Restart BTCPay Server
3. Go to your Ticket Tailor account and add a [new API key](https://app.tickettailor.com/box-office/api#dpop=/box-office/api-key/add) with `Admin` role and "hide personal data from responses" unchecked.
4. Go back to your BTCPay Server, choose the store to integrate with and click on Ticket Tailor in the navigation.
4. Go back to your BTCPay Server, choose the store to integrate with and click on Ticket Tailor in the navigation. This will create a ticket tailor app in your current store.
5. Enter the API Key and save.
6. Now you should be able to select your Ticket tailor events in the dropdown. One selected, click save.
7. You should now have a "ticket purchase" button on your store's page. Clicking it will take you to the btcpayserver event purchase page.

View File

@@ -0,0 +1,49 @@
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Plugins.TicketTailor;
public class TicketTailorApp : AppBaseType
{
private readonly LinkGenerator _linkGenerator;
private readonly IOptions<BTCPayServerOptions> _options;
public const string AppType = "TicketTailor";
public TicketTailorApp(
LinkGenerator linkGenerator,
IOptions<BTCPayServerOptions> options)
{
Description = "Ticket Tailor";
Type = AppType;
_linkGenerator = linkGenerator;
_options = options;
}
public override Task<string> ConfigureLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(
nameof(TicketTailorController.UpdateTicketTailorSettings),
"TicketTailor", new {appId = app.Id}, _options.Value.RootPath)!);
}
public override Task<object?> GetInfo(AppData appData)
{
return Task.FromResult<object?>(null);
}
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
{
appData.SetSettings(new TicketTailorSettings());
return Task.CompletedTask;
}
public override Task<string> ViewLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(TicketTailorController.View),
"TicketTailor", new {appId = app.Id}, _options.Value.RootPath)!);
}
}

View File

@@ -2,33 +2,78 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
namespace BTCPayServer.Plugins.TicketTailor
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/TicketTailor")]
public class TicketTailorController : Controller
{
[AllowAnonymous]
[HttpGet("")]
public async Task<IActionResult> View(string storeId)
private readonly IHttpClientFactory _httpClientFactory;
private readonly TicketTailorService _ticketTailorService;
private readonly AppService _appService;
private readonly ApplicationDbContextFactory _contextFactory;
private readonly InvoiceRepository _invoiceRepository;
private readonly UIInvoiceController _uiInvoiceController;
public TicketTailorController(IHttpClientFactory httpClientFactory,
TicketTailorService ticketTailorService,
AppService appService,
ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository,
UIInvoiceController uiInvoiceController )
{
var config = await _ticketTailorService.GetTicketTailorForStore(storeId);
_httpClientFactory = httpClientFactory;
_ticketTailorService = ticketTailorService;
_appService = appService;
_contextFactory = contextFactory;
_invoiceRepository = invoiceRepository;
_uiInvoiceController = uiInvoiceController;
}
[AllowAnonymous]
[HttpGet("plugins/{storeId}/TicketTailor")]
public async Task<IActionResult> ViewLegacy(string storeId)
{
await using var ctx = _contextFactory.CreateContext();
var app = await ctx.Apps
.Where(data => data.StoreDataId == storeId && data.AppType == TicketTailorApp.AppType)
.FirstOrDefaultAsync();
if (app is null)
return NotFound();
return RedirectToAction(nameof(View), new {storeId, appId = app.Id});
}
[AllowAnonymous]
[HttpGet("plugins/TicketTailor/{appId}")]
public async Task<IActionResult> View(string appId)
{
var app = await _appService.GetApp(appId, TicketTailorApp.AppType);
if (app is null)
return NotFound();
try
{
var config = app.GetSettings<TicketTailorSettings>();
if (config?.ApiKey is not null && config?.EventId is not null)
{
var client = new TicketTailorClient(_httpClientFactory, config.ApiKey);
@@ -49,18 +94,23 @@ namespace BTCPayServer.Plugins.TicketTailor
}
[AllowAnonymous]
[HttpPost("")]
public async Task<IActionResult> Purchase(string storeId, TicketTailorViewModel request, bool preview = false)
[HttpPost("plugins/TicketTailor/{appId}")]
public async Task<IActionResult> Purchase(string appId, TicketTailorViewModel request, bool preview = false)
{
var config = await _ticketTailorService.GetTicketTailorForStore(storeId);
var app = await _appService.GetApp(appId, TicketTailorApp.AppType, true);
if (app is null)
return NotFound();
(TicketTailorClient.Hold, string error)? hold = null;
var config = app.GetSettings<TicketTailorSettings>();
try
{
if (config?.ApiKey is not null && config?.EventId is not null)
{
var client = new TicketTailorClient(_httpClientFactory, config.ApiKey);
var evt = await client.GetEvent(config.EventId);
if (evt is null || (!config.BypassAvailabilityCheck && (evt.Unavailable == "true" || evt.TicketsAvailable == "false")))
if (evt is null || (!config.BypassAvailabilityCheck &&
(evt.Unavailable == "true" || evt.TicketsAvailable == "false")))
{
return NotFound();
}
@@ -70,7 +120,8 @@ namespace BTCPayServer.Plugins.TicketTailor
if (!string.IsNullOrEmpty(request.DiscountCode) && config.AllowDiscountCodes)
{
discountCode = await client.GetDiscountCode(request.DiscountCode);
if (discountCode?.expires?.unix is not null && DateTimeOffset.FromUnixTimeSeconds(discountCode.expires.unix) < DateTimeOffset.Now)
if (discountCode?.expires?.unix is not null &&
DateTimeOffset.FromUnixTimeSeconds(discountCode.expires.unix) < DateTimeOffset.Now)
{
discountCode = null;
}
@@ -83,14 +134,17 @@ namespace BTCPayServer.Plugins.TicketTailor
{
continue;
}
var ticketType = evt.TicketTypes.FirstOrDefault(type => type.Id == purchaseRequestItem.TicketTypeId);
var ticketType =
evt.TicketTypes.FirstOrDefault(type => type.Id == purchaseRequestItem.TicketTypeId);
var specificTicket =
config.SpecificTickets?.SingleOrDefault(ticket => ticketType?.Id == ticket.TicketTypeId);
if ((config.SpecificTickets?.Any() is true && specificTicket is null) || ticketType is null ||
(!string.IsNullOrEmpty(ticketType.AccessCode) &&
!ticketType.AccessCode.Equals(request.AccessCode, StringComparison.InvariantCultureIgnoreCase)) ||
!new []{"on_sale" , "locked"}.Contains(ticketType.Status.ToLowerInvariant())
!ticketType.AccessCode.Equals(request.AccessCode,
StringComparison.InvariantCultureIgnoreCase)) ||
!new[] {"on_sale", "locked"}.Contains(ticketType.Status.ToLowerInvariant())
|| specificTicket?.Hidden is true)
{
TempData.SetStatusMessageModel(new StatusMessageModel
@@ -98,24 +152,24 @@ namespace BTCPayServer.Plugins.TicketTailor
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "The ticket was not found."
});
return RedirectToAction("View", new {storeId});
return RedirectToAction("View", new {appId});
}
if (purchaseRequestItem.Quantity > ticketType.MaxPerOrder ||
purchaseRequestItem.Quantity < ticketType.MinPerOrder )
purchaseRequestItem.Quantity < ticketType.MinPerOrder)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "The amount of tickets was not allowed."
});
return RedirectToAction("View", new {storeId});
return RedirectToAction("View", new {appId});
}
var ticketCost = ticketType.Price;
if (specificTicket is not null)
{
ticketCost =specificTicket.Price.GetValueOrDefault(ticketType.Price);
ticketCost = specificTicket.Price.GetValueOrDefault(ticketType.Price);
}
var originalTicketCost = ticketCost;
@@ -163,15 +217,14 @@ namespace BTCPayServer.Plugins.TicketTailor
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"Could not reserve tickets because {hold.Value.error}"
});
return RedirectToAction("View", new {storeId});
return RedirectToAction("View", new {appId});
}
var btcpayClient = await CreateClient(storeId);
var redirectUrl = Request.GetAbsoluteUri(Url.Action("Receipt",
"TicketTailor", new {storeId, invoiceId = "kukkskukkskukks"}));
"TicketTailor", new {storeId = app.StoreDataId, invoiceId = "kukkskukkskukks"}));
redirectUrl = redirectUrl.Replace("kukkskukkskukks", "{InvoiceId}");
request.Name??=string.Empty;
request.Name ??= string.Empty;
var nameSplit = request.Name.Split(" ", StringSplitOptions.RemoveEmptyEntries);
if (config.RequireFullName && nameSplit.Length < 2)
{
@@ -180,7 +233,7 @@ namespace BTCPayServer.Plugins.TicketTailor
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "Please enter your full name."
});
return RedirectToAction("View", new {storeId});
return RedirectToAction("View", new {appId});
}
request.Name = nameSplit.Length switch
@@ -189,13 +242,12 @@ namespace BTCPayServer.Plugins.TicketTailor
< 2 => $"{nameSplit} Nakamoto",
_ => request.Name
};
var inv = await btcpayClient.CreateInvoice(storeId,
new CreateInvoiceRequest()
var inv = await _uiInvoiceController.CreateInvoiceCoreRaw( new CreateInvoiceRequest()
{
Amount = price,
Currency = evt.Currency,
Type = InvoiceType.Standard,
AdditionalSearchTerms = new[] {"tickettailor", hold.Value.Item1.Id, evt.Id},
AdditionalSearchTerms = new[] {"tickettailor", hold.Value.Item1.Id, evt.Id, AppService.GetAppSearchTerm(app)},
Checkout =
{
RequiresRefundEmail = true,
@@ -212,20 +264,22 @@ namespace BTCPayServer.Plugins.TicketTailor
buyerName = request.Name,
buyerEmail = request.Email,
holdId = hold.Value.Item1.Id,
orderId="tickettailor",
orderId = "tickettailor",
appId,
discountCode,
discountedAmount
})
});
}),
while (inv.Amount == 0 && inv.Status == InvoiceStatus.New)
}, app.StoreData, HttpContext.Request.GetAbsoluteRoot(),new List<string> { AppService.GetAppInternalTag(appId) }, CancellationToken.None);
while (inv.Price == 0 && inv.Status == InvoiceStatusLegacy.New)
{
if (inv.Status == InvoiceStatus.New)
inv = await btcpayClient.GetInvoice(inv.StoreId, inv.Id);
if (inv.Status == InvoiceStatusLegacy.New)
inv = await _invoiceRepository.GetInvoice(inv.Id);
}
return inv.Status == InvoiceStatus.Settled
? RedirectToAction("Receipt", new {storeId, invoiceId = inv.Id})
return inv.Status.ToModernStatus() == InvoiceStatus.Settled
? RedirectToAction("Receipt", new {invoiceId = inv.Id})
: RedirectToAction("Checkout", "UIInvoice", new {invoiceId = inv.Id});
}
}
@@ -238,35 +292,45 @@ namespace BTCPayServer.Plugins.TicketTailor
});
if (hold?.Item1 is not null)
{
var client = new TicketTailorClient(_httpClientFactory, config.ApiKey);
await client.DeleteHold(hold?.Item1.Id);
}
}
return RedirectToAction("View", new {storeId});
return RedirectToAction("View", new {appId});
}
[AllowAnonymous]
[HttpGet("receipt")]
public async Task<IActionResult> Receipt(string storeId, string invoiceId)
[HttpGet("plugins/{storeId}/TicketTailor/receipt")]
[HttpGet("plugins/TicketTailor/{invoiceId}/receipt")]
public async Task<IActionResult> Receipt(string invoiceId)
{
var btcpayClient = await CreateClient(storeId);
try
{
var result = new TicketReceiptPage() {InvoiceId = invoiceId};
var invoice = await btcpayClient.GetInvoice(storeId, invoiceId);
result.Status = invoice.Status;
if (invoice.Status == InvoiceStatus.Settled &&
invoice.Metadata.TryGetValue("orderId", out var orderId) && orderId.Value<string>() == "tickettailor" &&
invoice.Metadata.TryGetValue("ticketIds", out var ticketIds))
var inv =await _invoiceRepository.GetInvoice(invoiceId);
if (inv is null)
{
await SetTicketTailorTicketResult(storeId, result, ticketIds.Values<string>());
return NotFound();
}
}else if (invoice.Status == InvoiceStatus.Settled)
if (inv.Metadata.OrderId != "tickettailor")
{
await _ticketTailorService.CheckAndIssueTicket(invoice.Id);
return NotFound();
}
var appId = AppService.GetAppInternalTags(inv).First();
var result = new TicketReceiptPage() {InvoiceId = invoiceId};
result.Status = inv.Status.ToModernStatus();
if (result.Status == InvoiceStatus.Settled &&
inv.Metadata.AdditionalData.TryGetValue("ticketIds", out var ticketIds))
{
await SetTicketTailorTicketResult(appId, result, ticketIds.Values<string>());
}
else if (inv.Status.ToModernStatus() == InvoiceStatus.Settled)
{
await _ticketTailorService.CheckAndIssueTicket(inv.Id);
}
return View(result);
@@ -277,9 +341,12 @@ namespace BTCPayServer.Plugins.TicketTailor
}
}
private async Task SetTicketTailorTicketResult(string storeId, TicketReceiptPage result, IEnumerable<string> ticketIds)
private async Task SetTicketTailorTicketResult(string appId, TicketReceiptPage result,
IEnumerable<string> ticketIds)
{
var settings = await _ticketTailorService.GetTicketTailorForStore(storeId);
var app = await _appService.GetApp(appId, TicketTailorApp.AppType);
var settings = app.GetSettings<TicketTailorSettings>();
var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey);
var tickets = await Task.WhenAll(ticketIds.Select(s => client.GetTicket(s)));
var evt = await client.GetEvent(settings.EventId);
@@ -288,19 +355,6 @@ namespace BTCPayServer.Plugins.TicketTailor
result.Settings = settings;
}
private async Task<BTCPayServerClient> CreateClient(string storeId)
{
return await _btcPayServerClientFactory.Create(null, new[] {storeId}, new DefaultHttpContext()
{
Request =
{
Scheme = "https",
Host = Request.Host,
Path = Request.Path,
PathBase = Request.PathBase
}
});
}
public class TicketReceiptPage
{
@@ -312,39 +366,28 @@ namespace BTCPayServer.Plugins.TicketTailor
}
private readonly IHttpClientFactory _httpClientFactory;
private readonly TicketTailorService _ticketTailorService;
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
public TicketTailorController(IHttpClientFactory httpClientFactory,
TicketTailorService ticketTailorService,
IBTCPayServerClientFactory btcPayServerClientFactory)
{
_httpClientFactory = httpClientFactory;
_ticketTailorService = ticketTailorService;
_btcPayServerClientFactory = btcPayServerClientFactory;
}
[HttpGet("update")]
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId)
[HttpGet("~/plugins/TicketTailor/{appId}/update")]
public async Task<IActionResult> UpdateTicketTailorSettings(string appId)
{
UpdateTicketTailorSettingsViewModel vm = new();
TicketTailorSettings TicketTailor;
try
{
TicketTailor = await _ticketTailorService.GetTicketTailorForStore(storeId);
if (TicketTailor is not null)
var app = await _appService.GetApp(appId, TicketTailorApp.AppType, false, true);
TicketTailorSettings tt = app.GetSettings<TicketTailorSettings>();
if (tt is not null)
{
vm.ApiKey = TicketTailor.ApiKey;
vm.EventId = TicketTailor.EventId;
vm.ShowDescription = TicketTailor.ShowDescription;
vm.BypassAvailabilityCheck = TicketTailor.BypassAvailabilityCheck;
vm.CustomCSS = TicketTailor.CustomCSS;
vm.RequireFullName = TicketTailor.RequireFullName;
vm.AllowDiscountCodes = TicketTailor.AllowDiscountCodes;
vm.SpecificTickets = TicketTailor.SpecificTickets;
vm.ApiKey = tt.ApiKey;
vm.EventId = tt.EventId;
vm.ShowDescription = tt.ShowDescription;
vm.BypassAvailabilityCheck = tt.BypassAvailabilityCheck;
vm.CustomCSS = tt.CustomCSS;
vm.RequireFullName = tt.RequireFullName;
vm.AllowDiscountCodes = tt.AllowDiscountCodes;
vm.SpecificTickets = tt.SpecificTickets;
}
vm.Archived = app.Archived;
vm.AppName = app.Name;
}
catch (Exception)
{
@@ -402,8 +445,8 @@ namespace BTCPayServer.Plugins.TicketTailor
}
[HttpPost("update")]
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId,
[HttpPost("~/plugins/TicketTailor/{appId}/update")]
public async Task<IActionResult> UpdateTicketTailorSettings(string appId,
UpdateTicketTailorSettingsViewModel vm,
string command)
{
@@ -429,6 +472,7 @@ namespace BTCPayServer.Plugins.TicketTailor
{
return View(vm);
}
ModelState.Clear();
var settings = new TicketTailorSettings()
{
@@ -446,15 +490,16 @@ namespace BTCPayServer.Plugins.TicketTailor
switch (command?.ToLowerInvariant())
{
case "save":
await _ticketTailorService.SetTicketTailorForStore(storeId, settings);
var app = await _appService.GetApp(appId, TicketTailorApp.AppType, false, true);
app.SetSettings(settings);
app.Name = vm.AppName;
await _appService.UpdateOrCreateApp(app);
TempData["SuccessMessage"] = "TicketTailor settings modified";
return RedirectToAction(nameof(UpdateTicketTailorSettings), new {storeId});
return RedirectToAction(nameof(UpdateTicketTailorSettings), new {appId});
default:
return View(vm);
}
}
}
}

View File

@@ -1,6 +1,8 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Services.Apps;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.TicketTailor
@@ -9,16 +11,17 @@ namespace BTCPayServer.Plugins.TicketTailor
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.12.0" }
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.12.0"}
};
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddStartupTask<AppMigrate>();
applicationBuilder.AddSingleton<TicketTailorService>();
applicationBuilder.AddHostedService(s=>s.GetRequiredService<TicketTailorService>());
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("TicketTailor/StoreIntegrationTicketTailorOption",
"store-integrations-list"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("TicketTailor/TicketTailorNav",
"store-integrations-nav"));
applicationBuilder.AddHostedService(s => s.GetRequiredService<TicketTailorService>());
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("TicketTailor/NavExtension", "header-nav"));
applicationBuilder.AddSingleton<AppBaseType, TicketTailorApp>();
base.Execute(applicationBuilder);
}
}

View File

@@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Http;
@@ -24,63 +25,33 @@ namespace BTCPayServer.Plugins.TicketTailor;
public class TicketTailorService : EventHostedServiceBase
{
private readonly ISettingsRepository _settingsRepository;
private readonly IMemoryCache _memoryCache;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IStoreRepository _storeRepository;
private readonly ILogger<TicketTailorService> _logger;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly LinkGenerator _linkGenerator;
private readonly InvoiceRepository _invoiceRepository;
private readonly AppService _appService;
public TicketTailorService(ISettingsRepository settingsRepository, IMemoryCache memoryCache,
public TicketTailorService(IMemoryCache memoryCache,
IHttpClientFactory httpClientFactory,
IStoreRepository storeRepository, ILogger<TicketTailorService> logger,
ILogger<TicketTailorService> logger,
EmailSenderFactory emailSenderFactory ,
LinkGenerator linkGenerator,
EventAggregator eventAggregator, InvoiceRepository invoiceRepository) : base(eventAggregator, logger)
EventAggregator eventAggregator, InvoiceRepository invoiceRepository,
AppService appService) : base(eventAggregator, logger)
{
_settingsRepository = settingsRepository;
_memoryCache = memoryCache;
_httpClientFactory = httpClientFactory;
_storeRepository = storeRepository;
_logger = logger;
_emailSenderFactory = emailSenderFactory;
_linkGenerator = linkGenerator;
_invoiceRepository = invoiceRepository;
_appService = appService;
}
public async Task<TicketTailorSettings> GetTicketTailorForStore(string storeId)
{
var k = $"{nameof(TicketTailorSettings)}_{storeId}";
return await _memoryCache.GetOrCreateAsync(k, async _ =>
{
var res = await _storeRepository.GetSettingAsync<TicketTailorSettings>(storeId,
nameof(TicketTailorSettings));
if (res is not null) return res;
res = await _settingsRepository.GetSettingAsync<TicketTailorSettings>(k);
if (res is not null)
{
await SetTicketTailorForStore(storeId, res);
}
await _settingsRepository.UpdateSetting<TicketTailorSettings>(null, k);
return res;
});
}
public async Task SetTicketTailorForStore(string storeId, TicketTailorSettings TicketTailorSettings)
{
var k = $"{nameof(TicketTailorSettings)}_{storeId}";
await _storeRepository.UpdateSetting(storeId, nameof(TicketTailorSettings), TicketTailorSettings);
_memoryCache.Set(k, TicketTailorSettings);
}
private class IssueTicket
{
public InvoiceEntity Invoice { get; set; }
@@ -153,7 +124,9 @@ public class TicketTailorService : EventHostedServiceBase
{
var invLogs = new InvoiceLogs();
var settings = await GetTicketTailorForStore(issueTicket.Invoice.StoreId);
var appId = AppService.GetAppInternalTags(issueTicket.Invoice).First();
var app = await _appService.GetApp(appId, TicketTailorApp.AppType);
var settings = app.GetSettings<TicketTailorSettings>();
var invoice = issueTicket.Invoice;
if (settings?.ApiKey is null)
{
@@ -269,7 +242,7 @@ public class TicketTailorService : EventHostedServiceBase
var url =
_linkGenerator.GetUriByAction("Receipt",
"TicketTailor",
new {issueTicket.Invoice.StoreId, invoiceId = invoice.Id},
new {invoiceId = invoice.Id},
uri.Scheme,
new HostString(uri.Host),
uri.AbsolutePath);

View File

@@ -5,6 +5,8 @@ namespace BTCPayServer.Plugins.TicketTailor;
public class UpdateTicketTailorSettingsViewModel
{
public string AppName { get; set; }
public bool Archived { get; set; }
public string NewSpecificTicket { get; set; }
public string ApiKey { get; set; }
public SelectList Events { get; set; }

View File

@@ -0,0 +1,36 @@
@using BTCPayServer.Client
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Views.Apps
@using BTCPayServer.Services.Apps
@using BTCPayServer
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Plugins.TicketTailor
@inject AppService AppService;
@model BTCPayServer.Components.MainNav.MainNavViewModel
@{
var store = Context.GetStoreData();
}
@if (store != null)
{
var appType = AppService.GetAppType(TicketTailorApp.AppType)!;
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@store.Id" asp-route-appType="@appType.Type" class="nav-link @ViewData.IsActivePage(AppsNavPages.Create, appType.Type)" id="@($"StoreNav-Create{appType.Type}")">
<img style="width:14px; margin-right: 10px;" class="icon" src="~/Resources/assets/tt.png" />
<span>@appType.Description</span>
</a>
</li>
@foreach (var app in Model.Apps.Where(app => app.AppType == appType.Type))
{
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="TicketTailor" asp-action="UpdateTicketTailorSettings" asp-route-appId="@app.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, app.Id)" id="@($"StoreNav-App-{app.Id}")">
<span>@app.AppName</span>
</a>
</li>
<li class="nav-item nav-item-sub" not-permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="TicketTailor" asp-action="View" asp-route-appId="@app.Id" class="nav-link">
<span>@app.AppName</span>
</a>
</li>
}
}

View File

@@ -1,59 +0,0 @@
@using BTCPayServer.Client
@using BTCPayServer.Plugins.TicketTailor
@using Microsoft.AspNetCore.Routing
@using BTCPayServer.Abstractions.Contracts
@inject IScopeProvider ScopeProvider
@inject TicketTailorService TicketTailorService
@{
var storeId = ScopeProvider.GetCurrentStoreId();
TicketTailorSettings settings = null;
if (!string.IsNullOrEmpty(storeId))
{
try
{
settings = await TicketTailorService.GetTicketTailorForStore(storeId);
}
catch (Exception)
{
}
}
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="list-group-item bg-tile ">
<div class="d-flex align-items-center">
<span class="d-flex flex-wrap flex-fill flex-column flex-sm-row">
<strong class="me-3">
Ticket Tailor
</strong>
<span title="" class="d-flex me-3">
<span class="text-secondary">Sell tickets on Ticket Tailor using BTCPay Server</span>
</span>
</span>
<span class="d-flex align-items-center fw-semibold">
@if (settings?.ApiKey is not null)
{
<span class="d-flex align-items-center text-success">
<span class="me-2 btcpay-status btcpay-status--enabled"></span>
Active
</span>
<span class="text-light ms-3 me-2">|</span>
<a lass="btn btn-link px-1 py-1 fw-semibold" asp-controller="TicketTailor" asp-action="UpdateTicketTailorSettings" asp-route-storeId="@storeId">
Modify
</a>
}
else
{
<span class="d-flex align-items-center text-danger">
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
Disabled
</span>
<a class="btn btn-primary btn-sm ms-4 px-3 py-1 fw-semibold" asp-controller="TicketTailor" asp-action="UpdateTicketTailorSettings" asp-route-storeId="@storeId">
Setup
</a>
}
</span>
</div>
</li>
}

View File

@@ -1,17 +0,0 @@
@using BTCPayServer.Plugins.TicketTailor
@inject IScopeProvider ScopeProvider
@using BTCPayServer.Abstractions.Contracts
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(TicketTailorController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase);
}
@if (!string.IsNullOrEmpty(storeId))
{
<li class="nav-item">
<a asp-area="" asp-controller="TicketTailor" asp-action="UpdateTicketTailorSettings" asp-route-storeId="@storeId" class="nav-link js-scroll-trigger @(isActive? "active": string.Empty)">
<img style="width:14px; margin-right: 10px;" class="icon" src="~/Resources/assets/tt.png" />
<span>TicketTailor</span>
</a>
</li>
}

View File

@@ -1,12 +1,6 @@
@using BTCPayServer.Client.Models
@model BTCPayServer.Plugins.TicketTailor.TicketTailorController.TicketReceiptPage
@inject ContentSecurityPolicies contentSecurityPolicies
@using BTCPayServer.Security
@using NBitcoin
@{
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'");
contentSecurityPolicies.AllowUnsafeHashes();
Layout = "_LayoutSimple";
var reloadPage = false;
}
@@ -162,14 +156,14 @@
@if (reloadPage)
{
<script type="text/javascript" nonce="@nonce">
<script type="text/javascript" >
setTimeout(function(){
window.location.reload();
}, 3000);
</script>
}
<script type="text/javascript" nonce="@nonce">
<script type="text/javascript" >
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("btnPrint").addEventListener("click", function (){

View File

@@ -1,11 +1,16 @@
@using BTCPayServer.Plugins.TicketTailor
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Views.Apps
@using Microsoft.AspNetCore.Routing
@using BTCPayServer
@using BTCPayServer.Abstractions.Models
@using BTCPayServer.Services.Apps
@model BTCPayServer.Plugins.TicketTailor.UpdateTicketTailorSettingsViewModel
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
ViewData.SetActivePage("TicketTailor", "Update Store TicketTailor Settings", null);
var appId = Context.GetRouteValue("appId").ToString();
var storeId = Context.GetCurrentStoreId();
ViewData.SetActivePage(AppsNavPages.Update.ToString(), typeof(AppsNavPages).ToString(), "Update Ticket Tailor app", appId);
Model.SpecificTickets ??= new List<SpecificTicket>();
}
@@ -17,9 +22,13 @@
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">
<input type="submit" value="Save" name="command" class="btn btn-primary"/>
@if (this.ViewContext.ModelState.IsValid && Model.EventId is not null)
@if (Model.Archived)
{
<a class="btn btn-secondary" href=" @Url.Action("View", "TicketTailor", new {storeId})">
<button type="button" class="btn btn-outline-secondary" onclick="document.getElementById('btn-archive-toggle').click()">Unarchive</button>
}
else if (this.ViewContext.ModelState.IsValid && Model.EventId is not null)
{
<a class="btn btn-secondary" target="_blank" href=" @Url.Action("View", "TicketTailor", new {appId})">
Ticket purchase page
</a>
}
@@ -32,14 +41,18 @@
@if (ViewContext.ModelState.IsValid && Model.EventId is not null)
{
<div class="alert alert-warning">
Please ensure that emails in your store are configured if you wish to send the tickets via email to customers as TicketTailor does not handle it for tickets issued via its API.
Please ensure that emails in your store are configured if you wish to send the tickets via email to customers as TicketTailor does not handle it for tickets issued via its API. <a asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@storeId" class="alert-link">Configure here.</a>
</div>
}
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="AppName" class="form-label" data-required>App name</label>
<input asp-for="AppName" class="form-control" required/>
<span asp-validation-for="AppName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ApiKey" class="form-label" data-required>TicketTailor API Key</label>
<input asp-for="ApiKey" class="form-control" required/>
@@ -144,4 +157,25 @@
</div>
</form>
<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>
<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" >
@if (Model.Archived)
{
<span class="text-nowrap">Unarchive this app</span>
}
else
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this app so that it does not appear in the apps list by default">Archive this app</span>
}
</button>
</form>
<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>
<partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" />
<partial name="_ValidationScriptsPartial"/>

View File

@@ -3,7 +3,7 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Plugins.TicketTailor.TicketTailorViewModel
@{
var storeId = Context.GetRouteValue("storeId");
var appId = Context.GetRouteValue("appId");
Layout = "_LayoutSimple";
var available = Model.Settings.BypassAvailabilityCheck || (Model.Event.Unavailable != "true" && Model.Event.TicketsAvailable == "true");
Model.Settings.SpecificTickets ??= new List<SpecificTicket>();
@@ -70,7 +70,7 @@ document.addEventListener("DOMContentLoaded", ()=>{
}
}
}
xhttp.open("POST", "@Url.Action("Purchase", new {storeId, preview = true})", true);
xhttp.open("POST", "@Url.Action("Purchase", new {appId, preview = true})", true);
xhttp.send(data);
}
}))
@@ -92,7 +92,7 @@ document.addEventListener("DOMContentLoaded", ()=>{
<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-storeId="@storeId">
<form method="post" asp-controller="TicketTailor" asp-action="Purchase" asp-antiforgery="false" asp-route-appId="@appId">
<input type="hidden" asp-for="AccessCode" value="@accessCode"/>
<div class="row g-2 justify-content-center mb-4" id="ticket-form-container">
<div class="col-sm-6 col-md-4">

View File

@@ -1,9 +1,5 @@
@using BTCPayServer.Abstractions.Extensions
@inject BTCPayServer.Abstractions.Services.Safe Safe
@addTagHelper *, BTCPayServer.Abstractions
@addTagHelper *, BTCPayServer.TagHelpers
@addTagHelper *, BTCPayServer.Views.TagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Services
@inject Safe Safe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BTCPayServer
@addTagHelper *, BTCPayServer.Abstractions