diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 6bec76f44..fdb4c9155 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -828,19 +828,93 @@ namespace BTCPayServer.Tests Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource); s.Driver.FindElement(By.Id("CreateEmailRule")).Click(); - var select = new SelectElement(s.Driver.FindElement(By.Id("Rules_0__Trigger"))); - select.SelectByText("An invoice has been settled", true); - s.Driver.FindElement(By.Id("Rules_0__To")).SendKeys("test@gmail.com"); - s.Driver.FindElement(By.Id("Rules_0__CustomerEmail")).Click(); - s.Driver.FindElement(By.Id("Rules_0__Subject")).SendKeys("Thanks!"); + var select = new SelectElement(s.Driver.FindElement(By.Id("Trigger"))); + select.SelectByValue("InvoicePaymentSettled"); + s.Driver.FindElement(By.Id("To")).SendKeys("test@gmail.com"); + s.Driver.FindElement(By.Id("CustomerEmail")).Click(); + s.Driver.FindElement(By.Id("Subject")).SendKeys("Thanks!"); s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your invoice is settled"); s.Driver.FindElement(By.Id("SaveEmailRules")).Click(); - Assert.Contains("Store email rules saved", s.FindAlertMessage().Text); + // we now have a rule + Assert.DoesNotContain("There are no rules yet.", s.Driver.PageSource); + Assert.Contains("test@gmail.com", s.Driver.PageSource); s.GoToStore(StoreNavPages.Emails); Assert.True(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected); } + [Fact(Timeout = TestTimeout)] + public async Task CanSetupEmailRules() + { + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(true); + s.CreateNewStore(); + + // Store Email Rules + s.GoToStore(StoreNavPages.Emails); + s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click(); + Assert.Contains("There are no rules yet.", s.Driver.PageSource); + Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource); + + // invoice created rule + s.Driver.FindElement(By.Id("CreateEmailRule")).Click(); + var select = new SelectElement(s.Driver.FindElement(By.Id("Trigger"))); + select.SelectByValue("InvoiceCreated"); + s.Driver.FindElement(By.Id("To")).SendKeys("invoicecreated@gmail.com"); + s.Driver.FindElement(By.Id("CustomerEmail")).Click(); + s.Driver.FindElement(By.Id("SaveEmailRules")).Click(); + + // Ensure that the rule is created + Assert.DoesNotContain("There are no rules yet.", s.Driver.PageSource); + Assert.Contains("invoicecreated@gmail.com", s.Driver.PageSource); + Assert.Contains("Invoice {Invoice.Id} created", s.Driver.PageSource); + Assert.Contains("Yes", s.Driver.PageSource); + + // payment request status changed rule + s.Driver.FindElement(By.Id("CreateEmailRule")).Click(); + select = new SelectElement(s.Driver.FindElement(By.Id("Trigger"))); + select.SelectByValue("PaymentRequestStatusChanged"); + s.Driver.FindElement(By.Id("To")).SendKeys("statuschanged@gmail.com"); + s.Driver.FindElement(By.Id("Subject")).SendKeys("Status changed!"); + s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your Payment Request Status is Changed"); + s.Driver.FindElement(By.Id("SaveEmailRules")).Click(); + + // Validate the second rule is added + Assert.Contains("statuschanged@gmail.com", s.Driver.PageSource); + Assert.Contains("Status changed!", s.Driver.PageSource); + + // Select the second rule’s edit button + var editButtons = s.Driver.FindElements(By.XPath("//a[contains(text(), 'Edit')]")); + Assert.True(editButtons.Count >= 2, "Expected at least two edit buttons but found fewer."); + + editButtons[1].Click(); // Clicks the second Edit button + + // Modify the second rule from statuschanged@gmail.com to changedagain@gmail.com + var toField = s.Driver.FindElement(By.Id("To")); + toField.Clear(); + toField.SendKeys("changedagain@gmail.com"); + s.Driver.FindElement(By.Id("SaveEmailRules")).Click(); + + // Validate that the email is updated in the list of email rules + Assert.Contains("changedagain@gmail.com", s.Driver.PageSource); + Assert.DoesNotContain("statuschanged@gmail.com", s.Driver.PageSource); + + // Delete both email rules + var deleteLinks = s.Driver.FindElements(By.XPath("//a[contains(text(), 'Delete')]")); + Assert.True(deleteLinks.Count == 2, "Expected exactly two delete buttons but found a different number."); + + deleteLinks[0].Click(); + + deleteLinks = s.Driver.FindElements(By.XPath("//a[contains(text(), 'Delete')]")); // Refresh list + Assert.True(deleteLinks.Count == 1, "Expected one delete button remaining."); + + deleteLinks[0].Click(); + + // Validate that there are no more rules + Assert.Contains("There are no rules yet.", s.Driver.PageSource); + } + [Fact(Timeout = TestTimeout)] public async Task CanUseDynamicDns() { @@ -3883,7 +3957,6 @@ retry: s.Server.ActivateLightning(); await s.StartAsync(); await s.Server.EnsureChannelsSetup(); - var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails", "email-settings", "forms"}; // Setup user, store and wallets s.RegisterNewUser(); @@ -3916,6 +3989,10 @@ retry: s.AssertPageAccess(false, GetStorePath("lightning/BTC")); s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings")); s.AssertPageAccess(false, GetStorePath("apps/create")); + + var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", + "payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", + "emails/rules", "email-settings", "forms"}; foreach (var path in storeSettingsPaths) { // should have view access to settings, but no submit buttons or create links TestLogs.LogInformation($"Checking access to store page {path} as admin"); @@ -3936,7 +4013,8 @@ retry: s.Server.ActivateLightning(); await s.StartAsync(); await s.Server.EnsureChannelsSetup(); - var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails", "email-settings", "forms"}; + var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors", + "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails/rules", "email-settings", "forms"}; // Setup users var manager = s.RegisterNewUser(); diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index 27d649a5f..75ad21ceb 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -19,154 +19,6 @@ namespace BTCPayServer.Controllers; public partial class UIStoresController { - [HttpGet("{storeId}/emails")] - public async Task StoreEmails(string storeId) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); - - var configured = await _emailSenderFactory.IsComplete(store.Id); - if (!configured && !TempData.HasStatusMessage()) - { - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Warning, - Html = "You need to configure email settings before this feature works." + - $" Configure store email settings." - }); - } - - var vm = new StoreEmailRuleViewModel { Rules = store.GetStoreBlob().EmailRules ?? [] }; - return View(vm); - } - - [HttpPost("{storeId}/emails")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command) - { - vm.Rules ??= []; - int commandIndex = 0; - - var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries); - if (indSep.Length > 1) - { - commandIndex = int.Parse(indSep[1], CultureInfo.InvariantCulture); - } - - if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase)) - { - vm.Rules.RemoveAt(commandIndex); - } - else if (command == "add") - { - vm.Rules.Add(new StoreEmailRule()); - - return View(vm); - } - - for (var i = 0; i < vm.Rules.Count; i++) - { - var rule = vm.Rules[i]; - - if (!string.IsNullOrEmpty(rule.To) && rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Any(s => !MailboxAddressValidator.TryParse(s, out _))) - { - ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", - StringLocalizer["Invalid mailbox address provided. Valid formats are: '{0}' or '{1}'", "test@example.com", "Firstname Lastname "]); - } - else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To)) - ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", - StringLocalizer["Either recipient or \"Send the email to the buyer\" is required"]); - } - - if (!ModelState.IsValid) - { - return View(vm); - } - - var store = HttpContext.GetStoreData(); - - if (store == null) - return NotFound(); - - string message = ""; - - // update rules - var blob = store.GetStoreBlob(); - blob.EmailRules = vm.Rules; - if (store.SetStoreBlob(blob)) - { - await _storeRepo.UpdateStore(store); - message += StringLocalizer["Store email rules saved."] + " "; - } - - if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase)) - { - try - { - var rule = vm.Rules[commandIndex]; - if (await _emailSenderFactory.IsComplete(store.Id)) - { - var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries) - .Select(o => - { - MailboxAddressValidator.TryParse(o, out var mb); - return mb; - }) - .Where(o => o != null) - .ToArray(); - - var emailSender = await _emailSenderFactory.GetEmailSender(store.Id); - emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body); - message += StringLocalizer["Test email sent — please verify you received it."]; - } - else - { - message += StringLocalizer["Complete the email setup to send test emails."]; - } - } - catch (Exception ex) - { - TempData[WellKnownTempData.ErrorMessage] = message + StringLocalizer["Error sending test email: {0}", ex.Message].Value; - return RedirectToAction("StoreEmails", new { storeId }); - } - } - - if (!string.IsNullOrEmpty(message)) - { - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Success, - Message = message - }); - } - - return RedirectToAction("StoreEmails", new { storeId }); - } - - public class StoreEmailRuleViewModel - { - public List Rules { get; set; } - } - - public class StoreEmailRule - { - [Required] - public string Trigger { get; set; } - - public bool CustomerEmail { get; set; } - - - public string To { get; set; } - - [Required] - public string Subject { get; set; } - - [Required] - public string Body { get; set; } - } - [HttpGet("{storeId}/email-settings")] public async Task StoreEmailSettings(string storeId) { diff --git a/BTCPayServer/Controllers/UIStoresController.EmailRules.cs b/BTCPayServer/Controllers/UIStoresController.EmailRules.cs new file mode 100644 index 000000000..5a66d0b22 --- /dev/null +++ b/BTCPayServer/Controllers/UIStoresController.EmailRules.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Client; +using BTCPayServer.Data; +using BTCPayServer.Models; +using BTCPayServer.Services.Mails; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class UIStoresController + { + [HttpGet("{storeId}/emails/rules")] + public async Task StoreEmailRulesList(string storeId) + { + var store = HttpContext.GetStoreData(); + if (store == null) return NotFound(); + + var configured = await _emailSenderFactory.IsComplete(store.Id); + if (!configured && !TempData.HasStatusMessage()) + { + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Warning, + Html = "You need to configure email settings before this feature works." + + $" Configure store email settings." + }); + } + + var rules = store.GetStoreBlob().EmailRules ?? new List(); + return View("StoreEmailRulesList", rules); + } + + [HttpGet("{storeId}/emails/rules/create")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult StoreEmailRulesCreate(string storeId) + { + return View("StoreEmailRulesManage", new StoreEmailRule()); + } + + [HttpPost("{storeId}/emails/rules/create")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task StoreEmailRulesCreate(string storeId, StoreEmailRule model) + { + if (!ModelState.IsValid) + return View("StoreEmailRulesManage", model); + + var store = await _storeRepo.FindStore(storeId); + if (store == null) return NotFound(); + + var blob = store.GetStoreBlob(); + var rulesList = blob.EmailRules ?? new List(); + rulesList.Add(new StoreEmailRule + { + Trigger = model.Trigger, + CustomerEmail = model.CustomerEmail, + To = model.To, + Subject = model.Subject, + Body = model.Body + }); + + blob.EmailRules = rulesList; + store.SetStoreBlob(blob); + await _storeRepo.UpdateStore(store); + + return RedirectToAction(nameof(StoreEmailRulesList), new { storeId }); + } + + [HttpGet("{storeId}/emails/rules/{ruleIndex}/edit")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult StoreEmailRulesEdit(string storeId, int ruleIndex) + { + var store = HttpContext.GetStoreData(); + if (store == null) return NotFound(); + + var rules = store.GetStoreBlob().EmailRules; + if (rules == null || ruleIndex >= rules.Count) return NotFound(); + + var rule = rules[ruleIndex]; + return View("StoreEmailRulesManage", rule); + } + + [HttpPost("{storeId}/emails/rules/{ruleIndex}/edit")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task StoreEmailRulesEdit(string storeId, int ruleIndex, StoreEmailRule model) + { + if (!ModelState.IsValid) + return View("StoreEmailRulesManage", model); + + var store = await _storeRepo.FindStore(storeId); + if (store == null) return NotFound(); + + var blob = store.GetStoreBlob(); + if (blob.EmailRules == null || ruleIndex >= blob.EmailRules.Count) return NotFound(); + + var rule = blob.EmailRules[ruleIndex]; + rule.Trigger = model.Trigger; + rule.CustomerEmail = model.CustomerEmail; + rule.To = model.To; + rule.Subject = model.Subject; + rule.Body = model.Body; + store.SetStoreBlob(blob); + await _storeRepo.UpdateStore(store); + + return RedirectToAction(nameof(StoreEmailRulesList), new { storeId }); + } + + [HttpPost("{storeId}/emails/rules/{ruleIndex}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task StoreEmailRulesDelete(string storeId, int ruleIndex) + { + var store = await _storeRepo.FindStore(storeId); + if (store == null) return NotFound(); + + var blob = store.GetStoreBlob(); + if (blob.EmailRules == null || ruleIndex >= blob.EmailRules.Count) return NotFound(); + + blob.EmailRules.RemoveAt(ruleIndex); + store.SetStoreBlob(blob); + await _storeRepo.UpdateStore(store); + + return RedirectToAction(nameof(StoreEmailRulesList), new { storeId }); + } + + public class StoreEmailRule + { + [Required] + public string Trigger { get; set; } + + public bool CustomerEmail { get; set; } + + public string To { get; set; } + + [Required] + public string Subject { get; set; } + + [Required] + public string Body { get; set; } + } + } + +} diff --git a/BTCPayServer/HostedServices/PendingTransactionService.cs b/BTCPayServer/HostedServices/PendingTransactionService.cs index 92bdfc3d1..45dbb533d 100644 --- a/BTCPayServer/HostedServices/PendingTransactionService.cs +++ b/BTCPayServer/HostedServices/PendingTransactionService.cs @@ -1,14 +1,21 @@ #nullable enable using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Events; +using BTCPayServer.HostedServices.Webhooks; using BTCPayServer.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices; @@ -239,4 +246,5 @@ public class PendingTransactionService( pt.State = PendingTransactionState.Broadcast; await ctx.SaveChangesAsync(); } + } diff --git a/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs index 25d87f79b..7be8a8546 100644 --- a/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs @@ -22,13 +22,13 @@ public class InvoiceWebhookProvider : WebhookProvider { return new Dictionary { - {WebhookEventType.InvoiceCreated, "A new invoice has been created"}, - {WebhookEventType.InvoiceReceivedPayment, "A new payment has been received"}, - {WebhookEventType.InvoicePaymentSettled, "A payment has been settled"}, - {WebhookEventType.InvoiceProcessing, "An invoice is processing"}, - {WebhookEventType.InvoiceExpired, "An invoice has expired"}, - {WebhookEventType.InvoiceSettled, "An invoice has been settled"}, - {WebhookEventType.InvoiceInvalid, "An invoice became invalid"}, + {WebhookEventType.InvoiceCreated, "Invoice - Created"}, + {WebhookEventType.InvoiceReceivedPayment, "Invoice - Received Payment"}, + {WebhookEventType.InvoicePaymentSettled, "Invoice - Payment Settled"}, + {WebhookEventType.InvoiceProcessing, "Invoice - Is Processing"}, + {WebhookEventType.InvoiceExpired, "Invoice - Expired"}, + {WebhookEventType.InvoiceSettled, "Invoice - Is Settled"}, + {WebhookEventType.InvoiceInvalid, "Invoice - Became Invalid"}, }; } diff --git a/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs index d6fb48bcd..c018b3ea4 100644 --- a/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs @@ -18,10 +18,10 @@ public class PaymentRequestWebhookProvider: WebhookProvider { return new Dictionary() { - {WebhookEventType.PaymentRequestCreated, "Payment Request Created"}, - {WebhookEventType.PaymentRequestUpdated, "Payment Request Updated"}, - {WebhookEventType.PaymentRequestArchived, "Payment Request Archived"}, - {WebhookEventType.PaymentRequestStatusChanged, "Payment Request Status Changed"}, + {WebhookEventType.PaymentRequestCreated, "Payment Request - Created"}, + {WebhookEventType.PaymentRequestUpdated, "Payment Request - Updated"}, + {WebhookEventType.PaymentRequestArchived, "Payment Request - Archived"}, + {WebhookEventType.PaymentRequestStatusChanged, "Payment Request - Status Changed"}, }; } diff --git a/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs index 4a37064f0..28874fafd 100644 --- a/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs @@ -36,9 +36,9 @@ public class PayoutWebhookProvider(EventAggregator eventAggregator, ILogger() { - {WebhookEventType.PayoutCreated, "A payout has been created"}, - {WebhookEventType.PayoutApproved, "A payout has been approved"}, - {WebhookEventType.PayoutUpdated, "A payout was updated"} + {WebhookEventType.PayoutCreated, "Payout - Created"}, + {WebhookEventType.PayoutApproved, "Payout - Approved"}, + {WebhookEventType.PayoutUpdated, "Payout - Updated"} }; } diff --git a/BTCPayServer/Services/Translations.Default.cs b/BTCPayServer/Services/Translations.Default.cs index 77d434cff..47a536c28 100644 --- a/BTCPayServer/Services/Translations.Default.cs +++ b/BTCPayServer/Services/Translations.Default.cs @@ -523,6 +523,8 @@ namespace BTCPayServer.Services "Email password reset functionality is not configured for this server. Please contact the server administrator to assist with account recovery.": "", "email rules": "", "Email Rules": "", + "Create Email Rule": "", + "Edit Email Rule": "", "Email rules allow BTCPay Server to send customized emails from your store based on events.": "", "Email sent to {0}. Please verify you received it.": "", "Email Server": "", diff --git a/BTCPayServer/Views/UIStores/StoreEmailRulesList.cshtml b/BTCPayServer/Views/UIStores/StoreEmailRulesList.cshtml new file mode 100644 index 000000000..e949b7fbf --- /dev/null +++ b/BTCPayServer/Views/UIStores/StoreEmailRulesList.cshtml @@ -0,0 +1,70 @@ +@using BTCPayServer.Client +@using BTCPayServer.TagHelpers +@using Microsoft.AspNetCore.Mvc.TagHelpers +@model List + +@{ + var storeId = Context.GetStoreData().Id; + ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer["Email Rules"], storeId); +} + + + +

