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 {
+
+
+
+
+}
+
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;
+}
+
+
+
+
+ Boltcard Top-Up
+
+
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;
}