Compare commits

..

47 Commits

Author SHA1 Message Date
71c2bfffc1 Remove untested and unfinished code 2022-11-25 10:02:01 +09:00
4b952648a4 Refactoring: Use PostRedirect instead of TempData for data transfer 2022-11-25 00:24:02 +01:00
8b7b772d34 Add index - needs migration 2022-11-24 19:54:55 +01:00
713b87d07b Update test 2022-11-24 19:43:28 +01:00
82a690645e More minor syntax cleanups 2022-11-24 19:33:43 +01:00
dcade8b4a9 Remove custom form options from select for now 2022-11-24 19:09:41 +01:00
ad17234a86 Minor cleanups 2022-11-24 19:03:00 +01:00
7218c3077d Hide custom form feature in UI 2022-11-24 17:14:23 +01:00
3e1511c0a8 Merge branch 'master' into form_builder 2022-11-24 17:03:47 +01:00
3cb7c64321 Remove useless storeId parameter 2022-11-24 21:36:24 +09:00
9c88c53798 Remove storeId from step form 2022-11-24 21:15:55 +09:00
50d08de78b Fix modify 2022-11-24 21:15:27 +09:00
15ab7c051b Fix query to forms, ensure no permission bypass 2022-11-24 20:34:05 +09:00
4e8126a734 Fix ef request 2022-11-24 18:07:08 +09:00
8195acbbf7 Fix warnings 2022-11-24 17:53:43 +09:00
ec35b75324 Put the Authorize at controller level on UIForms 2022-11-24 17:48:21 +09:00
d65835b0fe Refactor FormQuery to only be able to query single store and single form 2022-11-24 17:42:55 +09:00
426a65d3fe Merge remote-tracking branch 'origin/master' into form_builder
# Conflicts:
#	BTCPayServer/Controllers/UIInvoiceController.UI.cs
#	BTCPayServer/Models/StoreViewModels/CheckoutAppearanceViewModel.cs
#	BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml
#	BTCPayServer/Views/UIStores/CheckoutAppearance.cshtml
2022-11-24 07:23:32 +01:00
2fe2efcb7a make pay request form submission redirect to invoice 2022-11-23 21:53:08 +01:00
f671f8fd26 fix 2022-11-23 07:43:39 +01:00
e0f15c8792 fix migration for forms 2022-11-22 22:58:37 +01:00
4e828f4398 Merge remote-tracking branch 'origin/master' into form_builder
# Conflicts:
#	BTCPayServer/Services/BTCPayServerEnvironment.cs
2022-11-22 22:52:44 +01:00
399fbe2827 add form test 2022-11-22 22:47:01 +01:00
6f547750ae Remove invoice and store level form 2022-11-22 22:04:50 +01:00
f0f09180d5 Fix flaky test (#4330)
* Fix flaky test

* Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs

Co-authored-by: d11n <mail@dennisreimann.de>

Co-authored-by: d11n <mail@dennisreimann.de>
2022-11-22 22:04:34 +01:00
95c6817d2a Add documentation link to plugins (#4329)
* Add documentation link to plugins

* Minor UI updates

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-11-22 22:04:34 +01:00
bb9c0989c6 POS: Fix null pointer
Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`.
2022-11-22 22:04:34 +01:00
fa2a3dd7f3 Change confirmed to settled. (#4328) 2022-11-22 22:04:34 +01:00
86361d0f75 Server Settings: Update Policies page (#4326)
Handles the multiple submit buttons on that page and closes #4319.

Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS.
2022-11-22 22:04:33 +01:00
a056e9b570 fix monero issue 2022-11-22 22:04:33 +01:00
b89918ff5d Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) 2022-11-22 22:04:33 +01:00
37693399d6 Fix warnings in Form builder (#4331)
* Fix build warnings about string?

Enable nullable on UIFormsController.cs
Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

* Clean up lack of space in injected services in Submit() of UIFormsController.cs

* Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600)

* Cleanup double semicolons while we're at tit
2022-11-22 20:26:25 +01:00
bc652507d8 UI updates 2022-11-21 22:44:14 +01:00
d8094e9fee fix pav bug 2022-11-21 11:55:42 +01:00
33c92c1428 general fixes for form system 2022-11-21 11:51:46 +01:00
6747f91d29 move checkoutform id in checkout appearance outside of checkotu v2 toggle 2022-11-21 11:51:46 +01:00
469ff5b11f fix up code 2022-11-21 11:51:46 +01:00
b3bc4e5745 Do not request additional forms on invoice from pay request 2022-11-21 11:51:46 +01:00
44b0addff5 Display form name in inherit from store setting 2022-11-21 11:51:46 +01:00
383520c453 invoice form through receipt page 2022-11-21 11:51:45 +01:00
0c3b345d1b pay request form rough support 2022-11-21 11:51:45 +01:00
3513e602f4 Add support for pos app + forms 2022-11-21 11:51:45 +01:00
cec83ab4ec Make predefined forms usable statically 2022-11-21 11:51:45 +01:00
16537509d9 Update UIFormsController.cs 2022-11-21 11:51:45 +01:00
6bd6a045f4 UI updates 2022-11-21 11:51:45 +01:00
5de665dfff Cleanups 2022-11-21 11:51:45 +01:00
e329368e21 wip 2022-11-21 11:51:38 +01:00
35 changed files with 304 additions and 402 deletions

View File

@ -1,18 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Abstractions
{
public class CamelCaseSerializerSettings
{
static CamelCaseSerializerSettings()
{
Settings = new JsonSerializerSettings()
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
};
Serializer = JsonSerializer.Create(Settings);
}
public static readonly JsonSerializerSettings Settings;
public static readonly JsonSerializer Serializer;
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -10,50 +9,16 @@ namespace BTCPayServer.Abstractions.Form;
public class Field
{
public static Field Create(string label, string name, string value, bool required, string helpText, string type = "text")
{
return new Field()
{
Label = label,
Name = name,
Value = value,
OriginalValue = value,
Required = required,
HelpText = helpText,
Type = type
};
}
// The name of the HTML5 node. Should be used as the key for the posted data.
public string Name;
public bool Hidden;
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
public string Type;
public static Field CreateFieldset()
{
return new Field() { Type = "fieldset" };
}
// The value field is what is currently in the DB or what the user entered, but possibly not saved yet due to validation errors.
// If this is the first the user sees the form, then value and original value are the same. Value changes as the user starts interacting with the form.
public string Value;
public bool Required;
// The translated label of the field.
public string Label;
// The original value is the value that is currently saved in the backend. A "reset" button can be used to revert back to this. Should only be set from the constructor.
public string OriginalValue;
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
public string HelpText;
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new();
// The field is considered "valid" if there are no validation errors
public List<string> ValidationErrors = new List<string>();
@ -61,4 +26,9 @@ public class Field
{
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
}
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new();
}

View File

@ -0,0 +1,12 @@
namespace BTCPayServer.Abstractions.Form;
public class Fieldset : Field
{
public bool Hidden { get; set; }
public string Label { get; set; }
public Fieldset()
{
Type = "fieldset";
}
}

View File

@ -2,24 +2,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Abstractions.Form;
public class Form
{
#nullable enable
public static Form Parse(string str)
{
ArgumentNullException.ThrowIfNull(str);
return JObject.Parse(str).ToObject<Form>(CamelCaseSerializerSettings.Serializer) ?? throw new InvalidOperationException("Impossible to deserialize Form");
}
public override string ToString()
{
return JObject.FromObject(this, CamelCaseSerializerSettings.Serializer).ToString(Newtonsoft.Json.Formatting.Indented);
}
#nullable restore
// Messages to be shown at the top of the form indicating user feedback like "Saved successfully" or "Please change X because of Y." or a warning, etc...
public List<AlertMessage> TopMessages { get; set; } = new();
@ -29,7 +17,7 @@ public class Form
// Are all the fields valid in the form?
public bool IsValid()
{
return Fields.Select(f => f.IsValid()).All(o => o);
return Fields.All(field => field.IsValid());
}
public Field GetFieldByName(string name)
@ -64,7 +52,6 @@ public class Form
}
return null;
}
public List<string> GetAllNames()
{
return GetAllNames(Fields);

View File

@ -0,0 +1,27 @@
namespace BTCPayServer.Abstractions.Form;
public class HtmlInputField : Field
{
// The translated label of the field.
public string Label;
// The original value is the value that is currently saved in the backend. A "reset" button can be used to revert back to this. Should only be set from the constructor.
public string OriginalValue;
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
public string HelpText;
public bool Required;
public HtmlInputField(string label, string name, string value, bool required, string helpText, string type = "text")
{
Label = label;
Name = name;
Value = value;
OriginalValue = value;
Required = required;
HelpText = helpText;
Type = type;
}
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
}

View File

@ -27,6 +27,6 @@ namespace BTCPayServer.Client.Models
public string FormId { get; set; }
public JObject FormResponse { get; set; }
public string FormResponse { get; set; }
}
}

View File

@ -27,36 +27,36 @@ namespace BTCPayServer.Data
public string Id { get; set; }
public string Data { get; set; }
public List<WalletObjectLinkData> Bs { get; set; }
public List<WalletObjectLinkData> As { get; set; }
public List<WalletObjectLinkData> ChildLinks { get; set; }
public List<WalletObjectLinkData> ParentLinks { get; set; }
public IEnumerable<(string type, string id, string linkdata, string objectdata)> GetLinks()
{
if (Bs is not null)
foreach (var c in Bs)
if (ChildLinks is not null)
foreach (var c in ChildLinks)
{
yield return (c.BType, c.BId, c.Data, c.B?.Data);
yield return (c.ChildType, c.ChildId, c.Data, c.Child?.Data);
}
if (As is not null)
foreach (var c in As)
if (ParentLinks is not null)
foreach (var c in ParentLinks)
{
yield return (c.AType, c.AId, c.Data, c.A?.Data);
yield return (c.ParentType, c.ParentId, c.Data, c.Parent?.Data);
}
}
public IEnumerable<WalletObjectData> GetNeighbours()
{
if (Bs != null)
foreach (var c in Bs)
if (ChildLinks != null)
foreach (var c in ChildLinks)
{
if (c.B != null)
yield return c.B;
if (c.Child != null)
yield return c.Child;
}
if (As != null)
foreach (var c in As)
if (ParentLinks != null)
foreach (var c in ParentLinks)
{
if (c.A != null)
yield return c.A;
if (c.Parent != null)
yield return c.Parent;
}
}

View File

@ -11,14 +11,14 @@ namespace BTCPayServer.Data
public class WalletObjectLinkData
{
public string WalletId { get; set; }
public string AType { get; set; }
public string AId { get; set; }
public string BType { get; set; }
public string BId { get; set; }
public string ParentType { get; set; }
public string ParentId { get; set; }
public string ChildType { get; set; }
public string ChildId { get; set; }
public string Data { get; set; }
public WalletObjectData A { get; set; }
public WalletObjectData B { get; set; }
public WalletObjectData Parent { get; set; }
public WalletObjectData Child { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -26,28 +26,28 @@ namespace BTCPayServer.Data
new
{
o.WalletId,
o.AType,
o.AId,
o.BType,
o.BId,
o.ParentType,
o.ParentId,
o.ChildType,
o.ChildId,
});
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
{
o.WalletId,
o.BType,
o.BId,
o.ChildType,
o.ChildId,
});
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.A)
.WithMany(o => o.Bs)
.HasForeignKey(o => new { o.WalletId, o.AType, o.AId })
.HasOne(o => o.Parent)
.WithMany(o => o.ChildLinks)
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.B)
.WithMany(o => o.As)
.HasForeignKey(o => new { o.WalletId, o.BType, o.BId })
.HasOne(o => o.Child)
.WithMany(o => o.ParentLinks)
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
.OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())

View File

@ -40,33 +40,33 @@ namespace BTCPayServer.Migrations
columns: table => new
{
WalletId = table.Column<string>(type: "TEXT", nullable: false),
AType = table.Column<string>(type: "TEXT", nullable: false),
AId = table.Column<string>(type: "TEXT", nullable: false),
BType = table.Column<string>(type: "TEXT", nullable: false),
BId = table.Column<string>(type: "TEXT", nullable: false),
ParentType = table.Column<string>(type: "TEXT", nullable: false),
ParentId = table.Column<string>(type: "TEXT", nullable: false),
ChildType = table.Column<string>(type: "TEXT", nullable: false),
ChildId = table.Column<string>(type: "TEXT", nullable: false),
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.AType, x.AId, x.BType, x.BId });
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.ParentType, x.ParentId, x.ChildType, x.ChildId });
table.ForeignKey(
name: "FK_WalletObjectLinks_WalletObjects_WalletId_BType_BId",
columns: x => new { x.WalletId, x.BType, x.BId },
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
principalTable: "WalletObjects",
principalColumns: new[] { "WalletId", "Type", "Id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_WalletObjectLinks_WalletObjects_WalletId_AType_AId",
columns: x => new { x.WalletId, x.AType, x.AId },
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ParentType_ParentId",
columns: x => new { x.WalletId, x.ParentType, x.ParentId },
principalTable: "WalletObjects",
principalColumns: new[] { "WalletId", "Type", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_WalletObjectLinks_WalletId_BType_BId",
name: "IX_WalletObjectLinks_WalletId_ChildType_ChildId",
table: "WalletObjectLinks",
columns: new[] { "WalletId", "BType", "BId" });
columns: new[] { "WalletId", "ChildType", "ChildId" });
}
protected override void Down(MigrationBuilder migrationBuilder)

View File

@ -872,24 +872,24 @@ namespace BTCPayServer.Migrations
b.Property<string>("WalletId")
.HasColumnType("TEXT");
b.Property<string>("AType")
b.Property<string>("ParentType")
.HasColumnType("TEXT");
b.Property<string>("AId")
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("BType")
b.Property<string>("ChildType")
.HasColumnType("TEXT");
b.Property<string>("BId")
b.Property<string>("ChildId")
.HasColumnType("TEXT");
b.Property<string>("Data")
.HasColumnType("TEXT");
b.HasKey("WalletId", "AType", "AId", "BType", "BId");
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
b.HasIndex("WalletId", "BType", "BId");
b.HasIndex("WalletId", "ChildType", "ChildId");
b.ToTable("WalletObjectLinks");
});
@ -1384,21 +1384,21 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
{
b.HasOne("BTCPayServer.Data.WalletObjectData", "A")
.WithMany("Bs")
.HasForeignKey("WalletId", "AType", "AId")
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
.WithMany("ParentLinks")
.HasForeignKey("WalletId", "ChildType", "ChildId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BTCPayServer.Data.WalletObjectData", "B")
.WithMany("As")
.HasForeignKey("WalletId", "BType", "BId")
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
.WithMany("ChildLinks")
.HasForeignKey("WalletId", "ParentType", "ParentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("A");
b.Navigation("Child");
b.Navigation("B");
b.Navigation("Parent");
});
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
@ -1545,9 +1545,9 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
{
b.Navigation("As");
b.Navigation("ChildLinks");
b.Navigation("Bs");
b.Navigation("ParentLinks");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>

View File

@ -24,6 +24,7 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -178,6 +179,11 @@ namespace BTCPayServer.Controllers
}
JToken? receiptData = null;
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
string? formResponse = null;
if (i.Metadata?.AdditionalData?.TryGetValue("formResponse", out var formResponseRaw)is true)
{
formResponseRaw.Value<string>();
}
var payments = i.GetPayments(true)
.Select(paymentEntity =>

View File

@ -193,8 +193,7 @@ namespace BTCPayServer.Controllers
Metadata = invoiceMetadata.ToJObject(),
Currency = pr.Currency,
Amount = amount,
Checkout = { RedirectURL = redirectUrl },
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
Checkout = { RedirectURL = redirectUrl }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };

View File

@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
@ -18,8 +17,10 @@ using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json.Linq;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
using StoreData = BTCPayServer.Data.StoreData;
@ -39,8 +40,6 @@ namespace BTCPayServer.Controllers
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
private FormComponentProviders FormProviders { get; }
public UIPaymentRequestController(
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
@ -49,8 +48,7 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator,
CurrencyNameTable currencies,
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders)
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -60,7 +58,6 @@ namespace BTCPayServer.Controllers
_Currencies = currencies;
_storeRepository = storeRepository;
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
}
[BitpayAPIConstraint(false)]
@ -184,7 +181,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, [FromForm] string formId, [FromForm] string formData)
{
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (result == null)
@ -198,37 +195,32 @@ namespace BTCPayServer.Controllers
{
case null:
case { } when string.IsNullOrEmpty(prFormId):
case { } when Request.Method == "GET" && prBlob.FormResponse is not null:
return RedirectToAction("ViewPaymentRequest", new { payReqId });
case { } when Request.Method == "GET" && prBlob.FormResponse is null:
break;
break;
default:
// POST case: Handle form submit
var formData = Form.Parse(UIFormsController.GetFormData(prFormId).Config);
formData.ApplyValuesFromForm(Request.Form);
if (FormProviders.Validate(formData, ModelState))
if (!string.IsNullOrEmpty(formData) && formId == prFormId)
{
prBlob.FormResponse = JObject.FromObject(formData.GetValues());
prBlob.FormResponse = formData;
result.SetBlob(prBlob);
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
return RedirectToAction("PayPaymentRequest", new { payReqId });
}
break;
// GET or empty form data case: Redirect to form
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
FormParameters =
{
{ "formId", prFormId },
{ "redirectUrl", Request.GetCurrentUrl() }
}
});
}
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", prFormId }
},
FormParameters =
{
{ "redirectUrl", Request.GetCurrentUrl() }
}
});
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
[HttpGet("{payReqId}/pay")]

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class FormComponentProvider : IFormComponentProvider
{
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
public FormComponentProvider(IEnumerable<IFormComponentProvider> formComponentProviders)
{
_formComponentProviders = formComponentProviders;
}
public string CanHandle(Field field)
{
return _formComponentProviders.Select(formComponentProvider => formComponentProvider.CanHandle(field)).FirstOrDefault(result => !string.IsNullOrEmpty(result));
}
}

View File

@ -1,34 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Forms;
public class FormComponentProviders
{
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
public Dictionary<string, IFormComponentProvider> TypeToComponentProvider = new Dictionary<string, IFormComponentProvider>();
public FormComponentProviders(IEnumerable<IFormComponentProvider> formComponentProviders)
{
_formComponentProviders = formComponentProviders;
foreach (var prov in _formComponentProviders)
prov.Register(TypeToComponentProvider);
}
public bool Validate(Form form, ModelStateDictionary modelState)
{
foreach (var field in form.Fields)
{
if (TypeToComponentProvider.TryGetValue(field.Type, out var provider))
{
provider.Validate(form, field);
foreach (var err in field.ValidationErrors)
modelState.TryAddModelError(field.Name, err);
}
}
return modelState.IsValid;
}
}

View File

@ -10,7 +10,7 @@ public static class FormDataExtensions
public static void AddForms(this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<FormDataService>();
serviceCollection.AddSingleton<FormComponentProviders>();
serviceCollection.AddSingleton<FormComponentProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
}

View File

@ -15,21 +15,21 @@ public class FormDataService
public static readonly Form StaticFormEmail = new()
{
Fields = new List<Field>() {Field.Create("Enter your email", "buyerEmail", null, true, null, "email")}
Fields = new List<Field>() {new HtmlInputField("Enter your email", "buyerEmail", null, true, null)}
};
public static readonly Form StaticFormAddress = new()
{
Fields = new List<Field>()
{
Field.Create("Enter your email", "buyerEmail", null, true, null, "email"),
Field.Create("Name", "buyerName", null, true, null),
Field.Create("Address Line 1", "buyerAddress1", null, true, null),
Field.Create("Address Line 2", "buyerAddress2", null, false, null),
Field.Create("City", "buyerCity", null, true, null),
Field.Create("Postcode", "buyerZip", null, false, null),
Field.Create("State", "buyerState", null, false, null),
Field.Create("Country", "buyerCountry", null, true, null)
new HtmlInputField("Enter your email", "buyerEmail", null, true, null, "email"),
new HtmlInputField("Name", "buyerName", null, true, null),
new HtmlInputField("Address Line 1", "buyerAddress1", null, true, null),
new HtmlInputField("Address Line 2", "buyerAddress2", null, false, null),
new HtmlInputField("City", "buyerCity", null, true, null),
new HtmlInputField("Postcode", "buyerZip", null, false, null),
new HtmlInputField("State", "buyerState", null, false, null),
new HtmlInputField("Country", "buyerCountry", null, true, null)
}
};
}

View File

@ -1,23 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class HtmlFieldsetFormProvider: IFormComponentProvider
{
public string View => "Forms/FieldSetElement";
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
public string CanHandle(Field field)
{
typeToComponentProvider.Add("fieldset", this);
return new[] { "fieldset"}.Contains(field.Type) ? "Forms/FieldSetElement" : null;
}
public void Validate(Field field)
{
}
public void Validate(Form form, Field field)
{
}
}
}

View File

@ -1,16 +1,13 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Validation;
namespace BTCPayServer.Forms;
public class HtmlInputFormProvider: FormComponentProviderBase
public class HtmlInputFormProvider: IFormComponentProvider
{
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
public string CanHandle(Field field)
{
foreach (var t in new[] {
return new[] {
"text",
"radio",
"checkbox",
@ -32,20 +29,6 @@ public class HtmlInputFormProvider: FormComponentProviderBase
"search",
"url",
"tel",
"reset"})
typeToComponentProvider.Add(t, this);
"reset"}.Contains(field.Type) ? "Forms/InputElement" : null;
}
public override string View => "Forms/InputElement";
public override void Validate(Form form, Field field)
{
if (field.Required)
{
ValidateField<RequiredAttribute>(field);
}
if (field.Type == "email")
{
ValidateField<MailboxAddressAttribute>(field);
}
}
}
}

View File

@ -1,26 +1,8 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public interface IFormComponentProvider
{
string View { get; }
void Validate(Form form, Field field);
void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
}
public abstract class FormComponentProviderBase : IFormComponentProvider
{
public abstract string View { get; }
public abstract void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
public abstract void Validate(Form form, Field field);
public void ValidateField<T>(Field field) where T : ValidationAttribute, new()
{
var result = new T().GetValidationResult(field.Value, new ValidationContext(field) { DisplayName = field.Label, MemberName = field.Name });
if (result != null)
field.ValidationErrors.Add(result.ErrorMessage);
}
public string CanHandle(Field field);
}

View File

@ -8,6 +8,5 @@ public class FormViewModel
{
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 => JObject.Parse(FormData.Config).ToObject<Form>(); }
}

View File

@ -1,36 +1,36 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
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.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Forms;
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class UIFormsController : Controller
{
private FormComponentProviders FormProviders { get; }
public UIFormsController(FormComponentProviders formProviders)
{
FormProviders = formProviders;
}
[AllowAnonymous]
[HttpGet("~/forms/{formId}")]
[HttpPost("~/forms")]
public IActionResult ViewPublicForm(string? formId, string? redirectUrl)
{
if (!IsValidRedirectUri(redirectUrl))
return BadRequest();
FormData? formData = string.IsNullOrEmpty(formId) ? null : GetFormData(formId);
if (formData == null)
{
@ -39,35 +39,25 @@ public class UIFormsController : Controller
: Redirect(redirectUrl);
}
return GetFormView(formData, redirectUrl);
}
ViewResult GetFormView(FormData formData, string? redirectUrl)
{
return View("View", new FormViewModel { FormData = formData, RedirectUrl = redirectUrl });
}
[AllowAnonymous]
[HttpPost("~/forms/{formId}")]
public IActionResult SubmitForm(string formId, string? redirectUrl, string? command)
public IActionResult SubmitForm(
string formId, string? redirectUrl,
[FromServices] StoreRepository storeRepository,
[FromServices] UIInvoiceController invoiceController)
{
if (!IsValidRedirectUri(redirectUrl))
return BadRequest();
var formData = GetFormData(formId);
if (formData?.Config is null)
if (formData is null)
{
return NotFound();
if (command is not "Submit")
return GetFormView(formData, redirectUrl);
}
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 dbForm = JObject.Parse(formData.Config!).ToObject<Form>()!;
dbForm.ApplyValuesFromForm(Request.Form);
Dictionary<string, object> data = dbForm.GetValues();
// With redirect, the form comes from another entity that we need to send the data back to
if (!string.IsNullOrEmpty(redirectUrl))
@ -75,26 +65,30 @@ public class UIFormsController : Controller
return View("PostRedirect", new PostRedirectViewModel
{
FormUrl = redirectUrl,
FormParameters = form
FormParameters =
{
{ "formId", formData.Id },
{ "formData", JsonConvert.SerializeObject(data) }
}
});
}
return NotFound();
}
internal static FormData? GetFormData(string id)
private FormData? GetFormData(string id)
{
FormData? form = id switch
{
{ } formId when formId == GenericFormOption.Address.ToString() => new FormData
{
Config = FormDataService.StaticFormAddress.ToString(),
Config = JObject.FromObject(FormDataService.StaticFormAddress).ToString(),
Id = GenericFormOption.Address.ToString(),
Name = "Provide your address",
},
{ } formId when formId == GenericFormOption.Email.ToString() => new FormData
{
Config = FormDataService.StaticFormEmail.ToString(),
Config = JObject.FromObject(FormDataService.StaticFormEmail).ToString(),
Id = GenericFormOption.Email.ToString(),
Name = "Provide your email address",
},
@ -102,8 +96,4 @@ public class UIFormsController : Controller
};
return form;
}
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));
}

View File

@ -173,10 +173,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = Data.WalletObjectData.Types.Label,
AId = labelId
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = Data.WalletObjectData.Types.Label,
ParentId = labelId
});
if (label.Value is ReferenceLabel reflabel)
@ -195,10 +195,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = reflabel.Type,
AId = reflabel.Reference ?? String.Empty
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = reflabel.Type,
ParentId = reflabel.Reference ?? String.Empty
});
}
}
@ -224,10 +224,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = "payout",
AId = payout
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = "payout",
ParentId = payout
});
}
}

View File

@ -46,9 +46,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
CustomCSSLink = blob.CustomCSSLink;
EmbeddedCSS = blob.EmbeddedCSS;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
FormResponse = blob.FormResponse is null
FormResponse = string.IsNullOrEmpty(blob.FormResponse)
? null
: blob.FormResponse.ToObject<Dictionary<string, object>>();
: JObject.Parse(blob.FormResponse).ToObject<Dictionary<string, object>>();
}
[Display(Name = "Request customer data on checkout")]

View File

@ -98,7 +98,7 @@ namespace BTCPayServer.PaymentRequest
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
LastUpdated = DateTime.UtcNow,
FormId = blob.FormId,
FormSubmitted = blob.FormResponse is not null,
FormSubmitted = !string.IsNullOrEmpty(blob.FormResponse),
AnyPendingInvoice = pendingInvoice != null,
PendingInvoiceHasPayments = pendingInvoice != null &&
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,

View File

@ -9,13 +9,11 @@ using System.Threading;
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.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
@ -41,23 +39,19 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
AppService appService,
CurrencyNameTable currencies,
StoreRepository storeRepository,
UIInvoiceController invoiceController,
FormComponentProviders formProviders)
UIInvoiceController invoiceController)
{
_currencies = currencies;
_appService = appService;
_storeRepository = storeRepository;
_invoiceController = invoiceController;
FormProviders = formProviders;
}
private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
public FormComponentProviders FormProviders { get; }
[HttpGet("/")]
[HttpGet("/apps/{appId}/pos/{viewType?}")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
@ -124,6 +118,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
string notificationUrl,
string redirectUrl,
string choiceKey,
string formId = null,
string formData = null,
string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
@ -234,12 +230,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
default:
// POST case: Handle form submit
var formData = Form.Parse(Forms.UIFormsController.GetFormData(posFormId).Config);
formData.ApplyValuesFromForm(this.Request.Form);
if (FormProviders.Validate(formData, ModelState))
if (!string.IsNullOrEmpty(formData) && formId == posFormId)
{
formResponse = JObject.FromObject(formData.GetValues());
formResponse = JObject.Parse(formData);
break;
}
@ -254,12 +247,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", posFormId }
},
FormParameters =
{
{ "formId", posFormId },
{ "redirectUrl", Request.GetCurrentUrl() + query }
}
});

View File

@ -78,7 +78,7 @@ namespace BTCPayServer.Services
using var ctx = _ContextFactory.CreateContext();
// If we are using postgres, the `transactionIds.Contains(w.BId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
// If we are using postgres, the `transactionIds.Contains(w.ChildId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
// Such request isn't well optimized by postgres, and create different requests clogging up
// pg_stat_statements output, making it impossible to analyze the performance impact of this query.
// On top of this, the entity version is doing 2 left join to satisfy the Include queries, resulting in n*m row returned for each transaction.
@ -106,9 +106,9 @@ namespace BTCPayServer.Services
var query =
$"SELECT wos.\"WalletId\", wos.\"Id\", wos.\"Type\", wos.\"Data\", wol.\"LinkData\", wol.\"Type2\", wol.\"Id2\"{includeNeighbourSelect} FROM ({selectWalletObjects}) wos " +
$"LEFT JOIN LATERAL ( " +
"SELECT \"AType\" AS \"Type2\", \"AId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"BType\"=wos.\"Type\" AND \"BId\"=wos.\"Id\" " +
"SELECT \"ParentType\" AS \"Type2\", \"ParentId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ChildType\"=wos.\"Type\" AND \"ChildId\"=wos.\"Id\" " +
"UNION " +
"SELECT \"BType\" AS \"Type2\", \"BId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"AType\"=wos.\"Type\" AND \"AId\"=wos.\"Id\"" +
"SELECT \"ChildType\" AS \"Type2\", \"ChildId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ParentType\"=wos.\"Type\" AND \"ParentId\"=wos.\"Id\"" +
$" ) wol ON true " + includeNeighbourJoin;
cmd.CommandText = query;
if (queryObject.WalletId is not null)
@ -177,21 +177,21 @@ namespace BTCPayServer.Services
else
{
wosById.Add(id, wo);
wo.Bs = new List<WalletObjectLinkData>();
wo.ChildLinks = new List<WalletObjectLinkData>();
}
if (reader["Type2"] is not DBNull)
{
var l = new WalletObjectLinkData()
{
BType = (string)reader["Type2"],
BId = (string)reader["Id2"],
ChildType = (string)reader["Type2"],
ChildId = (string)reader["Id2"],
Data = reader["LinkData"] is DBNull ? null : (string)reader["LinkData"]
};
wo.Bs.Add(l);
l.B = new WalletObjectData()
wo.ChildLinks.Add(l);
l.Child = new WalletObjectData()
{
Type = l.BType,
Id = l.BId,
Type = l.ChildType,
Id = l.ChildId,
Data = (!queryObject.IncludeNeighbours || reader["Data2"] is DBNull) ? null : (string)reader["Data2"]
};
}
@ -215,8 +215,8 @@ namespace BTCPayServer.Services
}
if (queryObject.IncludeNeighbours)
{
q = q.Include(o => o.Bs).ThenInclude(o => o.B)
.Include(o => o.As).ThenInclude(o => o.A);
q = q.Include(o => o.ChildLinks).ThenInclude(o => o.Child)
.Include(o => o.ParentLinks).ThenInclude(o => o.Parent);
}
q = q.AsNoTracking();
@ -299,10 +299,10 @@ namespace BTCPayServer.Services
var l = new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
AType = a.Type,
AId = a.Id,
BType = b.Type,
BId = b.Id,
ParentType = a.Type,
ParentId = a.Id,
ChildType = b.Type,
ChildId = b.Id,
Data = data?.ToString(Formatting.None)
};
ctx.WalletObjectLinks.Add(l);
@ -345,10 +345,10 @@ namespace BTCPayServer.Services
var l = new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
AType = a.Type,
AId = a.Id,
BType = b.Type,
BId = b.Id,
ParentType = a.Type,
ParentId = a.Id,
ChildType = b.Type,
ChildId = b.Id,
Data = data?.ToString(Formatting.None)
};
var e = ctx.WalletObjectLinks.Add(l);
@ -453,10 +453,10 @@ namespace BTCPayServer.Services
ctx.WalletObjectLinks.Remove(new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
AId = a.Id,
AType = a.Type,
BId = b.Id,
BType = b.Type
ParentId = a.Id,
ParentType = a.Type,
ChildId = b.Id,
ChildType = b.Type
});
try
{

View File

@ -1,19 +1,27 @@
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Forms
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Newtonsoft.Json.Linq
@inject FormComponentProviders FormComponentProviders
@inject FormComponentProvider FormComponentProvider
@model BTCPayServer.Abstractions.Form.Field
@if (!Model.Hidden)
@{
if (Model is not Fieldset fieldset)
{
fieldset = JObject.FromObject(Model).ToObject<Fieldset>();
}
}
@if (!fieldset.Hidden)
{
<fieldset>
<legend class="h3 mt-4 mb-3">@Model.Label</legend>
@foreach (var field in Model.Fields)
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend>
@foreach (var field in fieldset.Fields)
{
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
{
<partial name="@partial.View" for="@field"></partial>
}
var partial = FormComponentProvider.CanHandle(field);
if (string.IsNullOrEmpty(partial))
{
continue;
}
<partial name="@partial" for="@field"></partial>
}
</fieldset>
}

View File

@ -1,34 +1,31 @@
@using BTCPayServer.Abstractions.Form
@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;
if (Model is not HtmlInputField field)
{
field = JObject.FromObject(Model).ToObject<HtmlInputField>();
}
}
<div class="form-group">
@if (Model.Required)
@if (field.Required)
{
<label class="form-label" for="@Model.Name" data-required>
@Model.Label
<label class="form-label" for="@field.Name" data-required>
@field.Label
</label>
}
else
{
<label class="form-label" for="@Model.Name">
@Model.Label
<label class="form-label" for="@field.Name">
@field.Label
</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)"/>
@if(isInvalid)
{
<span class="text-danger">@error</span>
}
@if (!string.IsNullOrEmpty(Model.HelpText))
<input class="form-control @(field.IsValid() ? "" : "is-invalid")" id="@field.Name" type="@field.Type" required="@field.Required" name="@field.Name" value="@field.Value" aria-describedby="@("HelpText" + field.Name)"/>
@if (!string.IsNullOrEmpty(field.HelpText))
{
<small id="@("HelpText" + Model.Name)" class="form-text text-muted">
@Model.HelpText
<small id="@("HelpText" + field.Name)" class="form-text text-muted">
@field.HelpText
</small>
}

