diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 530c3193b..a38bb8d5e 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2265,14 +2265,17 @@ namespace BTCPayServer.Tests [Fact] [Trait("Integration", "Integration")] + [Trait("Altcoins", "Altcoins")] public async Task CanUsePoSApp() { using (var tester = ServerTester.Create()) { + tester.ActivateLTC(); await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); + user.RegisterDerivationScheme("LTC"); var apps = user.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp().Result).Model); vm.Name = "test"; @@ -2443,6 +2446,41 @@ noninventoryitem: Assert.Equal(1, appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory); }, 10000); + + + //test payment methods option + + vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(appId).Result).Model); + vmpos.Title = "hello"; + vmpos.Currency = "BTC"; + vmpos.Template = @" +btconly: + price: 1.0 + title: good apple + payment_methods: + - BTC +normal: + price: 1.0"; + Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); + Assert.IsType(publicApps + .ViewPointOfSale(appId, 1, null, null, null, null, "btconly").Result); + Assert.IsType(publicApps + .ViewPointOfSale(appId, 1, null, null, null, null, "normal").Result); + invoices = user.BitPay.GetInvoices(); + var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal"); + var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly"); + Assert.Single(btcOnlyInvoice.CryptoInfo); + Assert.Equal("BTC", + btcOnlyInvoice.CryptoInfo.First().CryptoCode); + Assert.Equal(PaymentTypes.BTCLike.ToString(), + btcOnlyInvoice.CryptoInfo.First().PaymentType); + + Assert.Equal(2, normalInvoice.CryptoInfo.Length); + Assert.Contains( + normalInvoice.CryptoInfo, + s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] {"BTC", "LTC"}.Contains( + s.CryptoCode)); } } diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 8f298946b..715aa9bac 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using NBitpayClient; using static BTCPayServer.Controllers.AppsController; namespace BTCPayServer.Controllers @@ -111,6 +112,7 @@ namespace BTCPayServer.Controllers } string title = null; var price = 0.0m; + Dictionary paymentMethods = null; ViewPointOfSaleViewModel.Item choice = null; if (!string.IsNullOrEmpty(choiceKey)) { @@ -130,6 +132,12 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId }); } } + + if (choice?.PaymentMethods?.Any() is true) + { + paymentMethods = choice?.PaymentMethods.ToDictionary(s => s, + s => new InvoiceSupportedTransactionCurrency() {Enabled = true}); + } } else { @@ -182,6 +190,7 @@ namespace BTCPayServer.Controllers ExtendedNotifications = true, PosData = string.IsNullOrEmpty(posData) ? null : posData, RedirectAutomatically = settings.RedirectAutomatically, + SupportedTransactionCurrencies = paymentMethods, }, store, HttpContext.Request.GetAbsoluteRoot(), new List() { AppService.GetAppInternalTag(appId) }, cancellationToken); @@ -270,6 +279,7 @@ namespace BTCPayServer.Controllers var store = await _AppService.GetStore(app); var title = settings.Title; var price = request.Amount; + Dictionary paymentMethods = null; ViewPointOfSaleViewModel.Item choice = null; if (!string.IsNullOrEmpty(request.ChoiceKey)) { @@ -290,6 +300,13 @@ namespace BTCPayServer.Controllers return NotFound("Option was out of stock"); } } + + + if (choice?.PaymentMethods?.Any() is true) + { + paymentMethods = choice?.PaymentMethods.ToDictionary(s => s, + s => new InvoiceSupportedTransactionCurrency() {Enabled = true}); + } } if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price > @@ -311,6 +328,7 @@ namespace BTCPayServer.Controllers NotificationURL = settings.NotificationUrl, FullNotifications = true, ExtendedNotifications = true, + SupportedTransactionCurrencies = paymentMethods, RedirectURL = request.RedirectUrl ?? new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString() }, store, HttpContext.Request.GetAbsoluteRoot(), diff --git a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs index e87128ead..36666f180 100644 --- a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs @@ -21,6 +21,7 @@ namespace BTCPayServer.Models.AppViewModels public string Title { get; set; } public bool Custom { get; set; } public int? Inventory { get; set; } = null; + public string[] PaymentMethods { get; set; } } public class CurrencyInfoData diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index a5fc92579..8e76f713e 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -333,7 +333,9 @@ namespace BTCPayServer.Services.Apps Formatted = Currencies.FormatCurrency(cc.Value.Value, currency) }).Single(), Custom = c.GetDetailString("custom") == "true", - 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") + }) .ToArray(); } @@ -411,6 +413,14 @@ namespace BTCPayServer.Services.Apps { return GetDetail(field).FirstOrDefault()?.Value?.Value; } + public string[] GetDetailStringList(string field) + { + if (!Value.Children.ContainsKey(field) || !( Value.Children[field] is YamlSequenceNode sequenceNode)) + { + return null; + } + return sequenceNode.Children.Select(node => (node as YamlScalarNode)?.Value).Where( s => s!= null).ToArray(); + } } private class PosScalar {