diff --git a/BTCPayServer.Tests/CoinSwitchTests.cs b/BTCPayServer.Tests/CoinSwitchTests.cs new file mode 100644 index 000000000..cfecfb678 --- /dev/null +++ b/BTCPayServer.Tests/CoinSwitchTests.cs @@ -0,0 +1,89 @@ +using BTCPayServer.Controllers; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.CoinSwitch; +using BTCPayServer.Tests.Logging; +using Microsoft.AspNetCore.Mvc; +using Xunit; +using Xunit.Abstractions; + +namespace BTCPayServer.Tests +{ + public class CoinSwitchTests + { + public CoinSwitchTests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; + Logs.LogProvider = new XUnitLogProvider(helper); + } + + [Fact] + [Trait("Integration", "Integration")] + public async void CanSetCoinSwitchPaymentMethod() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + + + var storeBlob = controller.StoreData.GetStoreBlob(); + Assert.Null(storeBlob.CoinSwitchSettings); + + var updateModel = new UpdateCoinSwitchSettingsViewModel() + { + MerchantId = "aaa", + }; + + Assert.Equal("UpdateStore", Assert.IsType( + await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName); + + var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); + storeBlob = controller.StoreData.GetStoreBlob(); + Assert.NotNull(storeBlob.CoinSwitchSettings); + Assert.NotNull(storeBlob.CoinSwitchSettings); + Assert.IsType(storeBlob.CoinSwitchSettings); + Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId, + updateModel.MerchantId); + } + } + + + [Fact] + [Trait("Integration", "Integration")] + public async void CanToggleCoinSwitchPaymentMethod() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + + var updateModel = new UpdateCoinSwitchSettingsViewModel() + { + MerchantId = "aaa", + Enabled = true + }; + Assert.Equal("UpdateStore", Assert.IsType( + await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName); + + + var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); + + Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled); + + updateModel.Enabled = false; + + Assert.Equal("UpdateStore", Assert.IsType( + await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName); + + store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); + + Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled); + } + } + + } +} diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 8ea206191..f90f93a5e 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -13,6 +13,7 @@ using BTCPayServer.Models; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Changelly; +using BTCPayServer.Payments.CoinSwitch; using BTCPayServer.Payments.Lightning; using BTCPayServer.Security; using BTCPayServer.Services.Invoices; @@ -258,6 +259,11 @@ namespace BTCPayServer.Controllers storeBlob.ChangellySettings.IsConfigured()) ? storeBlob.ChangellySettings : null; + + CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled && + storeBlob.CoinSwitchSettings.IsConfigured()) + ? storeBlob.CoinSwitchSettings + : null; var changellyAmountDue = changelly != null @@ -309,6 +315,8 @@ namespace BTCPayServer.Controllers ChangellyEnabled = changelly != null, ChangellyMerchantId = changelly?.ChangellyMerchantId, ChangellyAmountDue = changellyAmountDue, + CoinSwitchEnabled = coinswitch != null, + CoinSwitchMerchantId = coinswitch?.MerchantId, StoreId = store.Id, AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider) .Where(i => i.Network != null) diff --git a/BTCPayServer/Controllers/StoresController.CoinSwitch.cs b/BTCPayServer/Controllers/StoresController.CoinSwitch.cs new file mode 100644 index 000000000..d60c721e7 --- /dev/null +++ b/BTCPayServer/Controllers/StoresController.CoinSwitch.cs @@ -0,0 +1,70 @@ +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.CoinSwitch; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class StoresController + { + [HttpGet] + [Route("{storeId}/coinswitch")] + public IActionResult UpdateCoinSwitchSettings(string storeId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + UpdateCoinSwitchSettingsViewModel vm = new UpdateCoinSwitchSettingsViewModel(); + SetExistingValues(store, vm); + return View(vm); + } + + private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm) + { + + var existing = store.GetStoreBlob().CoinSwitchSettings; + if (existing == null) return; + vm.MerchantId = existing.MerchantId; + vm.Enabled = existing.Enabled; + } + + [HttpPost] + [Route("{storeId}/coinswitch")] + public async Task UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm, + string command) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + if (vm.Enabled) + { + if (!ModelState.IsValid) + { + return View(vm); + } + } + + var coinSwitchSettings = new CoinSwitchSettings() + { + MerchantId = vm.MerchantId, + Enabled = vm.Enabled + }; + + switch (command) + { + case "save": + var storeBlob = store.GetStoreBlob(); + storeBlob.CoinSwitchSettings = coinSwitchSettings; + store.SetStoreBlob(storeBlob); + await _Repo.UpdateStore(store); + StatusMessage = "CoinSwitch settings modified"; + return RedirectToAction(nameof(UpdateStore), new { + storeId}); + + default: + return View(vm); + } + } + } +} diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 00e812ea0..f00765795 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -464,6 +464,14 @@ namespace BTCPayServer.Controllers Action = nameof(UpdateChangellySettings), Provider = "Changelly" }); + + var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled; + vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod() + { + Enabled = coinSwitchEnabled, + Action = nameof(UpdateCoinSwitchSettings), + Provider = "CoinSwitch" + }); } [HttpPost] diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index ab2955047..247c0006e 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -18,6 +18,7 @@ using System.ComponentModel.DataAnnotations; using BTCPayServer.Services; using System.Security.Claims; using BTCPayServer.Payments.Changelly; +using BTCPayServer.Payments.CoinSwitch; using BTCPayServer.Security; using BTCPayServer.Rating; @@ -305,6 +306,7 @@ namespace BTCPayServer.Data public bool AnyoneCanInvoice { get; set; } public ChangellySettings ChangellySettings { get; set; } + public CoinSwitchSettings CoinSwitchSettings { get; set; } string _LightningDescriptionTemplate; diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index 34fa52f3e..76e86ac40 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -61,5 +61,8 @@ namespace BTCPayServer.Models.InvoicingModels public string PeerInfo { get; set; } public string ChangellyMerchantId { get; set; } public decimal? ChangellyAmountDue { get; set; } + + public bool CoinSwitchEnabled { get; set; } + public string CoinSwitchMerchantId { get; set; } } } diff --git a/BTCPayServer/Models/StoreViewModels/UpdateCoinSwitchSettingsViewModel.cs b/BTCPayServer/Models/StoreViewModels/UpdateCoinSwitchSettingsViewModel.cs new file mode 100644 index 000000000..de534df83 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/UpdateCoinSwitchSettingsViewModel.cs @@ -0,0 +1,12 @@ +using BTCPayServer.Payments.CoinSwitch; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class UpdateCoinSwitchSettingsViewModel + { + public string MerchantId { get; set; } + public bool Enabled { get; set; } + + public string StatusMessage { get; set; } + } +} diff --git a/BTCPayServer/Payments/Coinswitch/CoinswitchSettings.cs b/BTCPayServer/Payments/Coinswitch/CoinswitchSettings.cs new file mode 100644 index 000000000..7ffc8ca19 --- /dev/null +++ b/BTCPayServer/Payments/Coinswitch/CoinswitchSettings.cs @@ -0,0 +1,15 @@ +namespace BTCPayServer.Payments.CoinSwitch +{ + public class CoinSwitchSettings + { + public string MerchantId { get; set; } + + public bool Enabled { get; set; } + + public bool IsConfigured() + { + return + !string.IsNullOrEmpty(MerchantId); + } + } +} diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index 9b11ad37e..4895fe985 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -151,7 +151,7 @@
{{$t("Copy")}}
- @if (Model.ChangellyEnabled) + @if (Model.ChangellyEnabled || Model.CoinSwitchEnabled) {
{{$t("Conversion")}} @@ -256,7 +256,7 @@
- @if (Model.ChangellyEnabled) + @if (Model.ChangellyEnabled || Model.CoinSwitchEnabled) {
-
- -
-
- - - - + + + +
+ + {{$t("Pay with Changelly")}} + + + +
+ +
- - Pay with Changelly - - - -
- -
- -
+ + }
diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index dd88532bf..18d1b2065 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -170,14 +170,16 @@ el: '#checkoutCtrl', components: { qrcode: VueQr, - changelly: ChangellyComponent + changelly: ChangellyComponent, + coinswitch: CoinSwitchComponent }, data: { srvModel: srvModel, lndModel: null, scanDisplayQr: "", expiringSoon: false, - isModal: srvModel.isModal + isModal: srvModel.isModal, + selectedThirdPartyProcessor: "" } }); diff --git a/BTCPayServer/Views/Stores/UpdateCoinSwitchSettings.cshtml b/BTCPayServer/Views/Stores/UpdateCoinSwitchSettings.cshtml new file mode 100644 index 000000000..edd44cfb9 --- /dev/null +++ b/BTCPayServer/Views/Stores/UpdateCoinSwitchSettings.cshtml @@ -0,0 +1,36 @@ +@using Microsoft.AspNetCore.Mvc.Rendering +@model UpdateCoinSwitchSettingsViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store CoinSwitch Settings"); +} + +