View File

@ -36,6 +36,7 @@
{
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>
<input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" />
@{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Type, item.Price.Value, item.Price.Value);}
</form>
}

View File

@ -1,12 +1,14 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Forms
@model BTCPayServer.Abstractions.Form.Form
@inject FormComponentProviders FormComponentProviders
@inject FormComponentProvider FormComponentProvider
@foreach (var field in Model.Fields)
{
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
var partial = FormComponentProvider.CanHandle(field);
if (string.IsNullOrEmpty(partial))
{
<partial name="@partial.View" for="@field"></partial>
continue;
}
<partial name="@partial" for="@field"></partial>
}

View File

@ -33,7 +33,7 @@
<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"/>
<input type="submit" class="btn btn-primary" value="Submit"/>
</form>
</div>
</div>

View File

@ -416,7 +416,7 @@
<h3 class="mb-3 mt-4">Webhooks</h3>
<div class="table-responsive-xl">
<table class="table table-hover table-responsive-md mb-5">
<thead>
<thead class="thead-inverse">
<tr>
<th>Status</th>
<th>ID</th>
@ -491,7 +491,7 @@
<h3 class="mb-3 mt-4">Refunds</h3>
<div class="table-responsive-xl">
<table class="table table-hover table-responsive-md mb-5">
<thead>
<thead class="thead-inverse">
<tr>
<th>Pull Payment</th>
<th>Amount</th>
@ -526,9 +526,9 @@
</table>
</div>
}
<h3 class="mb-0 mt-5">Events</h3>
<table class="table table-hover mt-3 mb-4">
<thead>
<h3 class="mb-0">Events</h3>
<table class="table table-hover">
<thead class="thead-inverse">
<tr>
<th>Date</th>
<th>Message</th>

