diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 83d797c94..a072e46a6 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -31,6 +31,9 @@ $(IncludeRazorContentInPack) + + $(IncludeRazorContentInPack) + false diff --git a/BTCPayServer/Plugins/BoltcardBalance/Controllers/UIBoltcardBalanceController.cs b/BTCPayServer/Plugins/BoltcardBalance/Controllers/UIBoltcardBalanceController.cs index ae45fdd44..5abcfbdff 100644 --- a/BTCPayServer/Plugins/BoltcardBalance/Controllers/UIBoltcardBalanceController.cs +++ b/BTCPayServer/Plugins/BoltcardBalance/Controllers/UIBoltcardBalanceController.cs @@ -44,8 +44,6 @@ namespace BTCPayServer.Plugins.BoltcardBalance.Controllers //return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", new BalanceViewModel() //{ - // Amount = 10000m, - // AmountCollected = 500m, // AmountDue = 10000m, // Currency = "SATS", // Transactions = [new() { Date = DateTimeOffset.UtcNow, Balance = -3.0m }, new() { Date = DateTimeOffset.UtcNow, Balance = -5.0m }] @@ -59,8 +57,13 @@ namespace BTCPayServer.Plugins.BoltcardBalance.Controllers var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, boltData, true); if (registration is null) return NotFound(); + return await GetBalanceView(registration.PullPaymentId); + } + [NonAction] + public async Task GetBalanceView(string ppId) + { using var ctx = _dbContextFactory.CreateContext(); - var pp = await ctx.PullPayments.FindAsync(registration.PullPaymentId); + var pp = await ctx.PullPayments.FindAsync(ppId); if (pp is null) return NotFound(); var blob = pp.GetBlob(); @@ -92,6 +95,12 @@ namespace BTCPayServer.Plugins.BoltcardBalance.Controllers Status = payout.Entity.State }); } + vm.Transactions.Add(new BalanceViewModel.Transaction() + { + Date = pp.StartDate, + Balance = blob.Limit, + Status = PayoutState.Completed + }); return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", vm); } diff --git a/BTCPayServer/Plugins/BoltcardBalance/Views/BalanceView.cshtml b/BTCPayServer/Plugins/BoltcardBalance/Views/BalanceView.cshtml index 40fc79148..04d991db1 100644 --- a/BTCPayServer/Plugins/BoltcardBalance/Views/BalanceView.cshtml +++ b/BTCPayServer/Plugins/BoltcardBalance/Views/BalanceView.cshtml @@ -11,9 +11,12 @@
diff --git a/BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml b/BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml index 9e57dc736..9b8ec7bb2 100644 --- a/BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml +++ b/BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml @@ -69,6 +69,9 @@ }); setState("ShowBalance"); } + else { + setState("WaitingForCard"); + } }; xhttp.open('GET', url, true); xhttp.send(new FormData()); diff --git a/BTCPayServer/Plugins/BoltcardTopUp/BoltcardTopUpPlugin.cs b/BTCPayServer/Plugins/BoltcardTopUp/BoltcardTopUpPlugin.cs new file mode 100644 index 000000000..a7e329ba5 --- /dev/null +++ b/BTCPayServer/Plugins/BoltcardTopUp/BoltcardTopUpPlugin.cs @@ -0,0 +1,22 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using BTCPayServer.Services.Apps; +using Microsoft.Extensions.DependencyInjection; +using static BTCPayServer.Plugins.BoltcardFactory.BoltcardFactoryPlugin; + +namespace BTCPayServer.Plugins.BoltcardTopUp; + +public class BoltcardTopUpPlugin : BaseBTCPayServerPlugin +{ + public const string ViewsDirectory = "/Plugins/BoltcardTopUp/Views"; + public override string Identifier => "BTCPayServer.Plugins.BoltcardTopUp"; + public override string Name => "BoltcardTopUp"; + public override string Description => "Add the ability to Top-Up a Boltcard"; + + public override void Execute(IServiceCollection services) + { + services.AddSingleton(new UIExtension($"{ViewsDirectory}/NavExtension.cshtml", "header-nav")); + base.Execute(services); + } +} diff --git a/BTCPayServer/Plugins/BoltcardTopUp/Controllers/UIBoltcardTopUpController.cs b/BTCPayServer/Plugins/BoltcardTopUp/Controllers/UIBoltcardTopUpController.cs new file mode 100644 index 000000000..9c3a26f37 --- /dev/null +++ b/BTCPayServer/Plugins/BoltcardTopUp/Controllers/UIBoltcardTopUpController.cs @@ -0,0 +1,207 @@ +using BTCPayServer.Client; +using BTCPayServer.Filters; +using BTCPayServer.Plugins.PointOfSale.Models; +using BTCPayServer.Services.Apps; +using Microsoft.AspNetCore.Authorization; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using System; +using Microsoft.AspNetCore.Mvc; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Data; +using BTCPayServer.Services.Rates; +using BTCPayServer.ModelBinders; +using BTCPayServer.Plugins.BoltcardBalance; +using System.Collections.Specialized; +using BTCPayServer.Client.Models; +using BTCPayServer.NTag424; +using BTCPayServer.Services; +using NBitcoin.DataEncoders; +using NBitcoin; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using Org.BouncyCastle.Ocsp; +using System.Security.Claims; +using BTCPayServer.Payments; +using BTCPayServer.Plugins.BoltcardBalance.Controllers; +using BTCPayServer.HostedServices; + +namespace BTCPayServer.Plugins.BoltcardTopUp.Controllers +{ + public class UIBoltcardTopUpController : Controller + { + private readonly ApplicationDbContextFactory _dbContextFactory; + private readonly SettingsRepository _settingsRepository; + private readonly BTCPayServerEnvironment _env; + private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings; + private readonly RateFetcher _rateFetcher; + private readonly BTCPayNetworkProvider _networkProvider; + private readonly UIBoltcardBalanceController _boltcardBalanceController; + private readonly PullPaymentHostedService _ppService; + + public UIBoltcardTopUpController( + ApplicationDbContextFactory dbContextFactory, + SettingsRepository settingsRepository, + BTCPayServerEnvironment env, + BTCPayNetworkJsonSerializerSettings jsonSerializerSettings, + RateFetcher rateFetcher, + BTCPayNetworkProvider networkProvider, + UIBoltcardBalanceController boltcardBalanceController, + PullPaymentHostedService ppService, + CurrencyNameTable currencies) + { + _dbContextFactory = dbContextFactory; + _settingsRepository = settingsRepository; + _env = env; + _jsonSerializerSettings = jsonSerializerSettings; + _rateFetcher = rateFetcher; + _networkProvider = networkProvider; + _boltcardBalanceController = boltcardBalanceController; + _ppService = ppService; + Currencies = currencies; + } + + public CurrencyNameTable Currencies { get; } + + [HttpGet("~/stores/{storeId}/boltcards/top-up")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)] + [AutoValidateAntiforgeryToken] + public async Task Keypad(string storeId, string currency = null) + { + var settings = new PointOfSaleSettings + { + Title = "Boltcards Top-Up" + }; + currency ??= this.HttpContext.GetStoreData().GetStoreBlob().DefaultCurrency; + var numberFormatInfo = Currencies.GetNumberFormatInfo(currency); + double step = Math.Pow(10, -numberFormatInfo.CurrencyDecimalDigits); + //var store = new Data.StoreData(); + //var storeBlob = new StoreBlob(); + + return View($"{BoltcardTopUpPlugin.ViewsDirectory}/Keypad.cshtml", new ViewPointOfSaleViewModel + { + Title = settings.Title, + //StoreName = store.StoreName, + //BrandColor = storeBlob.BrandColor, + //CssFileId = storeBlob.CssFileId, + //LogoFileId = storeBlob.LogoFileId, + Step = step.ToString(CultureInfo.InvariantCulture), + //ViewType = BTCPayServer.Plugins.PointOfSale.PosViewType.Light, + //ShowCustomAmount = settings.ShowCustomAmount, + //ShowDiscount = settings.ShowDiscount, + //ShowSearch = settings.ShowSearch, + //ShowCategories = settings.ShowCategories, + //EnableTips = settings.EnableTips, + //CurrencyCode = settings.Currency, + //CurrencySymbol = numberFormatInfo.CurrencySymbol, + CurrencyCode = currency, + CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData + { + CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol, + Divisibility = numberFormatInfo.CurrencyDecimalDigits, + DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator, + ThousandSeparator = numberFormatInfo.NumberGroupSeparator, + Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern), + SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern) + }, + //Items = AppService.Parse(settings.Template, false), + //ButtonText = settings.ButtonText, + //CustomButtonText = settings.CustomButtonText, + //CustomTipText = settings.CustomTipText, + //CustomTipPercentages = settings.CustomTipPercentages, + //CustomCSSLink = settings.CustomCSSLink, + //CustomLogoLink = storeBlob.CustomLogo, + //AppId = "vouchers", + StoreId = storeId, + //Description = settings.Description, + //EmbeddedCSS = settings.EmbeddedCSS, + //RequiresRefundEmail = settings.RequiresRefundEmail + }); + } + + [HttpPost("~/stores/{storeId}/boltcards/top-up")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)] + [AutoValidateAntiforgeryToken] + public IActionResult Keypad(string storeId, + [ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, string currency) + { + return RedirectToAction(nameof(ScanCard), + new + { + storeId = storeId, + amount = amount, + currency = currency + }); + } + + [HttpGet("~/stores/{storeId}/boltcards/top-up/scan")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)] + [AutoValidateAntiforgeryToken] + public async Task ScanCard(string storeId, + [ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, string currency) + { + return View($"{BoltcardTopUpPlugin.ViewsDirectory}/ScanCard.cshtml"); + } + + [HttpPost("~/stores/{storeId}/boltcards/top-up/scan")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)] + public async Task ScanCard(string storeId, + [ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, string currency, string p, string c) + { + //return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", new BoltcardBalance.ViewModels.BalanceViewModel() + //{ + // AmountDue = 10000m, + // Currency = "SATS", + // Transactions = [new() { Date = DateTimeOffset.UtcNow, Balance = -3.0m }, new() { Date = DateTimeOffset.UtcNow, Balance = -5.0m }] + //}); + + var issuerKey = await _settingsRepository.GetIssuerKey(_env); + var boltData = issuerKey.TryDecrypt(p); + if (boltData?.Uid is null) + return NotFound(); + var id = issuerKey.GetId(boltData.Uid); + var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, boltData, true); + if (registration is null) + return NotFound(); + + var pp = await _ppService.GetPullPayment(registration.PullPaymentId, false); + + var rules = this.HttpContext.GetStoreData().GetStoreBlob().GetRateRules(_networkProvider); + var rateResult = await _rateFetcher.FetchRate(new Rating.CurrencyPair("BTC", currency), rules, default); + var cryptoAmount = Math.Round(amount / rateResult.BidAsk.Bid, 11); + + var ppCurrency = pp.GetBlob().Currency; + rateResult = await _rateFetcher.FetchRate(new Rating.CurrencyPair(ppCurrency, currency), rules, default); + var ppAmount = Math.Round(amount / rateResult.BidAsk.Bid, Currencies.GetNumberFormatInfo(ppCurrency).CurrencyDecimalDigits); + + using var ctx = _dbContextFactory.CreateContext(); + var payout = new Data.PayoutData() + { + Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)), + Date = DateTimeOffset.UtcNow, + State = PayoutState.AwaitingApproval, + PullPaymentDataId = registration.PullPaymentId, + PaymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(), + Destination = null, + StoreDataId = storeId + }; + var payoutBlob = new PayoutBlob() + { + CryptoAmount = -cryptoAmount, + Amount = -ppAmount, + Destination = null, + Metadata = new JObject(), + }; + payout.SetBlob(payoutBlob, _jsonSerializerSettings); + await ctx.Payouts.AddAsync(payout); + await ctx.SaveChangesAsync(); + _boltcardBalanceController.ViewData["NoCancelWizard"] = true; + return await _boltcardBalanceController.GetBalanceView(registration.PullPaymentId); + } + } +} diff --git a/BTCPayServer/Plugins/BoltcardTopUp/Views/Keypad.cshtml b/BTCPayServer/Plugins/BoltcardTopUp/Views/Keypad.cshtml new file mode 100644 index 000000000..0346f3310 --- /dev/null +++ b/BTCPayServer/Plugins/BoltcardTopUp/Views/Keypad.cshtml @@ -0,0 +1,48 @@ +@inject BTCPayServer.Security.ContentSecurityPolicies Csp +@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel +@{ + Layout = "PointOfSale/Public/_Layout"; + Csp.UnsafeEval(); +} +@section PageHeadContent { + +} +@section PageFootContent { + + + + +} +
+ + + +
+ + + +
+
{{currencyCode}}
+
{{ formatCurrency(total, false) }}
+
{{ calculation }}
+
+
+ +
+ +
+ + + +
diff --git a/BTCPayServer/Plugins/BoltcardTopUp/Views/NavExtension.cshtml b/BTCPayServer/Plugins/BoltcardTopUp/Views/NavExtension.cshtml new file mode 100644 index 000000000..55c38330a --- /dev/null +++ b/BTCPayServer/Plugins/BoltcardTopUp/Views/NavExtension.cshtml @@ -0,0 +1,18 @@ +@using BTCPayServer.Client +@using BTCPayServer.Plugins.BoltcardFactory +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using BTCPayServer.Views.Apps +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Abstractions.TagHelpers +@using BTCPayServer + +@{ + var storeId = Context.GetStoreData().Id; +} + + diff --git a/BTCPayServer/Plugins/BoltcardTopUp/Views/ScanCard.cshtml b/BTCPayServer/Plugins/BoltcardTopUp/Views/ScanCard.cshtml new file mode 100644 index 000000000..b2f390a1d --- /dev/null +++ b/BTCPayServer/Plugins/BoltcardTopUp/Views/ScanCard.cshtml @@ -0,0 +1,150 @@ +@{ + ViewData["Title"] = "Boltcard TopUps"; + ViewData["ShowFooter"] = false; + Layout = "/Views/Shared/_LayoutWizard.cshtml"; +} + +@section PageHeadContent +{ + +} + +
+

Boltcard Top-Up

+

Scan your card to top it up

+
+ +
+ +
+
+
+ +
+
+

NFC not supported in this device

+
+
+
+ +
+
+
+
+
+
+ + + + diff --git a/BTCPayServer/Views/Shared/_StoreHeader.cshtml b/BTCPayServer/Views/Shared/_StoreHeader.cshtml index 3c4e95a47..68165a827 100644 --- a/BTCPayServer/Views/Shared/_StoreHeader.cshtml +++ b/BTCPayServer/Views/Shared/_StoreHeader.cshtml @@ -2,8 +2,8 @@ @using BTCPayServer.Abstractions.Contracts @model (string Title, StoreBrandingViewModel StoreBranding) @{ - var logoUrl = !string.IsNullOrEmpty(Model.StoreBranding.LogoFileId) - ? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.StoreBranding.LogoFileId) + var logoUrl = !string.IsNullOrEmpty(Model.StoreBranding?.LogoFileId) + ? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.StoreBranding?.LogoFileId) : null; }