diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index 835a471d8..db56a4490 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -1,9 +1,5 @@ -using System; -using BTCPayServer.Client.JsonConverters; using BTCPayServer.JsonConverters; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; namespace BTCPayServer.Client.Models { diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index 2d5a2ccd2..8e1bda53d 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -40,6 +40,7 @@ namespace BTCPayServer.Client.Models public string RedirectURL { get; set; } public bool? RedirectAutomatically { get; set; } + public bool? RequiresRefundEmail { get; set; } = null; public string DefaultLanguage { get; set; } } } diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index eec46ca20..17f8c7fe3 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using BTCPayServer.Payments; using BTCPayServer.Tests.Logging; @@ -73,6 +71,68 @@ namespace BTCPayServer.Tests } } + [Fact(Timeout = TestTimeout)] + public async Task CanHandleRefundEmailForm2() + { + + using (var s = SeleniumTester.Create()) + { + // Prepare user account and store + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(); + var store = s.CreateNewStore(); + s.AddDerivationScheme("BTC"); + + // Now create an invoice that requires a refund email + var invoice = s.CreateInvoice(store.storeName, 100, "USD", "", null, true); + s.GoToInvoiceCheckout(invoice); + + var emailInput = s.Driver.FindElement(By.Id("emailAddressFormInput")); + Assert.True(emailInput.Displayed); + + emailInput.SendKeys("a@g.com"); + + var actionButton = s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button")); + actionButton.Click(); + try // Sometimes the click only take the focus, without actually really clicking on it... + { + actionButton.Click(); + } + catch { } + + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + + s.GoToHome(); + + // Now create an invoice that doesn't require a refund email + s.CreateInvoice(store.storeName, 100, "USD", "", null, false); + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + s.Driver.AssertNoError(); + s.Driver.Navigate().Back(); + s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); + Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + + s.GoToHome(); + + // Now create an invoice that requires refund email but already has one set, email input shouldn't show up + s.CreateInvoice(store.storeName, 100, "USD", "a@g.com", null, true); + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + s.Driver.AssertNoError(); + s.Driver.Navigate().Back(); + s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); + Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + } + } + [Fact(Timeout = TestTimeout)] public async Task CanUseLanguageDropdown() { diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index bfd479532..9ca1756d9 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1094,11 +1094,13 @@ namespace BTCPayServer.Tests Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"), Checkout = new CreateInvoiceRequest.CheckoutOptions() { - RedirectAutomatically = true + RedirectAutomatically = true, + RequiresRefundEmail = true }, AdditionalSearchTerms = new string[] { "Banana" } }); Assert.True(newInvoice.Checkout.RedirectAutomatically); + Assert.True(newInvoice.Checkout.RequiresRefundEmail); Assert.Equal(user.StoreId, newInvoice.StoreId); //list var invoices = await viewOnly.GetInvoices(user.StoreId); diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index de1ba21c4..c8047fe33 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -364,6 +364,7 @@ namespace BTCPayServer.Tests string currency = "USD", string refundEmail = "", string defaultPaymentMethod = null, + bool? requiresRefundEmail = null, StatusMessageModel.StatusSeverity expectedSeverity = StatusMessageModel.StatusSeverity.Success ) { @@ -378,6 +379,8 @@ namespace BTCPayServer.Tests Driver.FindElement(By.Name("StoreId")).SendKeys(storeName); if (defaultPaymentMethod is string) new SelectElement(Driver.FindElement(By.Name("DefaultPaymentMethod"))).SelectByValue(defaultPaymentMethod); + if (requiresRefundEmail is bool) + new SelectElement(Driver.FindElement(By.Name("RequiresRefundEmail"))).SelectByValue(requiresRefundEmail == true ? "1" : "2"); Driver.FindElement(By.Id("Create")).Click(); var statusElement = FindAlertMessage(expectedSeverity); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index e99c05ca1..f0f627a21 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1344,7 +1344,7 @@ namespace BTCPayServer.Tests s.Driver.SetCheckbox(By.Id("LNURLBech32Mode"), false); s.Driver.SetCheckbox(By.Id("DisableBolt11PaymentMethod"), true); }, false); - s.CreateInvoice(store.storeName, 0.0000001m, "BTC","",null, StatusMessageModel.StatusSeverity.Error); + s.CreateInvoice(store.storeName, 0.0000001m, "BTC","",null, expectedSeverity: StatusMessageModel.StatusSeverity.Error); i = s.CreateInvoice(store.storeName, null, "BTC"); diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index e70c32bca..e7dcde226 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -56,6 +56,7 @@ namespace BTCPayServer.Controllers ShowCustomAmount = true; ShowDiscount = true; EnableTips = true; + RequiresRefundEmail = RequiresRefundEmail.InheritFromStore; } public string Title { get; set; } public string Currency { get; set; } @@ -65,6 +66,7 @@ namespace BTCPayServer.Controllers public bool ShowCustomAmount { get; set; } public bool ShowDiscount { get; set; } public bool EnableTips { get; set; } + public RequiresRefundEmail RequiresRefundEmail { get; set; } public const string BUTTON_TEXT_DEF = "Buy for {0}"; public string ButtonText { get; set; } = BUTTON_TEXT_DEF; @@ -118,7 +120,8 @@ namespace BTCPayServer.Controllers NotificationUrl = settings.NotificationUrl, RedirectUrl = settings.RedirectUrl, SearchTerm = $"storeid:{app.StoreDataId}", - RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "" + RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "", + RequiresRefundEmail = settings.RequiresRefundEmail }; if (HttpContext?.Request != null) { @@ -202,7 +205,8 @@ namespace BTCPayServer.Controllers RedirectUrl = vm.RedirectUrl, Description = vm.Description, EmbeddedCSS = vm.EmbeddedCSS, - RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically) + RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically), + RequiresRefundEmail = vm.RequiresRefundEmail, }); await _AppService.UpdateOrCreateApp(app); TempData[WellKnownTempData.SuccessMessage] = "App updated"; diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index bff5ec13c..e05bf7d25 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -101,7 +101,8 @@ namespace BTCPayServer.Controllers CustomLogoLink = storeBlob.CustomLogo, AppId = appId, Description = settings.Description, - EmbeddedCSS = settings.EmbeddedCSS + EmbeddedCSS = settings.EmbeddedCSS, + RequiresRefundEmail = settings.RequiresRefundEmail }); } @@ -120,7 +121,9 @@ namespace BTCPayServer.Controllers string notificationUrl, string redirectUrl, string choiceKey, - string posData = null, CancellationToken cancellationToken = default) + string posData = null, + RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore, + CancellationToken cancellationToken = default) { var app = await _AppService.GetApp(appId, AppType.PointOfSale); if (string.IsNullOrEmpty(choiceKey) && amount <= 0) @@ -226,6 +229,9 @@ namespace BTCPayServer.Controllers PosData = string.IsNullOrEmpty(posData) ? null : posData, RedirectAutomatically = settings.RedirectAutomatically, SupportedTransactionCurrencies = paymentMethods, + RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore + ? store.GetStoreBlob().RequiresRefundEmail + : requiresRefundEmail == RequiresRefundEmail.On, }, store, HttpContext.Request.GetAbsoluteRoot(), new List() { AppService.GetAppInternalTag(appId) }, cancellationToken); diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index a0c5cf780..1ff35f8df 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -420,6 +420,7 @@ namespace BTCPayServer.Controllers.GreenField SpeedPolicy = entity.SpeedPolicy, DefaultLanguage = entity.DefaultLanguage, RedirectAutomatically = entity.RedirectAutomatically, + RequiresRefundEmail = entity.RequiresRefundEmail, RedirectURL = entity.RedirectURLTemplate } }; diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index eb1b17378..245d0905d 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -18,8 +18,8 @@ using BTCPayServer.HostedServices; using BTCPayServer.Logging; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; -using BTCPayServer.Payments.Lightning; using BTCPayServer.Rating; +using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices.Export; using BTCPayServer.Services.Rates; @@ -30,7 +30,6 @@ using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NBitcoin; -using NBitcoin.RPC; using NBitpayClient; using NBXplorer; using Newtonsoft.Json.Linq; @@ -564,7 +563,7 @@ namespace BTCPayServer.Controllers IsUnsetTopUp = invoice.IsUnsetTopUp(), OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice), CustomerEmail = invoice.RefundMail, - RequiresRefundEmail = storeBlob.RequiresRefundEmail, + RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail, ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes, @@ -873,7 +872,10 @@ namespace BTCPayServer.Controllers }), DefaultPaymentMethod = model.DefaultPaymentMethod, NotificationEmail = model.NotificationEmail, - ExtendedNotifications = model.NotificationEmail != null + ExtendedNotifications = model.NotificationEmail != null, + RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore + ? store.GetStoreBlob().RequiresRefundEmail + : model.RequiresRefundEmail == RequiresRefundEmail.On }, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken); TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!"; diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 3d146a5a5..cbbead887 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -127,6 +127,7 @@ namespace BTCPayServer.Controllers entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite; entity.RedirectAutomatically = invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically); + entity.RequiresRefundEmail = invoice.RequiresRefundEmail; entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); IPaymentFilter? excludeFilter = null; @@ -151,6 +152,7 @@ namespace BTCPayServer.Controllers } entity.PaymentTolerance = storeBlob.PaymentTolerance; entity.DefaultPaymentMethod = invoice.DefaultPaymentMethod; + entity.RequiresRefundEmail = invoice.RequiresRefundEmail; return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken); } @@ -178,6 +180,7 @@ namespace BTCPayServer.Controllers entity.DefaultLanguage = invoice.Checkout.DefaultLanguage; entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod; entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically; + entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail; IPaymentFilter? excludeFilter = null; if (invoice.Checkout.PaymentMethods != null) { @@ -188,6 +191,7 @@ namespace BTCPayServer.Controllers } entity.PaymentTolerance = invoice.Checkout.PaymentTolerance ?? storeBlob.PaymentTolerance; entity.RedirectURLTemplate = invoice.Checkout.RedirectURL?.Trim(); + entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail; if (additionalTags != null) entity.InternalTags.AddRange(additionalTags); return await CreateInvoiceCoreRaw(entity, store, excludeFilter, invoice.AdditionalSearchTerms, cancellationToken); diff --git a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs index e217bc73b..6aa930e6f 100644 --- a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs @@ -89,5 +89,7 @@ namespace BTCPayServer.Models.AppViewModels [Display(Name = "Custom CSS Code")] public string EmbeddedCSS { get; set; } public string Description { get; set; } + [Display(Name = "Require refund email on checkout")] + public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore; } } diff --git a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs index fe1121a3d..25741d4b9 100644 --- a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs @@ -64,5 +64,6 @@ namespace BTCPayServer.Models.AppViewModels public string Description { get; set; } [Display(Name = "Custom CSS Code")] public string EmbeddedCSS { get; set; } + public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore; } } diff --git a/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs b/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs index a6f9d32a5..e739bd4fd 100644 --- a/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs +++ b/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs @@ -79,6 +79,8 @@ namespace BTCPayServer.Models [JsonProperty(PropertyName = "redirectAutomatically", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool? RedirectAutomatically { get; set; } + [JsonProperty(PropertyName = "requiresRefundEmail", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? RequiresRefundEmail { get; set; } //Bitpay compatibility: create invoice in btcpay uses this instead of supportedTransactionCurrencies [JsonProperty(PropertyName = "paymentCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)] diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index d074a4c3e..058cd2a78 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using BTCPayServer.Validation; using Microsoft.AspNetCore.Mvc.Rendering; +using BTCPayServer.Services.Apps; namespace BTCPayServer.Models.InvoicingModels { @@ -87,5 +88,11 @@ namespace BTCPayServer.Models.InvoicingModels { get; set; } + + [Display(Name = "Require Refund Email")] + public RequiresRefundEmail RequiresRefundEmail + { + get; set; + } } } diff --git a/BTCPayServer/Services/Apps/AppType.cs b/BTCPayServer/Services/Apps/AppType.cs index b3fa13d1a..cfc162165 100644 --- a/BTCPayServer/Services/Apps/AppType.cs +++ b/BTCPayServer/Services/Apps/AppType.cs @@ -18,4 +18,14 @@ namespace BTCPayServer.Services.Apps [Display(Name = "Keypad only")] Light } + + public enum RequiresRefundEmail + { + [Display(Name = "Inherit from store settings")] + InheritFromStore, + [Display(Name = "On")] + On, + [Display(Name = "Off")] + Off + } } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 39c69460f..81594d1d8 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -378,6 +378,7 @@ namespace BTCPayServer.Services.Invoices } #pragma warning restore CS0618 public bool Refundable { get; set; } + public bool? RequiresRefundEmail { get; set; } = null; public string RefundMail { get; set; } [JsonProperty("redirectURL")] public string RedirectURLTemplate { get; set; } diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index dfe653698..b98bb2da2 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -57,6 +57,11 @@ +
+ + + +

Discounts

diff --git a/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml b/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml index 958bd3ec3..6cd7f7f92 100644 --- a/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml +++ b/BTCPayServer/Views/AppsPublic/PointOfSale/Static.cshtml @@ -37,12 +37,14 @@ {
+ @{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Value, item.Price.Value);}
} else {
+ @@ -94,6 +96,7 @@ {
@Model.CurrencySymbol +
diff --git a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml index 02cc56526..86fd5ca90 100644 --- a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml +++ b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml @@ -1,4 +1,5 @@ @model BTCPayServer.Models.InvoicingModels.CreateInvoiceModel +@using BTCPayServer.Services.Apps @{ ViewData.SetActivePageAndTitle(InvoiceNavPages.Create, "Create an invoice"); } @@ -64,6 +65,11 @@
+
+ + + +
diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index d1c22bf1e..17510b1c7 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -1094,6 +1094,11 @@ "nullable": true, "description": "When the customer has paid the invoice, and a `redirectURL` is set, the checkout is redirected to `redirectURL` automatically if `redirectAutomatically` is true. Defaults to the store's settings. (The default store settings is false)" }, + "requiresRefundEmail": { + "type": "boolean", + "nullable": true, + "description": "Invoice will require user to provide a refund email if this option is set to `true`. Has no effect if `buyerEmail` metadata is set as there is no email to collect in this case." + }, "defaultLanguage": { "type": "string", "nullable": true,