View File

@ -477,7 +477,7 @@
"nullable": true
},
"formResponse": {
"type": "object",
"type": "string",
"description": "Form data response",
"nullable": true
}

View File

@ -34,16 +34,16 @@ public class FakeCustodian : ICustodian
var fakeConfig = ParseConfig(config);
var form = new Form();
var fieldset = Field.CreateFieldset();
var fieldset = new Fieldset();
// Maybe a decimal type field would be better?
var fakeBTCBalance = Field.Create("BTC Balance", "BTCBalance", fakeConfig?.BTCBalance.ToString(), true,
var fakeBTCBalance = new HtmlInputField("BTC Balance", "BTCBalance", fakeConfig?.BTCBalance.ToString(), true,
"Enter the amount of BTC you want to have.");
var fakeLTCBalance = Field.Create("LTC Balance", "LTCBalance", fakeConfig?.LTCBalance.ToString(), true,
var fakeLTCBalance = new HtmlInputField("LTC Balance", "LTCBalance", fakeConfig?.LTCBalance.ToString(), true,
"Enter the amount of LTC you want to have.");
var fakeEURBalance = Field.Create("EUR Balance", "EURBalance", fakeConfig?.EURBalance.ToString(), true,
var fakeEURBalance = new HtmlInputField("EUR Balance", "EURBalance", fakeConfig?.EURBalance.ToString(), true,
"Enter the amount of EUR you want to have.");
var fakeUSDBalance = Field.Create("USD Balance", "USDBalance", fakeConfig?.USDBalance.ToString(), true,
var fakeUSDBalance = new HtmlInputField("USD Balance", "USDBalance", fakeConfig?.USDBalance.ToString(), true,
"Enter the amount of USD you want to have.");
fieldset.Label = "Your fake balances";