mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Form Builder (#4137)
* wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * 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 * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * 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. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * 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> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
This commit is contained in:
parent
bb60c2ac48
commit
022285806b
65 changed files with 936 additions and 260 deletions
18
BTCPayServer.Abstractions/CamelCaseSerializerSettings.cs
Normal file
18
BTCPayServer.Abstractions/CamelCaseSerializerSettings.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,35 +1,34 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public abstract class Field
|
||||
public class Field
|
||||
{
|
||||
// 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;
|
||||
|
||||
// The name of the HTML5 node. Should be used as the key for the posted data.
|
||||
public string Name;
|
||||
|
||||
// The translated label of the field.
|
||||
public string Label;
|
||||
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// The field is considered "valid" if there are no validation errors
|
||||
public List<string> ValidationErrors = new List<string>();
|
||||
|
||||
public bool Required = false;
|
||||
|
||||
public bool IsValid()
|
||||
public virtual bool IsValid()
|
||||
{
|
||||
return ValidationErrors.Count == 0;
|
||||
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
|
||||
}
|
||||
|
||||
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
public List<Field> Fields { get; set; } = new();
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public class Fieldset
|
||||
public class Fieldset : Field
|
||||
{
|
||||
public bool Hidden { get; set; }
|
||||
public string Label { get; set; }
|
||||
|
||||
public Fieldset()
|
||||
{
|
||||
this.Fields = new List<Field>();
|
||||
Type = "fieldset";
|
||||
}
|
||||
|
||||
public string Label { get; set; }
|
||||
public List<Field> Fields { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,60 +1,154 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
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();
|
||||
|
||||
// Groups of fields in the form
|
||||
public List<Fieldset> Fieldsets { get; set; } = new();
|
||||
public List<Field> Fields { get; set; } = new();
|
||||
|
||||
|
||||
// Are all the fields valid in the form?
|
||||
public bool IsValid()
|
||||
{
|
||||
foreach (var fieldset in Fieldsets)
|
||||
{
|
||||
foreach (var field in fieldset.Fields)
|
||||
{
|
||||
if (!field.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return Fields.All(field => field.IsValid());
|
||||
}
|
||||
|
||||
public Field GetFieldByName(string name)
|
||||
{
|
||||
foreach (var fieldset in Fieldsets)
|
||||
return GetFieldByName(name, Fields, null);
|
||||
}
|
||||
|
||||
private static Field GetFieldByName(string name, List<Field> fields, string prefix)
|
||||
{
|
||||
prefix ??= string.Empty;
|
||||
foreach (var field in fields)
|
||||
{
|
||||
foreach (var field in fieldset.Fields)
|
||||
var currentPrefix = prefix;
|
||||
if (!string.IsNullOrEmpty(field.Name))
|
||||
{
|
||||
if (name.Equals(field.Name))
|
||||
|
||||
currentPrefix = $"{prefix}{field.Name}";
|
||||
if (currentPrefix.Equals(name, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return field;
|
||||
}
|
||||
|
||||
currentPrefix += "_";
|
||||
}
|
||||
|
||||
var subFieldResult = GetFieldByName(name, field.Fields, currentPrefix);
|
||||
if (subFieldResult is not null)
|
||||
{
|
||||
return subFieldResult;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<string> GetAllNames()
|
||||
{
|
||||
return GetAllNames(Fields);
|
||||
}
|
||||
|
||||
private static List<string> GetAllNames(List<Field> fields)
|
||||
{
|
||||
var names = new List<string>();
|
||||
foreach (var fieldset in Fieldsets)
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
foreach (var field in fieldset.Fields)
|
||||
string prefix = string.Empty;
|
||||
if (!string.IsNullOrEmpty(field.Name))
|
||||
{
|
||||
names.Add(field.Name);
|
||||
prefix = $"{field.Name}_";
|
||||
}
|
||||
|
||||
if (field.Fields.Any())
|
||||
{
|
||||
names.AddRange(GetAllNames(field.Fields).Select(s => $"{prefix}{s}" ));
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
public void ApplyValuesFromOtherForm(Form form)
|
||||
{
|
||||
foreach (var fieldset in Fields)
|
||||
{
|
||||
foreach (var field in fieldset.Fields)
|
||||
{
|
||||
field.Value = form
|
||||
.GetFieldByName(
|
||||
$"{(string.IsNullOrEmpty(fieldset.Name) ? string.Empty : fieldset.Name + "_")}{field.Name}")
|
||||
?.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyValuesFromForm(IFormCollection form)
|
||||
{
|
||||
var names = GetAllNames();
|
||||
foreach (var name in names)
|
||||
{
|
||||
var field = GetFieldByName(name);
|
||||
if (field is null || !form.TryGetValue(name, out var val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
field.Value = val;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetValues()
|
||||
{
|
||||
return GetValues(Fields);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> GetValues(List<Field> fields)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
foreach (Field field in fields)
|
||||
{
|
||||
var name = field.Name ?? string.Empty;
|
||||
if (field.Fields.Any())
|
||||
{
|
||||
var values = GetValues(fields);
|
||||
values.Remove(string.Empty, out var keylessValue);
|
||||
|
||||
result.TryAdd(name, values);
|
||||
|
||||
if (keylessValue is not Dictionary<string, object> dict) continue;
|
||||
foreach (KeyValuePair<string,object> keyValuePair in dict)
|
||||
{
|
||||
result.TryAdd(keyValuePair.Key, keyValuePair.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TryAdd(name, field.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
27
BTCPayServer.Abstractions/Form/HtmlInputField.cs
Normal file
27
BTCPayServer.Abstractions/Form/HtmlInputField.cs
Normal 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.
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public class TextField : Field
|
||||
{
|
||||
public TextField(string label, string name, string value, bool required, string helpText)
|
||||
{
|
||||
this.Label = label;
|
||||
this.Name = name;
|
||||
this.Value = value;
|
||||
this.OriginalValue = value;
|
||||
this.Required = required;
|
||||
this.HelpText = helpText;
|
||||
this.Type = "text";
|
||||
}
|
||||
|
||||
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
|
||||
|
||||
|
||||
}
|
|
@ -37,7 +37,7 @@ namespace BTCPayServer.Client.Models
|
|||
public string RedirectUrl { get; set; } = null;
|
||||
public bool? RedirectAutomatically { get; set; } = null;
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string CheckoutFormId { get; set; } = null;
|
||||
public string FormId { get; set; } = null;
|
||||
public string EmbeddedCSS { get; set; } = null;
|
||||
public CheckoutType? CheckoutType { get; set; } = null;
|
||||
}
|
||||
|
|
|
@ -85,8 +85,6 @@ namespace BTCPayServer.Client.Models
|
|||
public bool? RedirectAutomatically { get; set; }
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string DefaultLanguage { get; set; }
|
||||
[JsonProperty("checkoutFormId")]
|
||||
public string CheckoutFormId { get; set; }
|
||||
public CheckoutType? CheckoutType { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,5 +24,9 @@ namespace BTCPayServer.Client.Models
|
|||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
public string FormId { get; set; }
|
||||
|
||||
public JObject FormResponse { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ namespace BTCPayServer.Client.Models
|
|||
public DateTimeOffset CreatedTime { get; set; }
|
||||
public string Id { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
public enum PaymentRequestStatus
|
||||
{
|
||||
Pending = 0,
|
||||
|
|
|
@ -31,8 +31,6 @@ namespace BTCPayServer.Client.Models
|
|||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string CheckoutFormId { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public CheckoutType CheckoutType { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
|
12
BTCPayServer.Data/Data/FormData.cs
Normal file
12
BTCPayServer.Data/Data/FormData.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data.Data;
|
||||
|
||||
public class FormData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Config { get; set; }
|
||||
}
|
|
@ -1599,13 +1599,11 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
RedirectAutomatically = true,
|
||||
RequiresRefundEmail = true,
|
||||
CheckoutFormId = GenericFormOption.Email.ToString()
|
||||
},
|
||||
AdditionalSearchTerms = new string[] { "Banana" }
|
||||
});
|
||||
Assert.True(newInvoice.Checkout.RedirectAutomatically);
|
||||
Assert.True(newInvoice.Checkout.RequiresRefundEmail);
|
||||
Assert.Equal(GenericFormOption.Email.ToString(), newInvoice.Checkout.CheckoutFormId);
|
||||
Assert.Equal(user.StoreId, newInvoice.StoreId);
|
||||
//list
|
||||
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
||||
|
|
|
@ -64,6 +64,61 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.Quit();
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseForms()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
|
||||
// Point Of Sale
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
|
||||
new SelectElement(s.Driver.FindElement(By.Id("SelectedAppType"))).SelectByValue("PointOfSale");
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.FindElement(By.CssSelector("button[type='submit']")).Click();
|
||||
|
||||
Assert.Contains("Enter your email", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
|
||||
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
|
||||
|
||||
s.PayInvoice(true);
|
||||
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
|
||||
s.GoToInvoice(invoiceId);
|
||||
Assert.Contains("aa@aa.com", s.Driver.PageSource);
|
||||
|
||||
// Payment Request
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
var editUrl = s.Driver.Url;
|
||||
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='form-button']")).Click();
|
||||
Assert.Contains("Enter your email", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
|
||||
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
|
||||
s.Driver.Navigate().GoToUrl(editUrl);
|
||||
Assert.Contains("aa@aa.com", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseCPFP()
|
||||
{
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
|
||||
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
|
||||
<_Parameter1>$(GitCommit)</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -233,7 +233,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
EmbeddedCSS = request.EmbeddedCSS,
|
||||
RedirectAutomatically = request.RedirectAutomatically,
|
||||
RequiresRefundEmail = BoolToRequiresRefundEmail(request.RequiresRefundEmail) ?? RequiresRefundEmail.InheritFromStore,
|
||||
CheckoutFormId = request.CheckoutFormId,
|
||||
FormId = request.FormId,
|
||||
CheckoutType = request.CheckoutType ?? CheckoutType.V1
|
||||
};
|
||||
}
|
||||
|
|
|
@ -437,7 +437,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
DefaultLanguage = entity.DefaultLanguage,
|
||||
RedirectAutomatically = entity.RedirectAutomatically,
|
||||
RequiresRefundEmail = entity.RequiresRefundEmail,
|
||||
CheckoutFormId = entity.CheckoutFormId,
|
||||
CheckoutType = entity.CheckoutType,
|
||||
RedirectURL = entity.RedirectURLTemplate
|
||||
},
|
||||
|
|
|
@ -127,7 +127,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
CheckoutFormId = storeBlob.CheckoutFormId,
|
||||
CheckoutType = storeBlob.CheckoutType,
|
||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
|
@ -167,7 +166,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
||||
blob.DefaultCurrency = restModel.DefaultCurrency;
|
||||
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
||||
blob.CheckoutFormId = restModel.CheckoutFormId;
|
||||
blob.CheckoutType = restModel.CheckoutType;
|
||||
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
|
|
|
@ -179,7 +179,12 @@ 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 =>
|
||||
{
|
||||
|
@ -229,7 +234,6 @@ namespace BTCPayServer.Controllers
|
|||
? new Dictionary<string, object>()
|
||||
: PosDataParser.ParsePosData(receiptData.ToString())
|
||||
});
|
||||
|
||||
}
|
||||
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
|
||||
{
|
||||
|
@ -760,7 +764,6 @@ namespace BTCPayServer.Controllers
|
|||
CustomLogoLink = storeBlob.CustomLogo,
|
||||
LogoFileId = storeBlob.LogoFileId,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
CheckoutFormId = invoice.CheckoutFormId ?? storeBlob.CheckoutFormId,
|
||||
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
||||
|
@ -1140,9 +1143,6 @@ namespace BTCPayServer.Controllers
|
|||
RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? storeBlob.RequiresRefundEmail
|
||||
: model.RequiresRefundEmail == RequiresRefundEmail.On,
|
||||
CheckoutFormId = model.CheckoutFormId == GenericFormOption.InheritFromStore.ToString()
|
||||
? storeBlob.CheckoutFormId
|
||||
: model.CheckoutFormId
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
|
||||
|
|
|
@ -142,7 +142,6 @@ namespace BTCPayServer.Controllers
|
|||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
||||
entity.CheckoutFormId = invoice.CheckoutFormId;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
IPaymentFilter? excludeFilter = null;
|
||||
|
@ -227,7 +226,6 @@ namespace BTCPayServer.Controllers
|
|||
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
||||
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
|
||||
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
||||
entity.CheckoutFormId = invoice.Checkout.CheckoutFormId;
|
||||
entity.CheckoutType = invoice.Checkout.CheckoutType;
|
||||
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
|
||||
IPaymentFilter? excludeFilter = null;
|
||||
|
|
|
@ -9,6 +9,7 @@ using BTCPayServer.Client;
|
|||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
@ -16,9 +17,11 @@ 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;
|
||||
|
||||
|
@ -145,6 +148,7 @@ namespace BTCPayServer.Controllers
|
|||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||
blob.FormId = viewModel.FormId;
|
||||
|
||||
data.SetBlob(blob);
|
||||
var isNewPaymentRequest = string.IsNullOrEmpty(payReqId);
|
||||
|
@ -174,6 +178,51 @@ namespace BTCPayServer.Controllers
|
|||
return View(result);
|
||||
}
|
||||
|
||||
[HttpGet("{payReqId}/form")]
|
||||
[HttpPost("{payReqId}/form")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId, [FromForm] string formId, [FromForm] string formData)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var prBlob = result.GetBlob();
|
||||
var prFormId = prBlob.FormId;
|
||||
switch (prFormId)
|
||||
{
|
||||
case null:
|
||||
case { } when string.IsNullOrEmpty(prFormId):
|
||||
break;
|
||||
|
||||
default:
|
||||
// POST case: Handle form submit
|
||||
if (!string.IsNullOrEmpty(formData) && formId == prFormId)
|
||||
{
|
||||
prBlob.FormResponse = JObject.Parse(formData);
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
}
|
||||
|
||||
// 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 RedirectToAction("ViewPaymentRequest", new { payReqId });
|
||||
}
|
||||
|
||||
[HttpGet("{payReqId}/pay")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PayPaymentRequest(string payReqId, bool redirectToInvoice = true,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using BTCPayServer.Services.Invoices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
|
@ -8,7 +9,7 @@ public static class CustodianAccountDataExtensions
|
|||
{
|
||||
var result = custodianAccountData.Blob == null
|
||||
? new JObject()
|
||||
: JObject.Parse(ZipUtils.Unzip(custodianAccountData.Blob));
|
||||
: InvoiceRepository.FromBytes<JObject>(custodianAccountData.Blob);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -17,7 +18,8 @@ public static class CustodianAccountDataExtensions
|
|||
var original = custodianAccountData.GetBlob();
|
||||
if (JToken.DeepEquals(original, blob))
|
||||
return false;
|
||||
custodianAccountData.Blob = blob is null ? null : ZipUtils.Zip(blob.ToString(Newtonsoft.Json.Formatting.None));
|
||||
|
||||
custodianAccountData.Blob = blob is null ? null : InvoiceRepository.ToBytes(blob);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,6 @@ namespace BTCPayServer.Data
|
|||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public CheckoutType CheckoutType { get; set; }
|
||||
public string CheckoutFormId { get; set; }
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
public bool LightningPrivateRouteHints { get; set; }
|
||||
|
|
20
BTCPayServer/Forms/FormComponentProvider.cs
Normal file
20
BTCPayServer/Forms/FormComponentProvider.cs
Normal 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));
|
||||
}
|
||||
}
|
22
BTCPayServer/Forms/FormDataExtensions.cs
Normal file
22
BTCPayServer/Forms/FormDataExtensions.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using BTCPayServer.Data.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public static class FormDataExtensions
|
||||
{
|
||||
public static void AddForms(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<FormDataService>();
|
||||
serviceCollection.AddSingleton<FormComponentProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
|
||||
}
|
||||
|
||||
public static string Serialize(this JObject form)
|
||||
{
|
||||
return JsonConvert.SerializeObject(form);
|
||||
}
|
||||
}
|
35
BTCPayServer/Forms/FormDataService.cs
Normal file
35
BTCPayServer/Forms/FormDataService.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class FormDataService
|
||||
{
|
||||
|
||||
public static readonly Form StaticFormEmail = new()
|
||||
{
|
||||
Fields = new List<Field>() {new HtmlInputField("Enter your email", "buyerEmail", null, true, null)}
|
||||
};
|
||||
|
||||
public static readonly Form StaticFormAddress = new()
|
||||
{
|
||||
Fields = new List<Field>()
|
||||
{
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
12
BTCPayServer/Forms/HtmlFieldsetFormProvider.cs
Normal file
12
BTCPayServer/Forms/HtmlFieldsetFormProvider.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class HtmlFieldsetFormProvider: IFormComponentProvider
|
||||
{
|
||||
public string CanHandle(Field field)
|
||||
{
|
||||
return new[] { "fieldset"}.Contains(field.Type) ? "Forms/FieldSetElement" : null;
|
||||
}
|
||||
}
|
34
BTCPayServer/Forms/HtmlInputFormProvider.cs
Normal file
34
BTCPayServer/Forms/HtmlInputFormProvider.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class HtmlInputFormProvider: IFormComponentProvider
|
||||
{
|
||||
public string CanHandle(Field field)
|
||||
{
|
||||
return new[] {
|
||||
"text",
|
||||
"radio",
|
||||
"checkbox",
|
||||
"password",
|
||||
"file",
|
||||
"hidden",
|
||||
"button",
|
||||
"submit",
|
||||
"color",
|
||||
"date",
|
||||
"datetime-local",
|
||||
"month",
|
||||
"week",
|
||||
"time",
|
||||
"email",
|
||||
"image",
|
||||
"number",
|
||||
"range",
|
||||
"search",
|
||||
"url",
|
||||
"tel",
|
||||
"reset"}.Contains(field.Type) ? "Forms/InputElement" : null;
|
||||
}
|
||||
}
|
8
BTCPayServer/Forms/IFormComponentProvider.cs
Normal file
8
BTCPayServer/Forms/IFormComponentProvider.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public interface IFormComponentProvider
|
||||
{
|
||||
public string CanHandle(Field field);
|
||||
}
|
13
BTCPayServer/Forms/Models/FormViewModel.cs
Normal file
13
BTCPayServer/Forms/Models/FormViewModel.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms.Models;
|
||||
|
||||
public class FormViewModel
|
||||
{
|
||||
public string RedirectUrl { get; set; }
|
||||
public FormData FormData { get; set; }
|
||||
Form _Form;
|
||||
public Form Form { get => _Form ??= Form.Parse(FormData.Config); }
|
||||
}
|
11
BTCPayServer/Forms/ModifyForm.cs
Normal file
11
BTCPayServer/Forms/ModifyForm.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class ModifyForm
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
[DisplayName("Form configuration (JSON)")]
|
||||
public string FormConfig { get; set; }
|
||||
}
|
99
BTCPayServer/Forms/UIFormsController.cs
Normal file
99
BTCPayServer/Forms/UIFormsController.cs
Normal file
|
@ -0,0 +1,99 @@
|
|||
#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
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/forms/{formId}")]
|
||||
[HttpPost("~/forms")]
|
||||
public IActionResult ViewPublicForm(string? formId, string? redirectUrl)
|
||||
{
|
||||
FormData? formData = string.IsNullOrEmpty(formId) ? null : GetFormData(formId);
|
||||
if (formData == null)
|
||||
{
|
||||
return string.IsNullOrEmpty(redirectUrl)
|
||||
? NotFound()
|
||||
: Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
return View("View", new FormViewModel() { FormData = formData, RedirectUrl = redirectUrl });
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("~/forms/{formId}")]
|
||||
public IActionResult SubmitForm(
|
||||
string formId, string? redirectUrl,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromServices] UIInvoiceController invoiceController)
|
||||
{
|
||||
var formData = GetFormData(formId);
|
||||
if (formData?.Config is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var dbForm = Form.Parse(formData.Config);
|
||||
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))
|
||||
{
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
FormUrl = redirectUrl,
|
||||
FormParameters =
|
||||
{
|
||||
{ "formId", formId },
|
||||
{ "formData", JsonConvert.SerializeObject(data) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
private 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
|
||||
};
|
||||
return form;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ using BTCPayServer.Controllers;
|
|||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Payouts.LightningLike;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
|
@ -437,6 +438,7 @@ namespace BTCPayServer.Hosting
|
|||
//also provide a factory that can impersonate user/store id
|
||||
services.AddSingleton<IBTCPayServerClientFactory, BTCPayServerClientFactory>();
|
||||
services.AddPayoutProcesors();
|
||||
services.AddForms();
|
||||
|
||||
services.AddAPIKeyAuthentication();
|
||||
services.AddBtcPayServerAuthenticationSchemes();
|
||||
|
|
|
@ -81,9 +81,6 @@ namespace BTCPayServer.Models
|
|||
public bool? RedirectAutomatically { get; set; }
|
||||
[JsonProperty(PropertyName = "requiresRefundEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool? RequiresRefundEmail { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "checkoutFormId", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string CheckoutFormId { get; set; }
|
||||
|
||||
//Bitpay compatibility: create invoice in btcpay uses this instead of supportedTransactionCurrencies
|
||||
[JsonProperty(PropertyName = "paymentCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
|
|
|
@ -268,9 +268,6 @@ namespace BTCPayServer.Models
|
|||
public Dictionary<string, InvoiceCryptoInfo.InvoicePaymentUrls> PaymentCodes { get; set; }
|
||||
[JsonProperty("buyer")]
|
||||
public JObject Buyer { get; set; }
|
||||
|
||||
[JsonProperty("checkoutFormId")]
|
||||
public string CheckoutFormId { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public CheckoutType? CheckoutType { get; set; }
|
||||
}
|
||||
|
|
|
@ -88,9 +88,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Request customer data on checkout")]
|
||||
public string CheckoutFormId { get; set; }
|
||||
|
||||
public bool UseNewCheckout { get; set; }
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public bool Activated { get; set; }
|
||||
public string InvoiceCurrency { get; set; }
|
||||
public string ReceiptLink { get; set; }
|
||||
public string CheckoutFormId { get; set; }
|
||||
public bool AltcoinsBuild { get; set; }
|
||||
public CheckoutType CheckoutType { get; set; }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using BTCPayServer.Services.Invoices;
|
|||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
|
||||
namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
|
@ -35,6 +36,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||
StoreId = data.StoreDataId;
|
||||
Archived = data.Archived;
|
||||
var blob = data.GetBlob();
|
||||
FormId = blob.FormId;
|
||||
Title = blob.Title;
|
||||
Amount = blob.Amount;
|
||||
Currency = blob.Currency;
|
||||
|
@ -44,8 +46,14 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||
CustomCSSLink = blob.CustomCSSLink;
|
||||
EmbeddedCSS = blob.EmbeddedCSS;
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||
FormResponse = blob.FormResponse is null
|
||||
? null
|
||||
: blob.FormResponse.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
|
||||
[Display(Name = "Request customer data on checkout")]
|
||||
public string FormId { get; set; }
|
||||
|
||||
public bool Archived { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
|
@ -77,6 +85,8 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||
public string EmbeddedCSS { get; set; }
|
||||
[Display(Name = "Allow payee to create invoices in their own denomination")]
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
|
||||
public Dictionary<string, object> FormResponse { get; set; }
|
||||
}
|
||||
|
||||
public class ViewPaymentRequestViewModel
|
||||
|
@ -165,6 +175,8 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||
public bool PendingInvoiceHasPayments { get; set; }
|
||||
public string HubPath { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public string FormId { get; set; }
|
||||
public bool FormSubmitted { get; set; }
|
||||
|
||||
public class PaymentRequestInvoice
|
||||
{
|
||||
|
|
|
@ -97,6 +97,8 @@ namespace BTCPayServer.PaymentRequest
|
|||
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
|
||||
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
||||
LastUpdated = DateTime.UtcNow,
|
||||
FormId = blob.FormId,
|
||||
FormSubmitted = blob.FormResponse is not null,
|
||||
AnyPendingInvoice = pendingInvoice != null,
|
||||
PendingInvoiceHasPayments = pendingInvoice != null &&
|
||||
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,
|
||||
|
|
|
@ -18,6 +18,7 @@ using BTCPayServer.ModelBinders;
|
|||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -25,6 +26,7 @@ using Microsoft.AspNetCore.Cors;
|
|||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
|
@ -116,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)
|
||||
|
@ -214,7 +218,42 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var store = await _appService.GetStore(app);
|
||||
var posFormId = settings.FormId;
|
||||
JObject formResponse = null;
|
||||
switch (posFormId)
|
||||
{
|
||||
case null:
|
||||
case { } when string.IsNullOrEmpty(posFormId):
|
||||
break;
|
||||
|
||||
default:
|
||||
// POST case: Handle form submit
|
||||
if (!string.IsNullOrEmpty(formData) && formId == posFormId)
|
||||
{
|
||||
formResponse = JObject.Parse(formData);
|
||||
break;
|
||||
}
|
||||
|
||||
var query = new QueryBuilder(Request.Query);
|
||||
foreach (var keyValuePair in Request.Form)
|
||||
{
|
||||
query.Add(keyValuePair.Key, keyValuePair.Value.ToArray());
|
||||
}
|
||||
|
||||
// GET or empty form data case: Redirect to form
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspController = "UIForms",
|
||||
AspAction = "ViewPublicForm",
|
||||
FormParameters =
|
||||
{
|
||||
{ "formId", posFormId },
|
||||
{ "redirectUrl", Request.GetCurrentUrl() + query }
|
||||
}
|
||||
});
|
||||
}
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest
|
||||
|
@ -235,7 +274,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
CheckoutFormId = store.GetStoreBlob().CheckoutFormId,
|
||||
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? store.GetStoreBlob().RequiresRefundEmail
|
||||
: requiresRefundEmail == RequiresRefundEmail.On,
|
||||
|
@ -244,6 +282,13 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
cancellationToken, (entity) =>
|
||||
{
|
||||
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
|
||||
|
||||
if (formResponse is not null)
|
||||
{
|
||||
var meta = entity.Metadata.ToJObject();
|
||||
meta.Merge(formResponse);
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(meta);
|
||||
}
|
||||
} );
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
|
@ -298,8 +343,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
|
||||
RequiresRefundEmail = settings.RequiresRefundEmail,
|
||||
CheckoutFormId = settings.CheckoutFormId,
|
||||
UseNewCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V2
|
||||
FormId = settings.FormId
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
|
@ -389,14 +433,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically),
|
||||
RequiresRefundEmail = vm.RequiresRefundEmail
|
||||
};
|
||||
|
||||
if (storeBlob.CheckoutType == Client.Models.CheckoutType.V2)
|
||||
{
|
||||
settings.CheckoutFormId = vm.CheckoutFormId == GenericFormOption.InheritFromStore.ToString()
|
||||
? storeBlob.CheckoutFormId
|
||||
: vm.CheckoutFormId;
|
||||
}
|
||||
|
||||
settings.FormId = vm.FormId;
|
||||
app.Name = vm.AppName;
|
||||
app.SetSettings(settings);
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
|
|
|
@ -101,10 +101,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
|||
|
||||
[Display(Name = "Require refund email on checkout")]
|
||||
public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore;
|
||||
|
||||
[Display(Name = "Request customer data on checkout")]
|
||||
public string CheckoutFormId { get; set; } = GenericFormOption.InheritFromStore.ToString();
|
||||
|
||||
public bool UseNewCheckout { get; set; }
|
||||
[Display(Name = "Request customer data on checkout")]
|
||||
public string FormId { get; set; } = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace BTCPayServer.Services.Apps
|
|||
public bool EnableTips { get; set; }
|
||||
public RequiresRefundEmail RequiresRefundEmail { get; set; }
|
||||
|
||||
public string CheckoutFormId { get; set; } = GenericFormOption.InheritFromStore.ToString();
|
||||
public string FormId { get; set; } = null;
|
||||
|
||||
public const string BUTTON_TEXT_DEF = "Buy for {0}";
|
||||
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
||||
|
|
|
@ -446,8 +446,6 @@ namespace BTCPayServer.Services.Invoices
|
|||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; }
|
||||
|
||||
[JsonProperty("checkoutFormId")]
|
||||
public string CheckoutFormId { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public CheckoutType? CheckoutType { get; set; }
|
||||
|
||||
|
@ -578,7 +576,6 @@ namespace BTCPayServer.Services.Invoices
|
|||
dto.TaxIncluded = Metadata.TaxIncluded ?? 0m;
|
||||
dto.Price = Price;
|
||||
dto.Currency = Currency;
|
||||
dto.CheckoutFormId = CheckoutFormId;
|
||||
dto.CheckoutType = CheckoutType;
|
||||
dto.Buyer = new JObject();
|
||||
dto.Buyer.Add(new JProperty("name", Metadata.BuyerName));
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||
|
||||
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
|
||||
{
|
||||
using var context = _ContextFactory.CreateContext();
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
if (string.IsNullOrEmpty(entity.Id))
|
||||
{
|
||||
entity.Id = Guid.NewGuid().ToString();
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Services.Stores;
|
||||
|
||||
public enum GenericFormOption
|
||||
{
|
||||
[Display(Name = "Inherit from store settings")]
|
||||
InheritFromStore,
|
||||
|
||||
[Display(Name = "Do not request any information")]
|
||||
None,
|
||||
|
||||
|
@ -24,24 +19,14 @@ public enum GenericFormOption
|
|||
|
||||
public static class CheckoutFormSelectList
|
||||
{
|
||||
public static SelectList ForStore(StoreData store, string selectedFormId, bool isStoreEntity)
|
||||
public static SelectList WithSelected(string selectedFormId)
|
||||
{
|
||||
var choices = new List<SelectListItem>();
|
||||
|
||||
if (isStoreEntity)
|
||||
var choices = new List<SelectListItem>
|
||||
{
|
||||
var blob = store.GetStoreBlob();
|
||||
var inherit = GenericOptionItem(GenericFormOption.InheritFromStore);
|
||||
inherit.Text += Enum.TryParse<GenericFormOption>(blob.CheckoutFormId, out var item)
|
||||
? $" ({DisplayName(item)})"
|
||||
: $" ({blob.CheckoutFormId})";
|
||||
|
||||
choices.Add(inherit);
|
||||
}
|
||||
|
||||
choices.Add(GenericOptionItem(GenericFormOption.None));
|
||||
choices.Add(GenericOptionItem(GenericFormOption.Email));
|
||||
choices.Add(GenericOptionItem(GenericFormOption.Address));
|
||||
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);
|
||||
|
|
27
BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml
Normal file
27
BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml
Normal file
|
@ -0,0 +1,27 @@
|
|||
@using BTCPayServer.Abstractions.Form
|
||||
@using BTCPayServer.Forms
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Newtonsoft.Json.Linq
|
||||
@inject FormComponentProvider FormComponentProvider
|
||||
@model BTCPayServer.Abstractions.Form.Field
|
||||
@{
|
||||
if (Model is not Fieldset fieldset)
|
||||
{
|
||||
fieldset = JObject.FromObject(Model).ToObject<Fieldset>();
|
||||
}
|
||||
}
|
||||
@if (!fieldset.Hidden)
|
||||
{
|
||||
<fieldset>
|
||||
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend>
|
||||
@foreach (var field in fieldset.Fields)
|
||||
{
|
||||
var partial = FormComponentProvider.CanHandle(field);
|
||||
if (string.IsNullOrEmpty(partial))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
<partial name="@partial" for="@field"></partial>
|
||||
}
|
||||
</fieldset>
|
||||
}
|
33
BTCPayServer/Views/Shared/Forms/InputElement.cshtml
Normal file
33
BTCPayServer/Views/Shared/Forms/InputElement.cshtml
Normal file
|
@ -0,0 +1,33 @@
|
|||
@using BTCPayServer.Abstractions.Form
|
||||
@using Newtonsoft.Json.Linq
|
||||
@model BTCPayServer.Abstractions.Form.Field
|
||||
@{
|
||||
if (Model is not HtmlInputField field)
|
||||
{
|
||||
field = JObject.FromObject(Model).ToObject<HtmlInputField>();
|
||||
}
|
||||
}
|
||||
<div class="form-group">
|
||||
@if (field.Required)
|
||||
{
|
||||
<label class="form-label" for="@field.Name" data-required>
|
||||
@field.Label
|
||||
</label>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label class="form-label" for="@field.Name">
|
||||
@field.Label
|
||||
</label>
|
||||
}
|
||||
|
||||
<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" + field.Name)" class="form-text text-muted">
|
||||
@field.HelpText
|
||||
</small>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
|
@ -7,8 +7,7 @@
|
|||
@{
|
||||
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
|
||||
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
var checkoutFormOptions = CheckoutFormSelectList.ForStore(store, Model.CheckoutFormId, true);
|
||||
var checkoutFormOptions = CheckoutFormSelectList.WithSelected(Model.FormId);
|
||||
}
|
||||
|
||||
<form method="post">
|
||||
|
@ -84,18 +83,14 @@
|
|||
<span asp-validation-for="ButtonText" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@if (Model.UseNewCheckout)
|
||||
{
|
||||
<label asp-for="CheckoutFormId" class="form-label"></label>
|
||||
<select asp-for="CheckoutFormId" class="form-select w-auto" asp-items="@checkoutFormOptions"></select>
|
||||
<span asp-validation-for="CheckoutFormId" class="text-danger"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||
}
|
||||
<label asp-for="FormId" class="form-label"></label>
|
||||
<select asp-for="FormId" class="form-select w-auto" asp-items="@checkoutFormOptions"></select>
|
||||
<span asp-validation-for="FormId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<section id="discounts" class="p-0">
|
||||
<h3 class="mt-5 mb-4">Discounts</h3>
|
||||
|
|
60
BTCPayServer/Views/Shared/PosData.cshtml
Normal file
60
BTCPayServer/Views/Shared/PosData.cshtml
Normal file
|
@ -0,0 +1,60 @@
|
|||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
@functions {
|
||||
private bool IsValidURL(string source)
|
||||
{
|
||||
return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) &&
|
||||
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
}
|
||||
|
||||
<table class="table my-0">
|
||||
@foreach (var (key, value) in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
@if (value is string str)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
<th class="w-150px">@Safe.Raw(key)</th>
|
||||
}
|
||||
<td>
|
||||
@if (IsValidURL(str))
|
||||
{
|
||||
<a href="@Safe.Raw(str)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str)</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Safe.Raw(value?.ToString())
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else if (value is Dictionary<string, object>subItems)
|
||||
{
|
||||
@* This is the array case *@
|
||||
if (subItems.Count == 1 && subItems.First().Value is string str2)
|
||||
{
|
||||
<th class="w-150px">@Safe.Raw(key)</th>
|
||||
<td>
|
||||
@if (IsValidURL(str2))
|
||||
{
|
||||
<a href="@Safe.Raw(str2)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str2)</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Safe.Raw(subItems.First().Value?.ToString())
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td colspan="2" >
|
||||
@Safe.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">{key}</h{Model.Level + 3}>")
|
||||
<partial name="PosData" model="(subItems, Model.Level + 1)"/>
|
||||
</td>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
|
|
@ -1,34 +1,14 @@
|
|||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Forms
|
||||
@model BTCPayServer.Abstractions.Form.Form
|
||||
@inject FormComponentProvider FormComponentProvider
|
||||
|
||||
@foreach (var fieldset in Model.Fieldsets)
|
||||
@foreach (var field in Model.Fields)
|
||||
{
|
||||
<fieldset>
|
||||
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend>
|
||||
@foreach (var field in fieldset.Fields)
|
||||
{
|
||||
@if ("text".Equals(field.Type))
|
||||
{
|
||||
<div class="form-group">
|
||||
@if (field.Required)
|
||||
{
|
||||
<label class="form-label" for="@field.Name" data-required>
|
||||
@field.Label
|
||||
</label>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label class="form-label" for="@field.Name">
|
||||
@field.Label
|
||||
</label>
|
||||
}
|
||||
|
||||
<input class="form-control @(field.IsValid() ? "" : "is-invalid")" id="@field.Name" type="text" required="@field.Required" name="@field.Name" value="@field.Value" aria-describedby="HelpText@field.Name"/>
|
||||
<small id="HelpText@field.Name" class="form-text text-muted">
|
||||
@field.HelpText
|
||||
</small>
|
||||
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</fieldset>
|
||||
var partial = FormComponentProvider.CanHandle(field);
|
||||
if (string.IsNullOrEmpty(partial))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
<partial name="@partial" for="@field"></partial>
|
||||
}
|
||||
|
|
56
BTCPayServer/Views/UIForms/View.cshtml
Normal file
56
BTCPayServer/Views/UIForms/View.cshtml
Normal file
|
@ -0,0 +1,56 @@
|
|||
@using BTCPayServer.Components.ThemeSwitch
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||
@inject BTCPayServer.Services.ThemeSettings Theme
|
||||
@model BTCPayServer.Forms.Models.FormViewModel
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = Model.FormData.Name;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" @(env.IsDeveloping ? " data-devenv" : "")>
|
||||
<head>
|
||||
<partial name="LayoutHead"/>
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
</head>
|
||||
<body>
|
||||
<div class="min-vh-100 d-flex flex-column">
|
||||
<main class="flex-grow-1 py-5">
|
||||
<div class="container" style="max-width:720px;">
|
||||
<partial name="_StatusMessage" model="@(new ViewDataDictionary(ViewData) {{"Margin", "mb-4"}})"/>
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></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" value="Submit"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="pt-2 pb-4 d-print-none">
|
||||
<div class="container d-flex flex-wrap align-items-center justify-content-center">
|
||||
<span class="text-muted mx-2">
|
||||
Powered by <a href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">BTCPay Server</a>
|
||||
</span>
|
||||
@if (!Theme.CustomTheme)
|
||||
{
|
||||
<vc:theme-switch css-class="text-muted mx-2" responsive="none"/>
|
||||
}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<partial name="LayoutFoot"/>
|
||||
</body>
|
||||
</html>
|
3
BTCPayServer/Views/UIForms/_ViewImports.cshtml
Normal file
3
BTCPayServer/Views/UIForms/_ViewImports.cshtml
Normal file
|
@ -0,0 +1,3 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Views.Stores
|
||||
@using BTCPayServer.Models.StoreViewModels
|
7
BTCPayServer/Views/UIForms/_ViewStart.cshtml
Normal file
7
BTCPayServer/Views/UIForms/_ViewStart.cshtml
Normal file
|
@ -0,0 +1,7 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Views
|
||||
@using BTCPayServer.Views.Stores
|
||||
|
||||
@{
|
||||
ViewData.SetActiveCategory(typeof(StoreNavPages));
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
@model BTCPayServer.Models.InvoicingModels.CreateInvoiceModel
|
||||
@using BTCPayServer.Services.Apps
|
||||
@using BTCPayServer.Services.Stores
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@{
|
||||
ViewData.SetActivePage(InvoiceNavPages.Create, "Create Invoice");
|
||||
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
var checkoutFormOptions = CheckoutFormSelectList.ForStore(store, Model.CheckoutFormId, true);
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
|
@ -90,22 +89,13 @@
|
|||
<h4 class="mt-5 mb-4">Customer Information</h4>
|
||||
<div class="form-group">
|
||||
<label asp-for="BuyerEmail" class="form-label"></label>
|
||||
<input asp-for="BuyerEmail" class="form-control" />
|
||||
<input asp-for="BuyerEmail" class="form-control"/>
|
||||
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@if (Model.UseNewCheckout)
|
||||
{
|
||||
<label asp-for="CheckoutFormId" class="form-label"></label>
|
||||
<select asp-for="CheckoutFormId" class="form-select w-auto" asp-items="@checkoutFormOptions"></select>
|
||||
<span asp-validation-for="CheckoutFormId" class="text-danger"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||
}
|
||||
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-5 mb-2">Additional Options</h4>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
else if (!isSettled)
|
||||
{
|
||||
<div class="lead text-center text-muted py-5 px-4 fw-semibold" id="invoice-unsettled">
|
||||
<div class="lead text-center text-muted py-5 px-4 fw-semibold" id="invoice-unsettled">
|
||||
The invoice is not settled.
|
||||
</div>
|
||||
}
|
||||
|
@ -78,24 +78,24 @@
|
|||
<div class="d-flex flex-column">
|
||||
<dd class="text-muted mb-0 fw-semibold">Order ID</dd>
|
||||
<dt class="fs-5 mb-0 text-break fw-semibold">
|
||||
@if (!string.IsNullOrEmpty(Model.OrderUrl))
|
||||
{
|
||||
<a href="@Model.OrderUrl" rel="noreferrer noopener" target="_blank">
|
||||
@if (string.IsNullOrEmpty(Model.OrderId))
|
||||
{
|
||||
<span>View Order</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Model.OrderId
|
||||
}
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Model.OrderId</span>
|
||||
}
|
||||
</dt>
|
||||
@if (!string.IsNullOrEmpty(Model.OrderUrl))
|
||||
{
|
||||
<a href="@Model.OrderUrl" rel="noreferrer noopener" target="_blank">
|
||||
@if (string.IsNullOrEmpty(Model.OrderId))
|
||||
{
|
||||
<span>View Order</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Model.OrderId
|
||||
}
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Model.OrderId</span>
|
||||
}
|
||||
</dt>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
@functions{
|
||||
public bool IsValidURL(string source)
|
||||
@functions {
|
||||
private bool IsValidURL(string source)
|
||||
{
|
||||
Uri uriResult;
|
||||
return Uri.TryCreate(source, UriKind.Absolute, out uriResult) &&
|
||||
return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) &&
|
||||
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
@using BTCPayServer.Services.PaymentRequests
|
||||
@using System.Globalization
|
||||
@using BTCPayServer.Services.Stores
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
||||
@{
|
||||
var checkoutFormOptions = CheckoutFormSelectList.WithSelected(Model.FormId);
|
||||
|
||||
ViewData.SetActivePage(PaymentRequestsNavPages.Create, $"{(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit")} Payment Request", Model.Id);
|
||||
}
|
||||
|
||||
|
@ -32,7 +37,7 @@
|
|||
</div>
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
@ -78,6 +83,18 @@
|
|||
Receive updates for this payment request.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="FormId" class="form-label"></label>
|
||||
<select asp-for="FormId" class="form-select w-auto" asp-items="@checkoutFormOptions"></select>
|
||||
<span asp-validation-for="FormId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
@if (Model.FormResponse is not null)
|
||||
{
|
||||
<div class="bg-tile rounded py-2 px-3 mb-5">
|
||||
<partial name="PosData" model="(Model.FormResponse, 1)"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -130,20 +147,19 @@
|
|||
@if (!string.IsNullOrEmpty(Model.Id))
|
||||
{
|
||||
<div class="d-flex gap-3 mt-3">
|
||||
<a class="btn btn-secondary"
|
||||
asp-action="ListInvoices"
|
||||
asp-controller="UIInvoice"
|
||||
asp-route-storeId="@Model.StoreId"
|
||||
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
|
||||
<a class="btn btn-secondary" asp-route-payReqId="@Model.Id" asp-action="ClonePaymentRequest" id="ClonePaymentRequest">Clone</a>
|
||||
@if (!Model.Archived)
|
||||
{
|
||||
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Archive this payment request so that it does not appear in the payment request list by default" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id" id="ArchivePaymentRequest">Archive</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Unarchive this payment request" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id" id="UnarchivePaymentRequest">Unarchive</a>
|
||||
}
|
||||
</div>
|
||||
<a class="btn btn-secondary"
|
||||
asp-action="ListInvoices"
|
||||
asp-controller="UIInvoice"
|
||||
asp-route-storeId="@Model.StoreId"
|
||||
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
|
||||
<a class="btn btn-secondary" asp-route-payReqId="@Model.Id" asp-action="ClonePaymentRequest" id="ClonePaymentRequest">Clone</a>
|
||||
@if (!Model.Archived)
|
||||
{
|
||||
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Archive this payment request so that it does not appear in the payment request list by default" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id" id="ArchivePaymentRequest">Archive</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-secondary" data-bs-toggle="tooltip" title="Unarchive this payment request" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id" id="UnarchivePaymentRequest">Unarchive</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -144,7 +144,12 @@
|
|||
</div>
|
||||
}
|
||||
</noscript>
|
||||
<template v-if="srvModel.isPending && !srvModel.archived" class="d-print-none">
|
||||
<template v-if="srvModel.formId && srvModel.formId != 'None' && !srvModel.formSubmitted">
|
||||
<a asp-action="ViewPaymentRequestForm" asp-route-payReqId="@Model.Id" class="btn btn-primary w-100 d-flex d-print-none align-items-center justify-content-center text-nowrap btn-lg" data-test="form-button">
|
||||
Pay Invoice
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="srvModel.isPending && !srvModel.archived">
|
||||
<template v-if="srvModel.allowCustomPaymentAmounts && !srvModel.anyPendingInvoice">
|
||||
<form v-on:submit="submitCustomAmountForm" class="d-print-none">
|
||||
<div class="row">
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
<input asp-for="ReceiptOptions.Enabled" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="ReceiptOptions.Enabled" class="form-check-label"></label>
|
||||
</div>
|
||||
<div class="form-check my-3">
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="ReceiptOptions.ShowPayments" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="ReceiptOptions.ShowPayments" class="form-check-label"></label>
|
||||
</div>
|
||||
|
|
|
@ -22,6 +22,7 @@ namespace BTCPayServer.Views.Stores
|
|||
PayoutProcessors,
|
||||
[Obsolete("Use StoreNavPages.Plugins instead")]
|
||||
Integrations,
|
||||
Emails
|
||||
Emails,
|
||||
Forms
|
||||
}
|
||||
}
|
||||
|
|
|
@ -434,9 +434,9 @@
|
|||
"checkoutType": {
|
||||
"$ref": "#/components/schemas/CheckoutType"
|
||||
},
|
||||
"checkoutFormId": {
|
||||
"formId": {
|
||||
"type": "string",
|
||||
"description": "Form ID to request customer data, in case the new checkout is used",
|
||||
"description": "Form ID to request customer data",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1088,11 +1088,6 @@
|
|||
"V2"
|
||||
]
|
||||
},
|
||||
"checkoutFormId": {
|
||||
"type": "string",
|
||||
"description": "Form ID to request customer data, in case the new checkout is used",
|
||||
"nullable": true
|
||||
},
|
||||
"defaultLanguage": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
|
|
|
@ -470,6 +470,16 @@
|
|||
"type": "boolean",
|
||||
"description": "Whether to allow users to create invoices that partially pay the payment request ",
|
||||
"nullable": true
|
||||
},
|
||||
"formId": {
|
||||
"type": "string",
|
||||
"description": "Form ID to request customer data",
|
||||
"nullable": true
|
||||
},
|
||||
"formResponse": {
|
||||
"type": "object",
|
||||
"description": "Form data response",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -344,11 +344,6 @@
|
|||
"checkoutType": {
|
||||
"$ref": "#/components/schemas/CheckoutType"
|
||||
},
|
||||
"checkoutFormId": {
|
||||
"type": "string",
|
||||
"description": "Form ID to request customer data, in case the new checkout is used",
|
||||
"nullable": true
|
||||
},
|
||||
"receipt": {
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/ReceiptOptions",
|
||||
|
|
|
@ -37,13 +37,13 @@ public class FakeCustodian : ICustodian
|
|||
var fieldset = new Fieldset();
|
||||
|
||||
// Maybe a decimal type field would be better?
|
||||
var fakeBTCBalance = new TextField("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 = new TextField("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 = new TextField("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 = new TextField("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";
|
||||
|
@ -51,7 +51,7 @@ public class FakeCustodian : ICustodian
|
|||
fieldset.Fields.Add(fakeLTCBalance);
|
||||
fieldset.Fields.Add(fakeEURBalance);
|
||||
fieldset.Fields.Add(fakeUSDBalance);
|
||||
form.Fieldsets.Add(fieldset);
|
||||
form.Fields.Add(fieldset);
|
||||
|
||||
return Task.FromResult(form);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue