Generic Forms (#4561)

* Custom Forms

* Update BTCPayServer.Data/Migrations/20230125085242_AddForms.cs

* Cleanups

* Explain public form

* Add store branding

* Add form name to POS form

* add tests

* fix migration

* Minor cleanups

* Code improvements

* Add form validation

Closes #4317.

* Adapt form validation for Bootstrap 5

* update logic for forms

* pr changes

* Minor code cleanup

* Remove unused parameters

* Refactor Form data handling to avoid O(n3) issues

* Rename Hidden to Constant

* Pre-populate FormView from the query string params

* Fix test

---------

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri
2023-02-20 11:35:54 +01:00
committed by GitHub
parent 60f84d5e30
commit bbbaacc350
30 changed files with 1174 additions and 354 deletions

View File

@@ -1,26 +1,48 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Data.Data;
using BTCPayServer.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Forms;
public class FormDataService
{
public const string InvoiceParameterPrefix = "invoice_";
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly FormComponentProviders _formProviders;
public FormDataService(
ApplicationDbContextFactory applicationDbContextFactory,
FormComponentProviders formProviders)
{
_applicationDbContextFactory = applicationDbContextFactory;
_formProviders = formProviders;
}
public static readonly Form StaticFormEmail = new()
{
Fields = new List<Field>() { Field.Create("Enter your email", "buyerEmail", null, true, null, "email") }
Fields = new List<Field> { Field.Create("Enter your email", "buyerEmail", null, true, null, "email") }
};
public static readonly Form StaticFormAddress = new()
{
Fields = new List<Field>()
Fields = new List<Field>
{
Field.Create("Enter your email", "buyerEmail", null, true, null, "email"),
Field.Create("Name", "buyerName", null, true, null),
@@ -32,4 +54,109 @@ public class FormDataService
Field.Create("Country", "buyerCountry", null, true, null)
}
};
private static readonly Dictionary<string, (string selectText, string name, Form form)> _hardcodedOptions = new()
{
{"", ("Do not request any information", null, null)!},
{"Email", ("Request email address only", "Provide your email address", StaticFormEmail )},
{"Address", ("Request shipping address", "Provide your address", StaticFormAddress)},
};
public async Task<SelectList> GetSelect(string storeId ,string selectedFormId)
{
var forms = await GetForms(storeId);
return new SelectList(_hardcodedOptions.Select(pair => new SelectListItem(pair.Value.selectText, pair.Key, selectedFormId == pair.Key)).Concat(forms.Select(data => new SelectListItem(data.Name, data.Id, data.Id == selectedFormId))),
nameof(SelectListItem.Value), nameof(SelectListItem.Text));
}
public async Task<List<FormData>> GetForms(string storeId)
{
ArgumentNullException.ThrowIfNull(storeId);
await using var context = _applicationDbContextFactory.CreateContext();
return await context.Forms.Where(data => data.StoreId == storeId).ToListAsync();
}
public async Task<FormData?> GetForm(string storeId, string? id)
{
if (id is null)
{
return null;
}
await using var context = _applicationDbContextFactory.CreateContext();
return await context.Forms.Where(data => data.Id == id && data.StoreId == storeId).FirstOrDefaultAsync();
}
public async Task<FormData?> GetForm(string? id)
{
if (id is null)
{
return null;
}
if (_hardcodedOptions.TryGetValue(id, out var hardcodedForm))
{
return new FormData
{
Config = hardcodedForm.form.ToString(),
Id = id,
Name = hardcodedForm.name,
Public = false
};
}
await using var context = _applicationDbContextFactory.CreateContext();
return await context.Forms.Where(data => data.Id == id).FirstOrDefaultAsync();
}
public async Task RemoveForm(string id, string storeId)
{
await using var context = _applicationDbContextFactory.CreateContext();
var item = await context.Forms.SingleOrDefaultAsync(data => data.StoreId == storeId && id == data.Id);
if (item is not null)
context.Remove(item);
await context.SaveChangesAsync();
}
public async Task AddOrUpdateForm(FormData data)
{
await using var context = _applicationDbContextFactory.CreateContext();
context.Update(data);
await context.SaveChangesAsync();
}
public bool Validate(Form form, ModelStateDictionary modelState)
{
return _formProviders.Validate(form, modelState);
}
public bool IsFormSchemaValid(string schema, [MaybeNullWhen(false)] out Form form, [MaybeNullWhen(false)] out string error)
{
error = null;
form = null;
try
{
form = Form.Parse(schema);
if (!form.ValidateFieldNames(out var errors))
{
error = errors.First();
}
}
catch (Exception ex)
{
error = $"Form config was invalid: {ex.Message}";
}
return error is null && form is not null;
}
public CreateInvoiceRequest GenerateInvoiceParametersFromForm(Form form)
{
var amt = form.GetFieldByFullName($"{InvoiceParameterPrefix}amount")?.Value;
return new CreateInvoiceRequest
{
Currency = form.GetFieldByFullName($"{InvoiceParameterPrefix}currency")?.Value,
Amount = string.IsNullOrEmpty(amt) ? null : decimal.Parse(amt, CultureInfo.InvariantCulture),
Metadata = form.GetValues(),
};
}
}