@ViewData["Title"]

+ + +
+
+
+

+ You can obtain a merchant id at + + https://coinswitch.co + +

+
+ + + +
+
+ + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/wwwroot/checkout/coinswitch.html b/BTCPayServer/wwwroot/checkout/coinswitch.html new file mode 100644 index 000000000..3b8cfe79c --- /dev/null +++ b/BTCPayServer/wwwroot/checkout/coinswitch.html @@ -0,0 +1,68 @@ + + + + + CoinSwitch + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BTCPayServer/wwwroot/checkout/js/coinswitchComponent.js b/BTCPayServer/wwwroot/checkout/js/coinswitchComponent.js new file mode 100644 index 000000000..824e5f1fe --- /dev/null +++ b/BTCPayServer/wwwroot/checkout/js/coinswitchComponent.js @@ -0,0 +1,38 @@ +var CoinSwitchComponent = + { + props: ["toCurrency", "toCurrencyDue", "toCurrencyAddress", "merchantId", "autoload"], + data: function () { + }, + computed: { + url: function () { + return window.location.origin + "/checkout/coinswitch.html?" + + "&toCurrency=" + + this.toCurrency + + "&toCurrencyAddress=" + + this.toCurrencyAddress + + "&toCurrencyDue=" + + this.toCurrencyDue + + (this.merchantId ? "&merchant_id=" + this.merchantId : ""); + } + }, + methods: { + openDialog: function (e) { + if (e && e.preventDefault) { + e.preventDefault(); + } + + var coinSwitchWindow = window.open( + this.url, + 'CoinSwitch', + 'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); + coinSwitchWindow.opener = null; + coinSwitchWindow.focus(); + + } + }, + mounted: function () { + if(this.autoload){ + this.openDialog(); + } + }, + };