+ Email rules allow BTCPay Server to send customized emails from your store based on events. +

+ +@if (Model.Any()) +{ +
+ + + + + + + + + + + + @foreach (var rule in Model.Select((value, index) => new { value, index })) + { + + + + + + + + } + +
TriggerCustomer EmailToSubjectActions
@rule.value.Trigger@(rule.value.CustomerEmail ? "Yes" : "No")@rule.value.To@rule.value.Subject + Edit + - +
+ Delete +
+
+
+} +else +{ +

+ There are no rules yet. +

+} diff --git a/BTCPayServer/Views/UIStores/StoreEmailRulesManage.cshtml b/BTCPayServer/Views/UIStores/StoreEmailRulesManage.cshtml new file mode 100644 index 000000000..563d2a749 --- /dev/null +++ b/BTCPayServer/Views/UIStores/StoreEmailRulesManage.cshtml @@ -0,0 +1,186 @@ +@using BTCPayServer.HostedServices.Webhooks +@using Microsoft.AspNetCore.Mvc.TagHelpers +@model BTCPayServer.Controllers.UIStoresController.StoreEmailRule +@inject WebhookSender WebhookSender + +@{ + var storeId = Context.GetStoreData().Id; + bool isEdit = Model.Trigger != null; + ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer[isEdit ? "Edit Email Rule" : "Create Email Rule"], storeId); +} + +@section PageHeadContent { + +} + +
+ + + +
+ + + +
Choose what event sends the email.
+
+ +
+ + + +
Who to send the email to. For multiple emails, separate with a comma.
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Placeholders
Invoice + {Invoice.Id}, + {Invoice.StoreId}, + {Invoice.Price}, + {Invoice.Currency}, + {Invoice.Status}, + {Invoice.AdditionalStatus}, + {Invoice.OrderId} + {Invoice.Metadata}* +
Request + {PaymentRequest.Id}, + {PaymentRequest.Price}, + {PaymentRequest.Currency}, + {PaymentRequest.Title}, + {PaymentRequest.Description}, + {PaymentRequest.Status} + {PaymentRequest.FormResponse}* +
Payout + {Payout.Id}, + {Payout.PullPaymentId}, + {Payout.Destination}, + {Payout.State} + {Payout.Metadata}* +
* These fields are JSON objects. You can access properties within them using this syntax. One example is {Invoice.Metadata.itemCode}
+
+
+ + + +@section PageFootContent { + + + + +} diff --git a/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml b/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml index cad31f50b..e228c8503 100644 --- a/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml +++ b/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml @@ -38,7 +38,8 @@

