mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Abstractions.Form;
|
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.
|
// The name of the HTML5 node. Should be used as the key for the posted data.
|
||||||
public string Name;
|
public string Name;
|
||||||
|
|
||||||
// The translated label of the 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 Label;
|
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.
|
// 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.
|
// 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 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
|
// The field is considered "valid" if there are no validation errors
|
||||||
public List<string> ValidationErrors = new List<string>();
|
public List<string> ValidationErrors = new List<string>();
|
||||||
|
|
||||||
public bool Required = false;
|
public virtual bool IsValid()
|
||||||
|
|
||||||
public 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;
|
namespace BTCPayServer.Abstractions.Form;
|
||||||
|
|
||||||
public class Fieldset
|
public class Fieldset : Field
|
||||||
{
|
{
|
||||||
|
public bool Hidden { get; set; }
|
||||||
|
public string Label { get; set; }
|
||||||
|
|
||||||
public Fieldset()
|
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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Abstractions.Form;
|
namespace BTCPayServer.Abstractions.Form;
|
||||||
|
|
||||||
public class 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...
|
// 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();
|
public List<AlertMessage> TopMessages { get; set; } = new();
|
||||||
|
|
||||||
// Groups of fields in the form
|
// 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?
|
// Are all the fields valid in the form?
|
||||||
public bool IsValid()
|
public bool IsValid()
|
||||||
{
|
{
|
||||||
foreach (var fieldset in Fieldsets)
|
return Fields.All(field => field.IsValid());
|
||||||
{
|
|
||||||
foreach (var field in fieldset.Fields)
|
|
||||||
{
|
|
||||||
if (!field.IsValid())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Field GetFieldByName(string name)
|
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)
|
||||||
{
|
{
|
||||||
foreach (var field in fieldset.Fields)
|
prefix ??= string.Empty;
|
||||||
|
foreach (var field in fields)
|
||||||
{
|
{
|
||||||
if (name.Equals(field.Name))
|
var currentPrefix = prefix;
|
||||||
|
if (!string.IsNullOrEmpty(field.Name))
|
||||||
|
{
|
||||||
|
|
||||||
|
currentPrefix = $"{prefix}{field.Name}";
|
||||||
|
if (currentPrefix.Equals(name, StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPrefix += "_";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subFieldResult = GetFieldByName(name, field.Fields, currentPrefix);
|
||||||
|
if (subFieldResult is not null)
|
||||||
|
{
|
||||||
|
return subFieldResult;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<string> GetAllNames()
|
public List<string> GetAllNames()
|
||||||
{
|
{
|
||||||
var names = new List<string>();
|
return GetAllNames(Fields);
|
||||||
foreach (var fieldset in Fieldsets)
|
}
|
||||||
|
|
||||||
|
private static List<string> GetAllNames(List<Field> fields)
|
||||||
{
|
{
|
||||||
foreach (var field in fieldset.Fields)
|
var names = new List<string>();
|
||||||
|
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
string prefix = string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(field.Name))
|
||||||
{
|
{
|
||||||
names.Add(field.Name);
|
names.Add(field.Name);
|
||||||
|
prefix = $"{field.Name}_";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.Fields.Any())
|
||||||
|
{
|
||||||
|
names.AddRange(GetAllNames(field.Fields).Select(s => $"{prefix}{s}" ));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return names;
|
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 string RedirectUrl { get; set; } = null;
|
||||||
public bool? RedirectAutomatically { get; set; } = null;
|
public bool? RedirectAutomatically { get; set; } = null;
|
||||||
public bool? RequiresRefundEmail { 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 string EmbeddedCSS { get; set; } = null;
|
||||||
public CheckoutType? CheckoutType { get; set; } = null;
|
public CheckoutType? CheckoutType { get; set; } = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,8 +85,6 @@ namespace BTCPayServer.Client.Models
|
||||||
public bool? RedirectAutomatically { get; set; }
|
public bool? RedirectAutomatically { get; set; }
|
||||||
public bool? RequiresRefundEmail { get; set; } = null;
|
public bool? RequiresRefundEmail { get; set; } = null;
|
||||||
public string DefaultLanguage { get; set; }
|
public string DefaultLanguage { get; set; }
|
||||||
[JsonProperty("checkoutFormId")]
|
|
||||||
public string CheckoutFormId { get; set; }
|
|
||||||
public CheckoutType? CheckoutType { get; set; }
|
public CheckoutType? CheckoutType { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,5 +24,9 @@ namespace BTCPayServer.Client.Models
|
||||||
|
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
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 DateTimeOffset CreatedTime { get; set; }
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public bool Archived { get; set; }
|
public bool Archived { get; set; }
|
||||||
|
|
||||||
public enum PaymentRequestStatus
|
public enum PaymentRequestStatus
|
||||||
{
|
{
|
||||||
Pending = 0,
|
Pending = 0,
|
||||||
|
|
|
@ -31,8 +31,6 @@ namespace BTCPayServer.Client.Models
|
||||||
public bool AnyoneCanCreateInvoice { get; set; }
|
public bool AnyoneCanCreateInvoice { get; set; }
|
||||||
public string DefaultCurrency { get; set; }
|
public string DefaultCurrency { get; set; }
|
||||||
public bool RequiresRefundEmail { get; set; }
|
public bool RequiresRefundEmail { get; set; }
|
||||||
|
|
||||||
public string CheckoutFormId { get; set; }
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public CheckoutType CheckoutType { get; set; }
|
public CheckoutType CheckoutType { get; set; }
|
||||||
public bool LightningAmountInSatoshi { 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,
|
RedirectAutomatically = true,
|
||||||
RequiresRefundEmail = true,
|
RequiresRefundEmail = true,
|
||||||
CheckoutFormId = GenericFormOption.Email.ToString()
|
|
||||||
},
|
},
|
||||||
AdditionalSearchTerms = new string[] { "Banana" }
|
AdditionalSearchTerms = new string[] { "Banana" }
|
||||||
});
|
});
|
||||||
Assert.True(newInvoice.Checkout.RedirectAutomatically);
|
Assert.True(newInvoice.Checkout.RedirectAutomatically);
|
||||||
Assert.True(newInvoice.Checkout.RequiresRefundEmail);
|
Assert.True(newInvoice.Checkout.RequiresRefundEmail);
|
||||||
Assert.Equal(GenericFormOption.Email.ToString(), newInvoice.Checkout.CheckoutFormId);
|
|
||||||
Assert.Equal(user.StoreId, newInvoice.StoreId);
|
Assert.Equal(user.StoreId, newInvoice.StoreId);
|
||||||
//list
|
//list
|
||||||
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
||||||
|
|
|
@ -64,6 +64,61 @@ namespace BTCPayServer.Tests
|
||||||
s.Driver.Quit();
|
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)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
public async Task CanUseCPFP()
|
public async Task CanUseCPFP()
|
||||||
{
|
{
|
||||||
|
|
|
@ -233,7 +233,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
EmbeddedCSS = request.EmbeddedCSS,
|
EmbeddedCSS = request.EmbeddedCSS,
|
||||||
RedirectAutomatically = request.RedirectAutomatically,
|
RedirectAutomatically = request.RedirectAutomatically,
|
||||||
RequiresRefundEmail = BoolToRequiresRefundEmail(request.RequiresRefundEmail) ?? RequiresRefundEmail.InheritFromStore,
|
RequiresRefundEmail = BoolToRequiresRefundEmail(request.RequiresRefundEmail) ?? RequiresRefundEmail.InheritFromStore,
|
||||||
CheckoutFormId = request.CheckoutFormId,
|
FormId = request.FormId,
|
||||||
CheckoutType = request.CheckoutType ?? CheckoutType.V1
|
CheckoutType = request.CheckoutType ?? CheckoutType.V1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -437,7 +437,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
DefaultLanguage = entity.DefaultLanguage,
|
DefaultLanguage = entity.DefaultLanguage,
|
||||||
RedirectAutomatically = entity.RedirectAutomatically,
|
RedirectAutomatically = entity.RedirectAutomatically,
|
||||||
RequiresRefundEmail = entity.RequiresRefundEmail,
|
RequiresRefundEmail = entity.RequiresRefundEmail,
|
||||||
CheckoutFormId = entity.CheckoutFormId,
|
|
||||||
CheckoutType = entity.CheckoutType,
|
CheckoutType = entity.CheckoutType,
|
||||||
RedirectURL = entity.RedirectURLTemplate
|
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)
|
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||||
CheckoutFormId = storeBlob.CheckoutFormId,
|
|
||||||
CheckoutType = storeBlob.CheckoutType,
|
CheckoutType = storeBlob.CheckoutType,
|
||||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||||
|
@ -167,7 +166,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
||||||
blob.DefaultCurrency = restModel.DefaultCurrency;
|
blob.DefaultCurrency = restModel.DefaultCurrency;
|
||||||
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
||||||
blob.CheckoutFormId = restModel.CheckoutFormId;
|
|
||||||
blob.CheckoutType = restModel.CheckoutType;
|
blob.CheckoutType = restModel.CheckoutType;
|
||||||
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
||||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||||
|
|
|
@ -179,6 +179,11 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
JToken? receiptData = null;
|
JToken? receiptData = null;
|
||||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
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)
|
var payments = i.GetPayments(true)
|
||||||
.Select(paymentEntity =>
|
.Select(paymentEntity =>
|
||||||
|
@ -229,7 +234,6 @@ namespace BTCPayServer.Controllers
|
||||||
? new Dictionary<string, object>()
|
? new Dictionary<string, object>()
|
||||||
: PosDataParser.ParsePosData(receiptData.ToString())
|
: PosDataParser.ParsePosData(receiptData.ToString())
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
|
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
|
||||||
{
|
{
|
||||||
|
@ -760,7 +764,6 @@ namespace BTCPayServer.Controllers
|
||||||
CustomLogoLink = storeBlob.CustomLogo,
|
CustomLogoLink = storeBlob.CustomLogo,
|
||||||
LogoFileId = storeBlob.LogoFileId,
|
LogoFileId = storeBlob.LogoFileId,
|
||||||
BrandColor = storeBlob.BrandColor,
|
BrandColor = storeBlob.BrandColor,
|
||||||
CheckoutFormId = invoice.CheckoutFormId ?? storeBlob.CheckoutFormId,
|
|
||||||
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
|
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
|
||||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||||
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
||||||
|
@ -1140,9 +1143,6 @@ namespace BTCPayServer.Controllers
|
||||||
RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||||
? storeBlob.RequiresRefundEmail
|
? storeBlob.RequiresRefundEmail
|
||||||
: model.RequiresRefundEmail == RequiresRefundEmail.On,
|
: model.RequiresRefundEmail == RequiresRefundEmail.On,
|
||||||
CheckoutFormId = model.CheckoutFormId == GenericFormOption.InheritFromStore.ToString()
|
|
||||||
? storeBlob.CheckoutFormId
|
|
||||||
: model.CheckoutFormId
|
|
||||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
|
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
|
||||||
|
|
|
@ -142,7 +142,6 @@ namespace BTCPayServer.Controllers
|
||||||
entity.RedirectAutomatically =
|
entity.RedirectAutomatically =
|
||||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||||
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
||||||
entity.CheckoutFormId = invoice.CheckoutFormId;
|
|
||||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||||
|
|
||||||
IPaymentFilter? excludeFilter = null;
|
IPaymentFilter? excludeFilter = null;
|
||||||
|
@ -227,7 +226,6 @@ namespace BTCPayServer.Controllers
|
||||||
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
||||||
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
|
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
|
||||||
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
||||||
entity.CheckoutFormId = invoice.Checkout.CheckoutFormId;
|
|
||||||
entity.CheckoutType = invoice.Checkout.CheckoutType;
|
entity.CheckoutType = invoice.Checkout.CheckoutType;
|
||||||
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
|
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
|
||||||
IPaymentFilter? excludeFilter = null;
|
IPaymentFilter? excludeFilter = null;
|
||||||
|
|
|
@ -9,6 +9,7 @@ using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Filters;
|
using BTCPayServer.Filters;
|
||||||
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||||
using BTCPayServer.PaymentRequest;
|
using BTCPayServer.PaymentRequest;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
@ -16,9 +17,11 @@ using BTCPayServer.Services.PaymentRequests;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
|
@ -145,6 +148,7 @@ namespace BTCPayServer.Controllers
|
||||||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||||
|
blob.FormId = viewModel.FormId;
|
||||||
|
|
||||||
data.SetBlob(blob);
|
data.SetBlob(blob);
|
||||||
var isNewPaymentRequest = string.IsNullOrEmpty(payReqId);
|
var isNewPaymentRequest = string.IsNullOrEmpty(payReqId);
|
||||||
|
@ -174,6 +178,51 @@ namespace BTCPayServer.Controllers
|
||||||
return View(result);
|
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")]
|
[HttpGet("{payReqId}/pay")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> PayPaymentRequest(string payReqId, bool redirectToInvoice = true,
|
public async Task<IActionResult> PayPaymentRequest(string payReqId, bool redirectToInvoice = true,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Data;
|
namespace BTCPayServer.Data;
|
||||||
|
@ -8,7 +9,7 @@ public static class CustodianAccountDataExtensions
|
||||||
{
|
{
|
||||||
var result = custodianAccountData.Blob == null
|
var result = custodianAccountData.Blob == null
|
||||||
? new JObject()
|
? new JObject()
|
||||||
: JObject.Parse(ZipUtils.Unzip(custodianAccountData.Blob));
|
: InvoiceRepository.FromBytes<JObject>(custodianAccountData.Blob);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +18,8 @@ public static class CustodianAccountDataExtensions
|
||||||
var original = custodianAccountData.GetBlob();
|
var original = custodianAccountData.GetBlob();
|
||||||
if (JToken.DeepEquals(original, blob))
|
if (JToken.DeepEquals(original, blob))
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@ namespace BTCPayServer.Data
|
||||||
|
|
||||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||||
public CheckoutType CheckoutType { get; set; }
|
public CheckoutType CheckoutType { get; set; }
|
||||||
public string CheckoutFormId { get; set; }
|
|
||||||
public bool RequiresRefundEmail { get; set; }
|
public bool RequiresRefundEmail { get; set; }
|
||||||
public bool LightningAmountInSatoshi { get; set; }
|
public bool LightningAmountInSatoshi { get; set; }
|
||||||
public bool LightningPrivateRouteHints { 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.Controllers.Greenfield;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Data.Payouts.LightningLike;
|
using BTCPayServer.Data.Payouts.LightningLike;
|
||||||
|
using BTCPayServer.Forms;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
@ -437,6 +438,7 @@ namespace BTCPayServer.Hosting
|
||||||
//also provide a factory that can impersonate user/store id
|
//also provide a factory that can impersonate user/store id
|
||||||
services.AddSingleton<IBTCPayServerClientFactory, BTCPayServerClientFactory>();
|
services.AddSingleton<IBTCPayServerClientFactory, BTCPayServerClientFactory>();
|
||||||
services.AddPayoutProcesors();
|
services.AddPayoutProcesors();
|
||||||
|
services.AddForms();
|
||||||
|
|
||||||
services.AddAPIKeyAuthentication();
|
services.AddAPIKeyAuthentication();
|
||||||
services.AddBtcPayServerAuthenticationSchemes();
|
services.AddBtcPayServerAuthenticationSchemes();
|
||||||
|
|
|
@ -82,9 +82,6 @@ namespace BTCPayServer.Models
|
||||||
[JsonProperty(PropertyName = "requiresRefundEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(PropertyName = "requiresRefundEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public bool? RequiresRefundEmail { get; set; }
|
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
|
//Bitpay compatibility: create invoice in btcpay uses this instead of supportedTransactionCurrencies
|
||||||
[JsonProperty(PropertyName = "paymentCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(PropertyName = "paymentCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public IEnumerable<string> PaymentCurrencies { get; set; }
|
public IEnumerable<string> PaymentCurrencies { get; set; }
|
||||||
|
|
|
@ -268,9 +268,6 @@ namespace BTCPayServer.Models
|
||||||
public Dictionary<string, InvoiceCryptoInfo.InvoicePaymentUrls> PaymentCodes { get; set; }
|
public Dictionary<string, InvoiceCryptoInfo.InvoicePaymentUrls> PaymentCodes { get; set; }
|
||||||
[JsonProperty("buyer")]
|
[JsonProperty("buyer")]
|
||||||
public JObject Buyer { get; set; }
|
public JObject Buyer { get; set; }
|
||||||
|
|
||||||
[JsonProperty("checkoutFormId")]
|
|
||||||
public string CheckoutFormId { get; set; }
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public CheckoutType? CheckoutType { get; set; }
|
public CheckoutType? CheckoutType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,9 +89,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Display(Name = "Request customer data on checkout")]
|
|
||||||
public string CheckoutFormId { get; set; }
|
|
||||||
|
|
||||||
public bool UseNewCheckout { get; set; }
|
public bool UseNewCheckout { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
public bool Activated { get; set; }
|
public bool Activated { get; set; }
|
||||||
public string InvoiceCurrency { get; set; }
|
public string InvoiceCurrency { get; set; }
|
||||||
public string ReceiptLink { get; set; }
|
public string ReceiptLink { get; set; }
|
||||||
public string CheckoutFormId { get; set; }
|
|
||||||
public bool AltcoinsBuild { get; set; }
|
public bool AltcoinsBuild { get; set; }
|
||||||
public CheckoutType CheckoutType { get; set; }
|
public CheckoutType CheckoutType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Validation;
|
using BTCPayServer.Validation;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.PaymentRequestViewModels
|
namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||||
|
@ -35,6 +36,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||||
StoreId = data.StoreDataId;
|
StoreId = data.StoreDataId;
|
||||||
Archived = data.Archived;
|
Archived = data.Archived;
|
||||||
var blob = data.GetBlob();
|
var blob = data.GetBlob();
|
||||||
|
FormId = blob.FormId;
|
||||||
Title = blob.Title;
|
Title = blob.Title;
|
||||||
Amount = blob.Amount;
|
Amount = blob.Amount;
|
||||||
Currency = blob.Currency;
|
Currency = blob.Currency;
|
||||||
|
@ -44,8 +46,14 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||||
CustomCSSLink = blob.CustomCSSLink;
|
CustomCSSLink = blob.CustomCSSLink;
|
||||||
EmbeddedCSS = blob.EmbeddedCSS;
|
EmbeddedCSS = blob.EmbeddedCSS;
|
||||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
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 bool Archived { get; set; }
|
||||||
|
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
|
@ -77,6 +85,8 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||||
public string EmbeddedCSS { get; set; }
|
public string EmbeddedCSS { get; set; }
|
||||||
[Display(Name = "Allow payee to create invoices in their own denomination")]
|
[Display(Name = "Allow payee to create invoices in their own denomination")]
|
||||||
public bool AllowCustomPaymentAmounts { get; set; }
|
public bool AllowCustomPaymentAmounts { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, object> FormResponse { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ViewPaymentRequestViewModel
|
public class ViewPaymentRequestViewModel
|
||||||
|
@ -165,6 +175,8 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||||
public bool PendingInvoiceHasPayments { get; set; }
|
public bool PendingInvoiceHasPayments { get; set; }
|
||||||
public string HubPath { get; set; }
|
public string HubPath { get; set; }
|
||||||
public bool Archived { get; set; }
|
public bool Archived { get; set; }
|
||||||
|
public string FormId { get; set; }
|
||||||
|
public bool FormSubmitted { get; set; }
|
||||||
|
|
||||||
public class PaymentRequestInvoice
|
public class PaymentRequestInvoice
|
||||||
{
|
{
|
||||||
|
|
|
@ -97,6 +97,8 @@ namespace BTCPayServer.PaymentRequest
|
||||||
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
|
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
|
||||||
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
||||||
LastUpdated = DateTime.UtcNow,
|
LastUpdated = DateTime.UtcNow,
|
||||||
|
FormId = blob.FormId,
|
||||||
|
FormSubmitted = blob.FormResponse is not null,
|
||||||
AnyPendingInvoice = pendingInvoice != null,
|
AnyPendingInvoice = pendingInvoice != null,
|
||||||
PendingInvoiceHasPayments = pendingInvoice != null &&
|
PendingInvoiceHasPayments = pendingInvoice != null &&
|
||||||
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,
|
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,
|
||||||
|
|
|
@ -18,6 +18,7 @@ using BTCPayServer.ModelBinders;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -25,6 +26,7 @@ using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using NicolasDorier.RateLimits;
|
using NicolasDorier.RateLimits;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
|
@ -116,6 +118,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
string notificationUrl,
|
string notificationUrl,
|
||||||
string redirectUrl,
|
string redirectUrl,
|
||||||
string choiceKey,
|
string choiceKey,
|
||||||
|
string formId = null,
|
||||||
|
string formData = null,
|
||||||
string posData = null,
|
string posData = null,
|
||||||
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
|
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
|
@ -214,7 +218,42 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = await _appService.GetStore(app);
|
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
|
try
|
||||||
{
|
{
|
||||||
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest
|
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest
|
||||||
|
@ -235,7 +274,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
||||||
RedirectAutomatically = settings.RedirectAutomatically,
|
RedirectAutomatically = settings.RedirectAutomatically,
|
||||||
SupportedTransactionCurrencies = paymentMethods,
|
SupportedTransactionCurrencies = paymentMethods,
|
||||||
CheckoutFormId = store.GetStoreBlob().CheckoutFormId,
|
|
||||||
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||||
? store.GetStoreBlob().RequiresRefundEmail
|
? store.GetStoreBlob().RequiresRefundEmail
|
||||||
: requiresRefundEmail == RequiresRefundEmail.On,
|
: requiresRefundEmail == RequiresRefundEmail.On,
|
||||||
|
@ -244,6 +282,13 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
cancellationToken, (entity) =>
|
cancellationToken, (entity) =>
|
||||||
{
|
{
|
||||||
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
|
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 });
|
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)}",
|
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
|
||||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
|
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
|
||||||
RequiresRefundEmail = settings.RequiresRefundEmail,
|
RequiresRefundEmail = settings.RequiresRefundEmail,
|
||||||
CheckoutFormId = settings.CheckoutFormId,
|
FormId = settings.FormId
|
||||||
UseNewCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V2
|
|
||||||
};
|
};
|
||||||
if (HttpContext?.Request != null)
|
if (HttpContext?.Request != null)
|
||||||
{
|
{
|
||||||
|
@ -390,13 +434,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||||
RequiresRefundEmail = vm.RequiresRefundEmail
|
RequiresRefundEmail = vm.RequiresRefundEmail
|
||||||
};
|
};
|
||||||
|
|
||||||
if (storeBlob.CheckoutType == Client.Models.CheckoutType.V2)
|
settings.FormId = vm.FormId;
|
||||||
{
|
|
||||||
settings.CheckoutFormId = vm.CheckoutFormId == GenericFormOption.InheritFromStore.ToString()
|
|
||||||
? storeBlob.CheckoutFormId
|
|
||||||
: vm.CheckoutFormId;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Name = vm.AppName;
|
app.Name = vm.AppName;
|
||||||
app.SetSettings(settings);
|
app.SetSettings(settings);
|
||||||
await _appService.UpdateOrCreateApp(app);
|
await _appService.UpdateOrCreateApp(app);
|
||||||
|
|
|
@ -103,8 +103,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||||
public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore;
|
public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore;
|
||||||
|
|
||||||
[Display(Name = "Request customer data on checkout")]
|
[Display(Name = "Request customer data on checkout")]
|
||||||
public string CheckoutFormId { get; set; } = GenericFormOption.InheritFromStore.ToString();
|
public string FormId { get; set; } = null;
|
||||||
|
|
||||||
public bool UseNewCheckout { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
public bool EnableTips { get; set; }
|
public bool EnableTips { get; set; }
|
||||||
public RequiresRefundEmail RequiresRefundEmail { 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 const string BUTTON_TEXT_DEF = "Buy for {0}";
|
||||||
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
||||||
|
|
|
@ -446,8 +446,6 @@ namespace BTCPayServer.Services.Invoices
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; }
|
public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; }
|
||||||
|
|
||||||
[JsonProperty("checkoutFormId")]
|
|
||||||
public string CheckoutFormId { get; set; }
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public CheckoutType? CheckoutType { get; set; }
|
public CheckoutType? CheckoutType { get; set; }
|
||||||
|
|
||||||
|
@ -578,7 +576,6 @@ namespace BTCPayServer.Services.Invoices
|
||||||
dto.TaxIncluded = Metadata.TaxIncluded ?? 0m;
|
dto.TaxIncluded = Metadata.TaxIncluded ?? 0m;
|
||||||
dto.Price = Price;
|
dto.Price = Price;
|
||||||
dto.Currency = Currency;
|
dto.Currency = Currency;
|
||||||
dto.CheckoutFormId = CheckoutFormId;
|
|
||||||
dto.CheckoutType = CheckoutType;
|
dto.CheckoutType = CheckoutType;
|
||||||
dto.Buyer = new JObject();
|
dto.Buyer = new JObject();
|
||||||
dto.Buyer.Add(new JProperty("name", Metadata.BuyerName));
|
dto.Buyer.Add(new JProperty("name", Metadata.BuyerName));
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Services.PaymentRequests
|
||||||
|
|
||||||
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
|
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
|
||||||
{
|
{
|
||||||
using var context = _ContextFactory.CreateContext();
|
await using var context = _ContextFactory.CreateContext();
|
||||||
if (string.IsNullOrEmpty(entity.Id))
|
if (string.IsNullOrEmpty(entity.Id))
|
||||||
{
|
{
|
||||||
entity.Id = Guid.NewGuid().ToString();
|
entity.Id = Guid.NewGuid().ToString();
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using BTCPayServer.Data;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Stores;
|
namespace BTCPayServer.Services.Stores;
|
||||||
|
|
||||||
public enum GenericFormOption
|
public enum GenericFormOption
|
||||||
{
|
{
|
||||||
[Display(Name = "Inherit from store settings")]
|
|
||||||
InheritFromStore,
|
|
||||||
|
|
||||||
[Display(Name = "Do not request any information")]
|
[Display(Name = "Do not request any information")]
|
||||||
None,
|
None,
|
||||||
|
|
||||||
|
@ -24,24 +19,14 @@ public enum GenericFormOption
|
||||||
|
|
||||||
public static class CheckoutFormSelectList
|
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>();
|
var choices = new List<SelectListItem>
|
||||||
|
|
||||||
if (isStoreEntity)
|
|
||||||
{
|
{
|
||||||
var blob = store.GetStoreBlob();
|
GenericOptionItem(GenericFormOption.None),
|
||||||
var inherit = GenericOptionItem(GenericFormOption.InheritFromStore);
|
GenericOptionItem(GenericFormOption.Email),
|
||||||
inherit.Text += Enum.TryParse<GenericFormOption>(blob.CheckoutFormId, out var item)
|
GenericOptionItem(GenericFormOption.Address)
|
||||||
? $" ({DisplayName(item)})"
|
};
|
||||||
: $" ({blob.CheckoutFormId})";
|
|
||||||
|
|
||||||
choices.Add(inherit);
|
|
||||||
}
|
|
||||||
|
|
||||||
choices.Add(GenericOptionItem(GenericFormOption.None));
|
|
||||||
choices.Add(GenericOptionItem(GenericFormOption.Email));
|
|
||||||
choices.Add(GenericOptionItem(GenericFormOption.Address));
|
|
||||||
|
|
||||||
var chosen = choices.FirstOrDefault(t => t.Value == selectedFormId);
|
var chosen = choices.FirstOrDefault(t => t.Value == selectedFormId);
|
||||||
return new SelectList(choices, nameof(SelectListItem.Value), nameof(SelectListItem.Text), chosen?.Value);
|
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);
|
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
|
||||||
|
|
||||||
var store = ViewContext.HttpContext.GetStoreData();
|
var checkoutFormOptions = CheckoutFormSelectList.WithSelected(Model.FormId);
|
||||||
var checkoutFormOptions = CheckoutFormSelectList.ForStore(store, Model.CheckoutFormId, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
@ -84,18 +83,14 @@
|
||||||
<span asp-validation-for="ButtonText" class="text-danger"></span>
|
<span asp-validation-for="ButtonText" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@if (Model.UseNewCheckout)
|
<label asp-for="FormId" class="form-label"></label>
|
||||||
{
|
<select asp-for="FormId" class="form-select w-auto" asp-items="@checkoutFormOptions"></select>
|
||||||
<label asp-for="CheckoutFormId" class="form-label"></label>
|
<span asp-validation-for="FormId" class="text-danger"></span>
|
||||||
<select asp-for="CheckoutFormId" class="form-select w-auto" asp-items="@checkoutFormOptions"></select>
|
</div>
|
||||||
<span asp-validation-for="CheckoutFormId" class="text-danger"></span>
|
<div class="form-group">
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
||||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<section id="discounts" class="p-0">
|
<section id="discounts" class="p-0">
|
||||||
<h3 class="mt-5 mb-4">Discounts</h3>
|
<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
|
@model BTCPayServer.Abstractions.Form.Form
|
||||||
|
@inject FormComponentProvider FormComponentProvider
|
||||||
|
|
||||||
@foreach (var fieldset in Model.Fieldsets)
|
@foreach (var field in Model.Fields)
|
||||||
{
|
{
|
||||||
<fieldset>
|
var partial = FormComponentProvider.CanHandle(field);
|
||||||
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend>
|
if (string.IsNullOrEmpty(partial))
|
||||||
@foreach (var field in fieldset.Fields)
|
|
||||||
{
|
{
|
||||||
@if ("text".Equals(field.Type))
|
continue;
|
||||||
{
|
|
||||||
<div class="form-group">
|
|
||||||
@if (field.Required)
|
|
||||||
{
|
|
||||||
<label class="form-label" for="@field.Name" data-required>
|
|
||||||
@field.Label
|
|
||||||
</label>
|
|
||||||
}
|
}
|
||||||
else
|
<partial name="@partial" for="@field"></partial>
|
||||||
{
|
|
||||||
<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>
|
|
||||||
}
|
}
|
||||||
|
|
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
|
@model BTCPayServer.Models.InvoicingModels.CreateInvoiceModel
|
||||||
@using BTCPayServer.Services.Apps
|
@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");
|
ViewData.SetActivePage(InvoiceNavPages.Create, "Create Invoice");
|
||||||
|
|
||||||
var store = ViewContext.HttpContext.GetStoreData();
|
|
||||||
var checkoutFormOptions = CheckoutFormSelectList.ForStore(store, Model.CheckoutFormId, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@section PageFootContent {
|
@section PageFootContent {
|
||||||
|
@ -90,22 +89,13 @@
|
||||||
<h4 class="mt-5 mb-4">Customer Information</h4>
|
<h4 class="mt-5 mb-4">Customer Information</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="BuyerEmail" class="form-label"></label>
|
<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>
|
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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>
|
<label asp-for="RequiresRefundEmail" class="form-label"></label>
|
||||||
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
<select asp-for="RequiresRefundEmail" asp-items="@Html.GetEnumSelectList<RequiresRefundEmail>()" class="form-select w-auto"></select>
|
||||||
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
<span asp-validation-for="RequiresRefundEmail" class="text-danger"></span>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="mt-5 mb-2">Additional Options</h4>
|
<h4 class="mt-5 mb-2">Additional Options</h4>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
@model (Dictionary<string, object> Items, int Level)
|
@model (Dictionary<string, object> Items, int Level)
|
||||||
|
|
||||||
@functions{
|
@functions {
|
||||||
public bool IsValidURL(string source)
|
private bool IsValidURL(string source)
|
||||||
{
|
{
|
||||||
Uri uriResult;
|
return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) &&
|
||||||
return Uri.TryCreate(source, UriKind.Absolute, out uriResult) &&
|
|
||||||
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
@using BTCPayServer.Services.PaymentRequests
|
@using BTCPayServer.Services.PaymentRequests
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
|
@using BTCPayServer.Services.Stores
|
||||||
|
@using BTCPayServer.TagHelpers
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
@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);
|
ViewData.SetActivePage(PaymentRequestsNavPages.Create, $"{(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit")} Payment Request", Model.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +83,18 @@
|
||||||
Receive updates for this payment request.
|
Receive updates for this payment request.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -146,4 +163,3 @@
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,12 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</noscript>
|
</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">
|
<template v-if="srvModel.allowCustomPaymentAmounts && !srvModel.anyPendingInvoice">
|
||||||
<form v-on:submit="submitCustomAmountForm" class="d-print-none">
|
<form v-on:submit="submitCustomAmountForm" class="d-print-none">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -22,6 +22,7 @@ namespace BTCPayServer.Views.Stores
|
||||||
PayoutProcessors,
|
PayoutProcessors,
|
||||||
[Obsolete("Use StoreNavPages.Plugins instead")]
|
[Obsolete("Use StoreNavPages.Plugins instead")]
|
||||||
Integrations,
|
Integrations,
|
||||||
Emails
|
Emails,
|
||||||
|
Forms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -434,9 +434,9 @@
|
||||||
"checkoutType": {
|
"checkoutType": {
|
||||||
"$ref": "#/components/schemas/CheckoutType"
|
"$ref": "#/components/schemas/CheckoutType"
|
||||||
},
|
},
|
||||||
"checkoutFormId": {
|
"formId": {
|
||||||
"type": "string",
|
"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
|
"nullable": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1088,11 +1088,6 @@
|
||||||
"V2"
|
"V2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"checkoutFormId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Form ID to request customer data, in case the new checkout is used",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"defaultLanguage": {
|
"defaultLanguage": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
|
|
|
@ -470,6 +470,16 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether to allow users to create invoices that partially pay the payment request ",
|
"description": "Whether to allow users to create invoices that partially pay the payment request ",
|
||||||
"nullable": true
|
"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": {
|
"checkoutType": {
|
||||||
"$ref": "#/components/schemas/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": {
|
"receipt": {
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"$ref": "#/components/schemas/ReceiptOptions",
|
"$ref": "#/components/schemas/ReceiptOptions",
|
||||||
|
|
|
@ -37,13 +37,13 @@ public class FakeCustodian : ICustodian
|
||||||
var fieldset = new Fieldset();
|
var fieldset = new Fieldset();
|
||||||
|
|
||||||
// Maybe a decimal type field would be better?
|
// 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.");
|
"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.");
|
"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.");
|
"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.");
|
"Enter the amount of USD you want to have.");
|
||||||
|
|
||||||
fieldset.Label = "Your fake balances";
|
fieldset.Label = "Your fake balances";
|
||||||
|
@ -51,7 +51,7 @@ public class FakeCustodian : ICustodian
|
||||||
fieldset.Fields.Add(fakeLTCBalance);
|
fieldset.Fields.Add(fakeLTCBalance);
|
||||||
fieldset.Fields.Add(fakeEURBalance);
|
fieldset.Fields.Add(fakeEURBalance);
|
||||||
fieldset.Fields.Add(fakeUSDBalance);
|
fieldset.Fields.Add(fakeUSDBalance);
|
||||||
form.Fieldsets.Add(fieldset);
|
form.Fields.Add(fieldset);
|
||||||
|
|
||||||
return Task.FromResult(form);
|
return Task.FromResult(form);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue