Compare commits
2 Commits
master
...
form_build
Author | SHA1 | Date | |
---|---|---|---|
7aa1cd6ac3 | |||
11fd7ecedd |
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AngleSharp.Common;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -105,12 +106,17 @@ public class Form
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyValuesFromForm(IFormCollection form)
|
||||
|
||||
public void ApplyValuesFromForm(IFormCollection form, string ignorePrefix = null)
|
||||
{
|
||||
var names = GetAllNames();
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (ignorePrefix is not null && name.StartsWith(ignorePrefix))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var field = GetFieldByName(name);
|
||||
if (field is null || !form.TryGetValue(name, out var val))
|
||||
{
|
||||
@ -121,6 +127,39 @@ public class Form
|
||||
}
|
||||
}
|
||||
|
||||
public void SetValues(Dictionary<string, object> values)
|
||||
{
|
||||
SetValues(values, null, null);
|
||||
}
|
||||
private void SetValues(Dictionary<string, object> values, List<Field> fields = null,string prefix = null)
|
||||
{
|
||||
foreach (var v in values)
|
||||
{
|
||||
var field = GetFieldByName(v.Key, fields?? Fields, prefix);
|
||||
if (field is null )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.Fields.Any())
|
||||
{
|
||||
if (v.Value is Dictionary<string, object> dict)
|
||||
{
|
||||
SetValues(dict, field.Fields, field.Name + "_");
|
||||
}else if (v.Value is JObject jObject)
|
||||
{
|
||||
dict = jObject.ToObject<Dictionary<string, object>>();
|
||||
SetValues(dict, field.Fields, field.Name + "_");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
field.Value = (string) v.Value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetValues()
|
||||
{
|
||||
return GetValues(Fields);
|
||||
|
@ -69,6 +69,7 @@ namespace BTCPayServer.Data
|
||||
public DbSet<WebhookData> Webhooks { get; set; }
|
||||
public DbSet<LightningAddressData> LightningAddresses { get; set; }
|
||||
public DbSet<PayoutProcessorData> PayoutProcessors { get; set; }
|
||||
public DbSet<FormData> Forms { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@ -122,6 +123,7 @@ namespace BTCPayServer.Data
|
||||
LightningAddressData.OnModelCreating(builder);
|
||||
PayoutProcessorData.OnModelCreating(builder);
|
||||
//WebhookData.OnModelCreating(builder);
|
||||
FormData.OnModelCreating(builder, Database);
|
||||
|
||||
|
||||
if (Database.IsSqlite() && !_designTime)
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
@ -6,7 +6,25 @@ namespace BTCPayServer.Data.Data;
|
||||
|
||||
public class FormData
|
||||
{
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public string Config { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<FormData>()
|
||||
.HasOne(o => o.Store)
|
||||
.WithMany(o => o.Forms).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<FormData>().HasIndex(o => o.StoreId);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<FormData>()
|
||||
.Property(o => o.Config)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ namespace BTCPayServer.Data
|
||||
public IEnumerable<PayoutData> Payouts { get; set; }
|
||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
public IEnumerable<FormData> Forms { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
|
52
BTCPayServer.Data/Migrations/20220919105333_FormBuilder.cs
Normal file
52
BTCPayServer.Data/Migrations/20220919105333_FormBuilder.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20220919105333_FormBuilder")]
|
||||
public partial class FormBuilder : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Forms",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Config = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Forms", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Forms_Stores_StoreId",
|
||||
column: x => x.StoreId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Forms_StoreId",
|
||||
table: "Forms",
|
||||
column: "StoreId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Forms");
|
||||
}
|
||||
}
|
||||
}
|
@ -205,6 +205,28 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("CustodianAccount");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Data.FormData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("Forms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -1129,6 +1151,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Data.FormData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("Forms")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
@ -1519,6 +1551,8 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.Navigation("CustodianAccounts");
|
||||
|
||||
b.Navigation("Forms");
|
||||
|
||||
b.Navigation("Invoices");
|
||||
|
||||
b.Navigation("LightningAddresses");
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
@ -40,6 +41,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
private FormComponentProviders FormProviders { get; }
|
||||
public FormDataService FormDataService { get; }
|
||||
|
||||
public UIPaymentRequestController(
|
||||
UIInvoiceController invoiceController,
|
||||
@ -50,7 +52,8 @@ namespace BTCPayServer.Controllers
|
||||
CurrencyNameTable currencies,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceRepository invoiceRepository,
|
||||
FormComponentProviders formProviders)
|
||||
FormComponentProviders formProviders,
|
||||
FormDataService formDataService)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
@ -61,6 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
_storeRepository = storeRepository;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
FormProviders = formProviders;
|
||||
FormDataService = formDataService;
|
||||
}
|
||||
|
||||
[BitpayAPIConstraint(false)]
|
||||
@ -193,7 +197,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("{payReqId}/form")]
|
||||
[HttpPost("{payReqId}/form")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId)
|
||||
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId, FormViewModel viewModel)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
|
||||
if (result == null)
|
||||
@ -202,42 +206,33 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var prBlob = result.GetBlob();
|
||||
var prFormId = prBlob.FormId;
|
||||
var formConfig = prFormId is null ? null : Forms.UIFormsController.GetFormData(prFormId)?.Config;
|
||||
switch (formConfig)
|
||||
if (prBlob.FormResponse is not null)
|
||||
{
|
||||
case null:
|
||||
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is not null:
|
||||
return RedirectToAction("ViewPaymentRequest", new { payReqId });
|
||||
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is null:
|
||||
break;
|
||||
default:
|
||||
// POST case: Handle form submit
|
||||
var formData = Form.Parse(formConfig);
|
||||
formData.ApplyValuesFromForm(Request.Form);
|
||||
if (FormProviders.Validate(formData, ModelState))
|
||||
{
|
||||
prBlob.FormResponse = JObject.FromObject(formData.GetValues());
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
}
|
||||
break;
|
||||
return RedirectToAction("PayPaymentRequest", new {payReqId});
|
||||
}
|
||||
var prFormId = prBlob.FormId;
|
||||
var formData = prFormId is null ? null : (await FormDataService.GetForm(prFormId));
|
||||
if (formData is null)
|
||||
{
|
||||
|
||||
return RedirectToAction("PayPaymentRequest", new {payReqId});
|
||||
}
|
||||
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
var form = Form.Parse(formData.Config);
|
||||
if (Request.Method == "POST" && Request.HasFormContentType)
|
||||
{
|
||||
AspController = "UIForms",
|
||||
AspAction = "ViewPublicForm",
|
||||
RouteParameters =
|
||||
{
|
||||
{ "formId", prFormId }
|
||||
},
|
||||
FormParameters =
|
||||
{
|
||||
{ "redirectUrl", Request.GetCurrentUrl() }
|
||||
form.ApplyValuesFromForm(Request.Form);
|
||||
if (await FormDataService.Validate(form, ModelState))
|
||||
{ prBlob.FormResponse = JObject.FromObject(form.GetValues());
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new {payReqId});
|
||||
}
|
||||
});
|
||||
}
|
||||
viewModel.FormName = formData.Name;
|
||||
viewModel.Form = form;
|
||||
return View("Views/UIForms/View", viewModel);
|
||||
|
||||
}
|
||||
|
||||
[HttpGet("{payReqId}/pay")]
|
||||
@ -266,6 +261,15 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest("Payment Request cannot be paid as it has been archived");
|
||||
}
|
||||
|
||||
if (!result.FormSubmitted && !string.IsNullOrEmpty(result.FormId))
|
||||
{
|
||||
var formData = result.FormId is null ? null : (await FormDataService.GetForm(result.FormId));
|
||||
if (formData is not null)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequestForm", new {payReqId});
|
||||
}
|
||||
}
|
||||
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(Request);
|
||||
if (result.AmountDue <= 0)
|
||||
{
|
||||
|
@ -1,3 +1,4 @@
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
@ -14,6 +15,11 @@ public static class FormDataExtensions
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
|
||||
}
|
||||
|
||||
public static JObject Deserialize(this FormData form)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<JObject>(form.Config);
|
||||
}
|
||||
|
||||
public static string Serialize(this JObject form)
|
||||
{
|
||||
|
@ -2,16 +2,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class FormDataService
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
private readonly FormComponentProviders _formProviders;
|
||||
|
||||
public FormDataService(
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
FormComponentProviders formProviders)
|
||||
{
|
||||
_applicationDbContextFactory = applicationDbContextFactory;
|
||||
_formProviders = formProviders;
|
||||
}
|
||||
|
||||
public static readonly Form StaticFormEmail = new()
|
||||
{
|
||||
@ -32,4 +50,72 @@ public class FormDataService
|
||||
Field.Create("Country", "buyerCountry", null, true, null)
|
||||
}
|
||||
};
|
||||
private static Dictionary<string, (string selectText, string name, Form form)> HardcodedOptions = new()
|
||||
{
|
||||
{"", ("Do not request any information", null, null)},
|
||||
{"Email", ("Request email address only", "Provide your email address", StaticFormEmail )},
|
||||
{"Address", ("Request shipping address", "Provide your address", StaticFormAddress)},
|
||||
};
|
||||
|
||||
public async Task<SelectList> GetSelect(string storeId ,string selectedFormId)
|
||||
{
|
||||
var forms = await GetForms(storeId);
|
||||
return new SelectList(HardcodedOptions.Select(pair => new SelectListItem(pair.Value.selectText, pair.Key, selectedFormId == pair.Key)).Concat(forms.Select(data => new SelectListItem(data.Name, data.Id, data.Id == selectedFormId))),
|
||||
nameof(SelectListItem.Value), nameof(SelectListItem.Text));
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<FormData>> GetForms(string storeId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(storeId);
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
return await context.Forms.Where(data => data.StoreId == storeId).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<FormData?> GetForm(string storeId, string id)
|
||||
{
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
return await context.Forms.Where(data => data.Id == id && data.StoreId == storeId).FirstOrDefaultAsync();
|
||||
}
|
||||
public async Task<FormData?> GetForm(string id)
|
||||
{
|
||||
if (id is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HardcodedOptions.TryGetValue(id, out var hardcodedForm))
|
||||
{
|
||||
return new FormData
|
||||
{
|
||||
Config = hardcodedForm.form.ToString(),
|
||||
Id = id,
|
||||
Name = hardcodedForm.name
|
||||
};
|
||||
}
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
return await context.Forms.Where(data => data.Id == id).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveForm(string id, string storeId)
|
||||
{
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
var item = await context.Forms.SingleOrDefaultAsync(data => data.StoreId == storeId && id == data.Id);
|
||||
if (item is not null)
|
||||
context.Remove(item);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddOrUpdateForm(FormData data)
|
||||
{
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
|
||||
context.Update(data);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Validate(Form form, ModelStateDictionary modelState)
|
||||
{
|
||||
return _formProviders.Validate(form, modelState);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms.Models;
|
||||
|
||||
public class FormViewModel
|
||||
{
|
||||
|
||||
public string FormName { get; set; }
|
||||
public string RedirectUrl { get; set; }
|
||||
public FormData FormData { get; set; }
|
||||
Form _Form;
|
||||
public Form Form { get => _Form ??= Form.Parse(FormData.Config); }
|
||||
public Form Form { get; set; }
|
||||
public string AspController { get; set; }
|
||||
public string AspAction { get; set; }
|
||||
public Dictionary<string, string> RouteParameters { get; set; } = new();
|
||||
}
|
||||
|
@ -1,109 +1,196 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Data;
|
||||
using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public class UIFormsController : Controller
|
||||
{
|
||||
private readonly FormDataService _formDataService;
|
||||
private FormComponentProviders FormProviders { get; }
|
||||
|
||||
public UIFormsController(FormComponentProviders formProviders)
|
||||
public UIFormsController(FormComponentProviders formProviders, FormDataService formDataService)
|
||||
{
|
||||
FormProviders = formProviders;
|
||||
_formDataService = formDataService;
|
||||
}
|
||||
|
||||
[HttpGet("~/stores/{storeId}/forms")]
|
||||
public async Task<IActionResult> FormsList(string storeId)
|
||||
{
|
||||
var forms = await _formDataService.GetForms(storeId);
|
||||
|
||||
return View(forms);
|
||||
}
|
||||
|
||||
[HttpGet("~/stores/{storeId}/forms/new")]
|
||||
public IActionResult Create(string storeId)
|
||||
{
|
||||
var vm = new ModifyForm { FormConfig = new Form().ToString() };
|
||||
return View("Modify", vm);
|
||||
}
|
||||
|
||||
[HttpGet("~/stores/{storeId}/forms/modify/{id}")]
|
||||
public async Task<IActionResult> Modify(string storeId, string id)
|
||||
{
|
||||
var form = await _formDataService.GetForm(storeId, id);
|
||||
if (form is null) return NotFound();
|
||||
|
||||
var config = Form.Parse(form.Config);
|
||||
return View(new ModifyForm { Name = form.Name, FormConfig = config.ToString() });
|
||||
}
|
||||
|
||||
[HttpPost("~/stores/{storeId}/forms/modify/{id?}")]
|
||||
public async Task<IActionResult> Modify(string storeId, string? id, ModifyForm modifyForm)
|
||||
{
|
||||
if (id is not null)
|
||||
{
|
||||
var form = await _formDataService.GetForm(storeId, id);
|
||||
if (form is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
modifyForm.FormConfig = Form.Parse(modifyForm.FormConfig).ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(modifyForm.FormConfig), $"Form config was invalid: {ex.Message}");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(modifyForm);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var form = new FormData
|
||||
{
|
||||
Id = id,
|
||||
StoreId = storeId,
|
||||
Name = modifyForm.Name,
|
||||
Config = modifyForm.FormConfig
|
||||
};
|
||||
var isNew = id is null;
|
||||
await _formDataService.AddOrUpdateForm(form);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = $"Form {(isNew ? "created": "updated")} successfully."
|
||||
});
|
||||
if (isNew)
|
||||
{
|
||||
return RedirectToAction("Modify", new { storeId, id = form.Id });
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError("", $"An error occurred while saving: {e.Message}");
|
||||
}
|
||||
|
||||
return View(modifyForm);
|
||||
}
|
||||
|
||||
[HttpPost("~/stores/{storeId}/forms/{id}/remove")]
|
||||
public async Task<IActionResult> Remove(string storeId, string id)
|
||||
{
|
||||
await _formDataService.RemoveForm(id, storeId);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Form removed"
|
||||
});
|
||||
return RedirectToAction("FormsList", new { storeId });
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/forms/{formId}")]
|
||||
[HttpPost("~/forms")]
|
||||
public IActionResult ViewPublicForm(string? formId, string? redirectUrl)
|
||||
public async Task<IActionResult> ViewPublicForm(string? formId)
|
||||
{
|
||||
if (!IsValidRedirectUri(redirectUrl))
|
||||
return BadRequest();
|
||||
|
||||
FormData? formData = string.IsNullOrEmpty(formId) ? null : GetFormData(formId);
|
||||
// if (!IsValidRedirectUri(redirectUrl))
|
||||
// return BadRequest();
|
||||
|
||||
FormData? formData = string.IsNullOrEmpty(formId) ? null : await _formDataService.GetForm(formId);
|
||||
if (formData == null)
|
||||
{
|
||||
return string.IsNullOrEmpty(redirectUrl)
|
||||
? NotFound()
|
||||
: Redirect(redirectUrl);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return GetFormView(formData, redirectUrl);
|
||||
return GetFormView(formData);
|
||||
}
|
||||
|
||||
ViewResult GetFormView(FormData formData, string? redirectUrl)
|
||||
ViewResult GetFormView(FormData formData)
|
||||
{
|
||||
return View("View", new FormViewModel { FormData = formData, RedirectUrl = redirectUrl });
|
||||
return View("View", new FormViewModel {
|
||||
FormName = formData.Name,
|
||||
Form = Form.Parse(formData.Config) });
|
||||
}
|
||||
ViewResult GetFormView(Form form, string name)
|
||||
{
|
||||
return View("View", new FormViewModel {
|
||||
FormName = name,
|
||||
Form = form});
|
||||
}
|
||||
[AllowAnonymous]
|
||||
[HttpPost("~/forms/{formId}")]
|
||||
public IActionResult SubmitForm(string formId, string? redirectUrl, string? command)
|
||||
public async Task<IActionResult> SubmitForm(string formId,
|
||||
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromServices] UIInvoiceController invoiceController)
|
||||
{
|
||||
if (!IsValidRedirectUri(redirectUrl))
|
||||
return BadRequest();
|
||||
|
||||
var formData = GetFormData(formId);
|
||||
|
||||
var formData = await _formDataService.GetForm(formId);
|
||||
if (formData?.Config is null)
|
||||
return NotFound();
|
||||
|
||||
if (!Request.HasFormContentType)
|
||||
return GetFormView(formData, redirectUrl);
|
||||
return GetFormView(formData);
|
||||
|
||||
var conf = Form.Parse(formData.Config);
|
||||
conf.ApplyValuesFromForm(Request.Form);
|
||||
if (!FormProviders.Validate(conf, ModelState))
|
||||
return GetFormView(formData, redirectUrl);
|
||||
|
||||
var form = new MultiValueDictionary<string, string>();
|
||||
foreach (var kv in Request.Form)
|
||||
form.Add(kv.Key, kv.Value);
|
||||
var form = Form.Parse(formData.Config);
|
||||
form.ApplyValuesFromForm(Request.Form);
|
||||
|
||||
if (!await _formDataService.Validate(form, ModelState))
|
||||
return GetFormView(form, formData.Name);
|
||||
|
||||
// With redirect, the form comes from another entity that we need to send the data back to
|
||||
if (!string.IsNullOrEmpty(redirectUrl))
|
||||
|
||||
// Create invoice after public form has been filled
|
||||
var store = await storeRepository.FindStore(formData.StoreId);
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
var amt = form.GetFieldByName("internal_amount")?.Value;
|
||||
var request = new CreateInvoiceRequest
|
||||
{
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
FormUrl = redirectUrl,
|
||||
FormParameters = form
|
||||
});
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
internal static FormData? GetFormData(string id)
|
||||
{
|
||||
FormData? form = id switch
|
||||
{
|
||||
{ } formId when formId == GenericFormOption.Address.ToString() => new FormData
|
||||
{
|
||||
Config = FormDataService.StaticFormAddress.ToString(),
|
||||
Id = GenericFormOption.Address.ToString(),
|
||||
Name = "Provide your address",
|
||||
},
|
||||
{ } formId when formId == GenericFormOption.Email.ToString() => new FormData
|
||||
{
|
||||
Config = FormDataService.StaticFormEmail.ToString(),
|
||||
Id = GenericFormOption.Email.ToString(),
|
||||
Name = "Provide your email address",
|
||||
},
|
||||
_ => null
|
||||
Currency = form.GetFieldByName("internal_currency")?.Value ?? store.GetStoreBlob().DefaultCurrency,
|
||||
Amount = string.IsNullOrEmpty(amt) ? null : decimal.Parse(amt, CultureInfo.InvariantCulture),
|
||||
Metadata = JObject.FromObject( form.GetValues())
|
||||
};
|
||||
return form;
|
||||
}
|
||||
var inv = await invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteRoot());
|
||||
|
||||
private bool IsValidRedirectUri(string? redirectUrl) =>
|
||||
!string.IsNullOrEmpty(redirectUrl) && Uri.TryCreate(redirectUrl, UriKind.RelativeOrAbsolute, out var uri) &&
|
||||
(Url.IsLocalUrl(redirectUrl) || uri.Host.Equals(Request.Host.Host));
|
||||
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Common;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
@ -16,6 +17,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
@ -27,6 +29,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NicolasDorier.RateLimits;
|
||||
@ -42,21 +45,20 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
CurrencyNameTable currencies,
|
||||
StoreRepository storeRepository,
|
||||
UIInvoiceController invoiceController,
|
||||
FormComponentProviders formProviders)
|
||||
FormDataService formDataService)
|
||||
{
|
||||
_currencies = currencies;
|
||||
_appService = appService;
|
||||
_storeRepository = storeRepository;
|
||||
_invoiceController = invoiceController;
|
||||
FormProviders = formProviders;
|
||||
FormDataService = formDataService;
|
||||
}
|
||||
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly AppService _appService;
|
||||
private readonly UIInvoiceController _invoiceController;
|
||||
|
||||
public FormComponentProviders FormProviders { get; }
|
||||
public FormDataService FormDataService { get; }
|
||||
|
||||
[HttpGet("/")]
|
||||
[HttpGet("/apps/{appId}/pos/{viewType?}")]
|
||||
@ -117,14 +119,15 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
[DomainMappingConstraint(AppType.PointOfSale)]
|
||||
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
PosViewType? viewType,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey,
|
||||
PosViewType? viewType = null,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount = null,
|
||||
string email = null,
|
||||
string orderId = null,
|
||||
string notificationUrl = null,
|
||||
string redirectUrl = null,
|
||||
string choiceKey = null,
|
||||
string posData = null,
|
||||
string formResponse = null,
|
||||
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
@ -226,44 +229,44 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var store = await _appService.GetStore(app);
|
||||
var posFormId = settings.FormId;
|
||||
|
||||
var formConfig = posFormId is null ? null : Forms.UIFormsController.GetFormData(posFormId)?.Config;
|
||||
JObject formResponse = null;
|
||||
switch (formConfig)
|
||||
var formData = posFormId is null ? null : (await FormDataService.GetForm(posFormId));
|
||||
|
||||
JObject formResponseJObject = null;
|
||||
switch (formData)
|
||||
{
|
||||
case null:
|
||||
case { } when !this.Request.HasFormContentType:
|
||||
break;
|
||||
default:
|
||||
var formData = Form.Parse(formConfig);
|
||||
formData.ApplyValuesFromForm(this.Request.Form);
|
||||
|
||||
if (FormProviders.Validate(formData, ModelState))
|
||||
case not null:
|
||||
if (formResponse is null)
|
||||
{
|
||||
formResponse = JObject.FromObject(formData.GetValues());
|
||||
break;
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspAction = nameof(POSForm),
|
||||
RouteParameters = new Dictionary<string, string>()
|
||||
{
|
||||
{ "appId", appId}
|
||||
},
|
||||
AspController = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture),
|
||||
FormParameters = new MultiValueDictionary<string, string>(Request.Form.Select(pair => new KeyValuePair<string, IReadOnlyCollection<string>>(pair.Key, pair.Value)))
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var query = new QueryBuilder(Request.Query);
|
||||
foreach (var keyValuePair in Request.Form)
|
||||
formResponseJObject = JObject.Parse(formResponse);
|
||||
var formValues = formResponseJObject.ToObject<Dictionary<string, object>>();
|
||||
var form = Form.Parse(formData.Config);
|
||||
form.SetValues(formValues);
|
||||
if (!await FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
query.Add(keyValuePair.Key, keyValuePair.Value.ToArray());
|
||||
//someone tried to bypass validation
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new {appId});
|
||||
}
|
||||
|
||||
// GET or empty form data case: Redirect to form
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspController = "UIForms",
|
||||
AspAction = "ViewPublicForm",
|
||||
RouteParameters =
|
||||
{
|
||||
{ "formId", posFormId }
|
||||
},
|
||||
FormParameters =
|
||||
{
|
||||
{ "redirectUrl", Request.GetCurrentUrl() + query }
|
||||
}
|
||||
});
|
||||
formResponseJObject = JObject.FromObject(form.GetValues());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest
|
||||
@ -293,10 +296,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
|
||||
|
||||
if (formResponse is not null)
|
||||
if (formResponseJObject is not null)
|
||||
{
|
||||
var meta = entity.Metadata.ToJObject();
|
||||
meta.Merge(formResponse);
|
||||
meta.Merge(formResponseJObject);
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(meta);
|
||||
}
|
||||
});
|
||||
@ -314,6 +317,76 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("/apps/{appId}/pos/form")]
|
||||
|
||||
public async Task<IActionResult> POSForm(string appId)
|
||||
{
|
||||
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var formData = settings.FormId is null ? null : (await FormDataService.GetForm( settings.FormId));
|
||||
if (formData is null)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new {appId });
|
||||
}
|
||||
var myDictionary =
|
||||
Request.Form.Where(pair => pair.Key != "__RequestVerificationToken").ToDictionary(p => p.Key, p => p.Value.ToString());
|
||||
myDictionary.Add("appId", appId);
|
||||
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
|
||||
var redirectUrl = Url.Action(nameof(ViewPointOfSale), controller, myDictionary );
|
||||
|
||||
|
||||
return View("Views/UIForms/View", new FormViewModel()
|
||||
{
|
||||
Form = Form.Parse(formData.Config),
|
||||
RedirectUrl = redirectUrl,
|
||||
AspController = controller,
|
||||
AspAction = nameof(POSFormSubmit),
|
||||
RouteParameters = new Dictionary<string, string>(){{"appId", appId}},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("/apps/{appId}/pos/form/submit")]
|
||||
public async Task<IActionResult> POSFormSubmit(string appId, FormViewModel viewModel)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var formData = settings.FormId is null ? null : (await FormDataService.GetForm( settings.FormId));
|
||||
if (formData is null || viewModel.RedirectUrl is null)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new {appId });
|
||||
}
|
||||
|
||||
var form = Form.Parse(formData.Config);
|
||||
if (Request.Method == "POST" && Request.HasFormContentType)
|
||||
{
|
||||
form.ApplyValuesFromForm(Request.Form);
|
||||
if (await FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
FormUrl = viewModel.RedirectUrl,
|
||||
FormParameters =
|
||||
{
|
||||
{ "formResponse", JObject.FromObject(form.GetValues()).ToString() }
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.FormName = formData.Name;
|
||||
viewModel.Form = form;
|
||||
return View("Views/UIForms/View", viewModel);
|
||||
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId)
|
||||
|
@ -1,40 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Services.Stores;
|
||||
|
||||
public enum GenericFormOption
|
||||
{
|
||||
[Display(Name = "Do not request any information")]
|
||||
None,
|
||||
|
||||
[Display(Name = "Request email address only")]
|
||||
Email,
|
||||
|
||||
[Display(Name = "Request shipping address")]
|
||||
Address
|
||||
}
|
||||
|
||||
public static class CheckoutFormSelectList
|
||||
{
|
||||
public static SelectList WithSelected(string selectedFormId)
|
||||
{
|
||||
var choices = new List<SelectListItem>
|
||||
{
|
||||
GenericOptionItem(GenericFormOption.None),
|
||||
GenericOptionItem(GenericFormOption.Email),
|
||||
GenericOptionItem(GenericFormOption.Address)
|
||||
};
|
||||
|
||||
var chosen = choices.FirstOrDefault(t => t.Value == selectedFormId);
|
||||
return new SelectList(choices, nameof(SelectListItem.Value), nameof(SelectListItem.Text), chosen?.Value);
|
||||
}
|
||||
|
||||
private static string DisplayName(GenericFormOption opt) =>
|
||||
typeof(GenericFormOption).DisplayName(opt.ToString());
|
||||
|
||||
private static SelectListItem GenericOptionItem(GenericFormOption opt) =>
|
||||
new() { Text = DisplayName(opt), Value = opt == GenericFormOption.None ? null : opt.ToString() };
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
@using BTCPayServer.Abstractions.Form
|
||||
@using Newtonsoft.Json.Linq
|
||||
@model BTCPayServer.Abstractions.Form.Field
|
||||
@{
|
||||
var isInvalid = this.ViewContext.ModelState[Model.Name]?.ValidationState is Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid;
|
||||
var error = isInvalid ? this.ViewContext.ModelState[Model.Name].Errors[0].ErrorMessage : null;
|
||||
var errors = isInvalid ? this.ViewContext.ModelState[Model.Name].Errors : null;
|
||||
|
||||
}
|
||||
<div class="form-group">
|
||||
@ -20,10 +18,13 @@
|
||||
</label>
|
||||
}
|
||||
|
||||
<input class="form-control @(Model.IsValid() ? "" : "is-invalid")" id="@Model.Name" type="@Model.Type" required="@Model.Required" name="@Model.Name" value="@Model.Value" aria-describedby="@("HelpText" + Model.Name)"/>
|
||||
<input class="form-control @(errors is null ? "" : "is-invalid")" id="@Model.Name" type="@Model.Type" required="@Model.Required" name="@Model.Name" value="@Model.Value" aria-describedby="@("HelpText" + Model.Name)"/>
|
||||
@if(isInvalid)
|
||||
{
|
||||
<span class="text-danger">@error</span>
|
||||
@foreach (var error in errors)
|
||||
{
|
||||
<span class="invalid-feedback">@error.ErrorMessage</span>
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.HelpText))
|
||||
{
|
||||
|
@ -2,12 +2,14 @@
|
||||
@using BTCPayServer.Abstractions.Models
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Forms
|
||||
@using BTCPayServer.Services.Stores
|
||||
@inject FormDataService FormDataService
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.UpdatePointOfSaleViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
|
||||
|
||||
var checkoutFormOptions = CheckoutFormSelectList.WithSelected(Model.FormId);
|
||||
|
||||
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
|
||||
}
|
||||
|
||||
<form method="post">
|
||||
|
55
BTCPayServer/Views/UIForms/FormsList.cshtml
Normal file
55
BTCPayServer/Views/UIForms/FormsList.cshtml
Normal file
@ -0,0 +1,55 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Models
|
||||
@model List<BTCPayServer.Data.Data.FormData>
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["NavPartialName"] = "../UIStores/_Nav";
|
||||
ViewData.SetActivePage(StoreNavPages.Forms, "Forms");
|
||||
var storeId = Context.GetCurrentStoreId();
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xxl-constrain col-xl-10">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||
<a asp-action="Create" asp-route-storeId="@storeId" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateForm">
|
||||
<span class="fa fa-plus"></span>
|
||||
Create Form
|
||||
</a>
|
||||
</div>
|
||||
@if (Model.Any())
|
||||
{
|
||||
<table class="table table-hover table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="Modify" asp-route-storeId="@item.StoreId" asp-route-id="@item.Id" id="Edit-@item.Name">@item.Name</a>
|
||||
</td>
|
||||
|
||||
<td class="text-end">
|
||||
<a asp-action="Remove" asp-route-storeId="@item.StoreId" asp-route-id="@item.Id" id="Remove-@item.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-confirm-input="DELETE">Remove</a> -
|
||||
<a asp-action="ViewPublicForm" asp-route-formId="@item.Id" id="View-@item.Name">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no forms yet.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="_Confirm" model="@(new ConfirmModel("Delete form", "This form will be removed from this store.", "Delete"))" />
|
77
BTCPayServer/Views/UIForms/Modify.cshtml
Normal file
77
BTCPayServer/Views/UIForms/Modify.cshtml
Normal file
@ -0,0 +1,77 @@
|
||||
@using BTCPayServer.Forms
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using Newtonsoft.Json
|
||||
@model BTCPayServer.Forms.ModifyForm
|
||||
|
||||
@{
|
||||
var formId = Context.GetRouteValue("id");
|
||||
var isNew = formId is null;
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["NavPartialName"] = "../UIStores/_Nav";
|
||||
ViewData.SetActivePage(StoreNavPages.Forms, $"{(isNew ? "Create" : "Edit")} Form", Model.Name);
|
||||
|
||||
var storeId = Context.GetCurrentStoreId();
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const $config = document.getElementById("FormConfig");
|
||||
delegate("click", "[data-form-template]", e => {
|
||||
const { formTemplate: id } = e.target.dataset
|
||||
const $template = document.getElementById(`form-template-${id}`)
|
||||
$config.value = $template.innerHTML.trim()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
}
|
||||
|
||||
<template id="form-template-email">
|
||||
@Json.Serialize(FormDataService.StaticFormEmail, new JsonSerializerSettings()
|
||||
{
|
||||
Formatting = Formatting.Indented
|
||||
})
|
||||
</template>
|
||||
<template id="form-template-address">
|
||||
@Json.Serialize(FormDataService.StaticFormAddress, new JsonSerializerSettings()
|
||||
{
|
||||
Formatting = Formatting.Indented
|
||||
})
|
||||
</template>
|
||||
|
||||
<form method="post" asp-action="Modify" asp-route-id="@formId" asp-route-storeId="@storeId">
|
||||
<div class="row">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveButton">Save</button>
|
||||
@if (!isNew)
|
||||
{
|
||||
<a class="btn btn-secondary" asp-action="ViewPublicForm" asp-route-formId="@formId" id="ViewForm">View</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label" data-required></label>
|
||||
<input asp-for="Name" class="form-control" required/>
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<label asp-for="FormConfig" class="form-label" data-required></label>
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span>Templates:</span>
|
||||
<button type="button" class="btn btn-link p-0" data-form-template="email">Email</button>
|
||||
<button type="button" class="btn btn-link p-0" data-form-template="address">Address</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea asp-for="FormConfig" class="form-control" rows="10" cols="21"></textarea>
|
||||
<span asp-validation-for="FormConfig" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -5,7 +5,7 @@
|
||||
@model BTCPayServer.Forms.Models.FormViewModel
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = Model.FormData.Name;
|
||||
ViewData["Title"] = Model.FormName;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -21,20 +21,37 @@
|
||||
<partial name="_StatusMessage" model="@(new ViewDataDictionary(ViewData) {{"Margin", "mb-4"}})"/>
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div asp-validation-summary="ModelOnly" class="invalid-feedback"></div>
|
||||
}
|
||||
<partial name="_FormTopMessages" model="@Model.Form"/>
|
||||
<div class="d-flex flex-column justify-content-center gap-4">
|
||||
<h1 class="h3 text-center">@ViewData["Title"]</h1>
|
||||
<div class="bg-tile p-3 p-sm-4 rounded">
|
||||
<form asp-action="SubmitForm" asp-route-formId="@Model.FormData.Id">
|
||||
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
|
||||
{
|
||||
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl"/>
|
||||
}
|
||||
<partial name="_Form" model="@Model.Form"/>
|
||||
<input type="submit" class="btn btn-primary" name="command" value="Submit"/>
|
||||
</form>
|
||||
<div class="bg-tile p-3 p-sm-4 rounded">
|
||||
|
||||
@if (string.IsNullOrEmpty(Model.AspAction))
|
||||
{
|
||||
<form method="post">
|
||||
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
|
||||
{
|
||||
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl" />
|
||||
}
|
||||
<partial name="_Form" model="@Model.Form" />
|
||||
<input type="submit" class="btn btn-primary" name="command" value="Submit" />
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-action="@Model.AspAction" asp-controller="@Model.AspController" asp-all-route-data="Model.RouteParameters">
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.RedirectUrl))
|
||||
{
|
||||
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl" />
|
||||
}
|
||||
<partial name="_Form" model="@Model.Form" />
|
||||
<input type="submit" class="btn btn-primary" name="command" value="Submit" />
|
||||
</form>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,14 @@
|
||||
@using BTCPayServer.Services.PaymentRequests
|
||||
@using System.Globalization
|
||||
@using BTCPayServer.Forms
|
||||
@using BTCPayServer.Services.Stores
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject FormDataService FormDataService
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
||||
@{
|
||||
var checkoutFormOptions = CheckoutFormSelectList.WithSelected(Model.FormId);
|
||||
|
||||
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
|
||||
|
||||
ViewData.SetActivePage(PaymentRequestsNavPages.Create, $"{(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit")} Payment Request", Model.Id);
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@storeId">Webhooks</a>
|
||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.PayoutProcessors))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayoutProcessors)" asp-controller="UIPayoutProcessors" asp-action="ConfigureStorePayoutProcessors" asp-route-storeId="@storeId">Payout Processors</a>
|
||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Emails))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Emails)" asp-controller="UIStores" asp-action="StoreEmailSettings" asp-route-storeId="@storeId">Emails</a>
|
||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Forms))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Forms)" asp-controller="UIForms" asp-action="FormsList" asp-route-storeId="@storeId">Forms</a>
|
||||
<vc:ui-extension-point location="store-nav" model="@Model"/>
|
||||
</div>
|
||||
</nav>
|
||||
|
Reference in New Issue
Block a user