Email Rules

Email rules allow BTCPay Server to send customized emails from your store based on events.

- + Configure
diff --git a/BTCPayServer/Views/UIStores/StoreEmails.cshtml b/BTCPayServer/Views/UIStores/StoreEmails.cshtml deleted file mode 100644 index 99e96577c..000000000 --- a/BTCPayServer/Views/UIStores/StoreEmails.cshtml +++ /dev/null @@ -1,211 +0,0 @@ -@using BTCPayServer.HostedServices.Webhooks -@using Microsoft.AspNetCore.Mvc.TagHelpers -@using BTCPayServer.Client -@using BTCPayServer.Abstractions.TagHelpers -@model BTCPayServer.Controllers.UIStoresController.StoreEmailRuleViewModel -@inject WebhookSender WebhookSender - -@{ - var storeId = Context.GetStoreData().Id; - ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer["Email Rules"], storeId); -} - -@section PageHeadContent { - -} - -
- - -

- Email rules allow BTCPay Server to send customized emails from your store based on events. -

-
-
- @if (!ViewContext.ModelState.IsValid) - { -
- } - @if (Model.Rules.Any()) - { -
    - @for (var index = 0; index < Model.Rules.Count; index++) - { - - } -
- } - else - { -

- There are no rules yet. -

- } -
-
- - -@section PageFootContent { - - - -}