Refactor: Move crowdfund files into Plugins/Crowdfund (#7112)
What changed, and why it matters
This commit is a code reorganization: it moves the Crowdfund feature's files into a dedicated plugin folder and updates the controller to use newer ASP.NET Core patterns (primary constructor, area routing). It also makes a small change so that looking up a store for an app throws an exception if the store is missing, instead of returning null. There is no clear security fix or vulnerability being patched here.
No immediate security action required. Treat as a normal refactor. Review that the area-routing changes do not break existing authorization attributes or antiforgery behavior, and verify the new GetStore exception path is handled appropriately by callers.
Security signals we found
Removal of unused HTML-sanitizer import (Ganss.Xss) — not a vulnerability fix, just cleanup
GetStore now throws when an app's store is missing, which may prevent null-dereference issues downstream
Routing changed to use MVC Areas; no authorization or input-validation changes observed
Evidence from the diff
The commit refactors BTCPay Server’s Crowdfund app into a plugin structure under BTCPayServer/Plugins/Crowdfund. The controller is converted to a primary constructor, view paths are updated from ‘Crowdfund/…’ to ‘Plugins/Crowdfund/…’, and routing now uses an MVC Area (‘Crowdfund’). The unused Ganss.Xss import is removed. In AppService.cs, GetStore changed from returning Task
Changed components
BTCPayServer.Plugins.CrowdfundBTCPayServer.Services.Apps.AppServiceBTCPayServer.Services.Stores.StoreRepositoryInspect captured patch +984 / −1008
diff --git a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
index 162ddd6..2124e91 100644
--- a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
+++ b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
@@ -23,7 +23,6 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
-using Ganss.Xss;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
@@ -40,49 +39,24 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
{
[AutoValidateAntiforgeryToken]
[Route("apps")]
- public class UICrowdfundController : Controller
+ [Area(CrowdfundPlugin.Area)]
+ public class UICrowdfundController(
+ AppService appService,
+ CurrencyNameTable currencies,
+ EventAggregator eventAggregator,
+ UriResolver uriResolver,
+ StoreRepository storeRepository,
+ IFileService fileService,
+ UIInvoiceController invoiceController,
+ UserManager<ApplicationUser> userManager,
+ FormDataService formDataService,
+ IStringLocalizer stringLocalizer,
+ CrowdfundAppType appType,
+ Safe safe)
+ : Controller
{
- public UICrowdfundController(
- AppService appService,
- CurrencyNameTable currencies,
- EventAggregator eventAggregator,
- UriResolver uriResolver,
- StoreRepository storeRepository,
- IFileService fileService,
- UIInvoiceController invoiceController,
- UserManager<ApplicationUser> userManager,
- FormDataService formDataService,
- IStringLocalizer stringLocalizer,
- CrowdfundAppType app,
- Safe safe)
- {
- _currencies = currencies;
- _appService = appService;
- _userManager = userManager;
- _app = app;
- _fileService = fileService;
- _storeRepository = storeRepository;
- _eventAggregator = eventAggregator;
- _uriResolver = uriResolver;
- _invoiceController = invoiceController;
- FormDataService = formDataService;
- StringLocalizer = stringLocalizer;
- _safe = safe;
- }
-
- private readonly EventAggregator _eventAggregator;
- private readonly IFileService _fileService;
- private readonly UriResolver _uriResolver;
- private readonly CurrencyNameTable _currencies;
- private readonly StoreRepository _storeRepository;
- private readonly AppService _appService;
- private readonly UIInvoiceController _invoiceController;
- private readonly UserManager<ApplicationUser> _userManager;
- private readonly CrowdfundAppType _app;
- private readonly Safe _safe;
-
- public FormDataService FormDataService { get; }
- public IStringLocalizer StringLocalizer { get; }
+ public FormDataService FormDataService { get; } = formDataService;
+ public IStringLocalizer StringLocalizer { get; } = stringLocalizer;
[HttpGet("/")]
[HttpGet("/apps/{appId}/crowdfund")]
@@ -90,13 +64,13 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
[DomainMappingConstraint(CrowdfundAppType.AppType)]
public async Task<IActionResult> ViewCrowdfund(string appId)
{
- var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true);
+ var app = await appService.GetApp(appId, CrowdfundAppType.AppType, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
- var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundAppType.AppType) != null;
+ var isAdmin = await appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundAppType.AppType) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
if (!hasEnoughSettingsToLoad)
@@ -109,11 +83,11 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
var appInfo = await GetAppInfo(appId);
if (settings.Enabled)
- return View("Crowdfund/Public/ViewCrowdfund", appInfo);
+ return View("Public/ViewCrowdfund", appInfo);
if (!isAdmin)
return NotFound();
- return View("Crowdfund/Public/ViewCrowdfund", appInfo);
+ return View("Public/ViewCrowdfund", appInfo);
}
[HttpPost("/")]
@@ -125,13 +99,13 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, string formResponse = null, CancellationToken cancellationToken = default)
{
- var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true);
+ var app = await appService.GetApp(appId, CrowdfundAppType.AppType, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
- var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundAppType.AppType) != null;
+ var isAdmin = await appService.GetAppDataIfOwner(GetUserId(), appId, CrowdfundAppType.AppType) != null;
if (!settings.Enabled && !isAdmin)
{
@@ -150,7 +124,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
}
JObject formResponseJObject = null;
- var store = await _appService.GetStore(app);
+ var store = await appService.GetStore(app);
decimal? price = request.Amount;
var title = settings.Title;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
@@ -187,7 +161,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
price = request.Amount;
}
-
+
if (settings.FormId is not null)
{
var formData = await FormDataService.GetForm(settings.FormId);
@@ -201,8 +175,8 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
// someone tried to bypass validation
return RedirectToAction(nameof(ViewCrowdfund), new { appId });
}
-
-
+
+
var amtField = form.GetFieldByFullName($"{FormDataService.InvoiceParameterPrefix}amount");
if (amtField is null)
{
@@ -219,7 +193,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
amtField.Value = price?.ToString();
}
formResponseJObject = FormDataService.GetValues(form);
-
+
var invoiceRequest = FormDataService.GenerateInvoiceParametersFromForm(form);
if (invoiceRequest.Amount is not null)
{
@@ -236,9 +210,9 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
try
{
- var appPath = await _appService.ViewLink(app);
+ var appPath = await appService.ViewLink(app);
var appUrl = HttpContext.Request.GetAbsoluteUri(appPath);
- var invoice = await _invoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest()
+ var invoice = await invoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest()
{
Amount = price,
Currency = settings.TargetCurrency,
@@ -302,7 +276,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> CrowdfundForm(string appId, decimal? amount=0, string choiceKey="")
{
- var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
+ var app = await appService.GetApp(appId, CrowdfundAppType.AppType);
if (app == null)
return NotFound();
@@ -316,14 +290,14 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
var prefix = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)) + "_";
var formParameters = new MultiValueDictionary<string, string>();
var controller = nameof(UICrowdfundController).TrimEnd("Controller", StringComparison.InvariantCulture);
- var store = await _appService.GetStore(app);
+ var store = await appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
var form = Form.Parse(formData.Config);
form.ApplyValuesFromForm(Request.Query);
var vm = new FormViewModel
{
StoreName = store.StoreName,
- StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob),
+ StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, uriResolver, storeBlob),
FormName = formData.Name,
Form = form,
AspController = controller,
@@ -341,7 +315,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> CrowdfundFormSubmit(string appId, decimal amount, string choiceKey, FormViewModel viewModel)
{
- var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
+ var app = await appService.GetApp(appId, CrowdfundAppType.AppType);
if (app == null)
return NotFound();
@@ -412,7 +386,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
HtmlMetaTags= settings.HtmlMetaTags,
Language = settings.HtmlLang,
TargetCurrency = settings.TargetCurrency,
- MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
+ MainImageUrl = settings.MainImageUrl == null ? null : await uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
Description = settings.Description,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
@@ -436,7 +410,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors),
FormId = settings.FormId
};
- return View("Crowdfund/UpdateCrowdfund", vm);
+ return View("UpdateCrowdfund", vm);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
@@ -454,7 +428,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
vm.AppId = app.Id;
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
- if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
+ if (currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
@@ -518,7 +492,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
UploadImageResultModel imageUpload = null;
if (vm.MainImageFile != null)
{
- imageUpload = await _fileService.UploadImage(vm.MainImageFile, userId);
+ imageUpload = await fileService.UploadImage(vm.MainImageFile, userId);
if (!imageUpload.Success)
{
ModelState.AddModelError(nameof(vm.MainImageFile), imageUpload.Response);
@@ -527,7 +501,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
if (!ModelState.IsValid)
{
- return View("Crowdfund/UpdateCrowdfund", vm);
+ return View("UpdateCrowdfund", vm);
}
app.Name = vm.AppName;
@@ -541,7 +515,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate?.ToUniversalTime(),
TargetCurrency = vm.TargetCurrency,
- HtmlMetaTags= _safe.RawMeta(vm.HtmlMetaTags, out wasHtmlModified),
+ HtmlMetaTags= safe.RawMeta(vm.HtmlMetaTags, out wasHtmlModified),
HtmlLang = vm.Language,
Description = vm.Description,
EndDate = vm.EndDate?.ToUniversalTime(),
@@ -578,9 +552,9 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
- await _appService.UpdateOrCreateApp(app);
+ await appService.UpdateOrCreateApp(app);
- _eventAggregator.Publish(new UIAppsController.AppUpdated
+ eventAggregator.Publish(new UIAppsController.AppUpdated
{
AppId = appId,
StoreId = app.StoreDataId,
@@ -601,7 +575,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
{
if (string.IsNullOrWhiteSpace(currency))
{
- var store = await _storeRepository.FindStore(storeId);
+ var store = await storeRepository.FindStore(storeId);
if (store == null)
{
throw new Exception($"Could not find store with id {storeId}");
@@ -614,19 +588,22 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
private AppData GetCurrentApp() => HttpContext.GetAppData();
- private string GetUserId() => _userManager.GetUserId(User);
+ private string GetUserId() => userManager.GetUserId(User);
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
{
- var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true);
- if (app is null)
+ var app1 = await appService.GetApp(appId, CrowdfundAppType.AppType, true);
+ if (app1 is null)
{
return null;
}
- var info = (ViewCrowdfundViewModel)await _app.GetInfo(app);
- info.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, app.StoreData.GetStoreBlob());
- info.HubPath = AppHub.GetHubPath(Request);
- info.SimpleDisplay = Request.Query.ContainsKey("simple");
+ var info = (ViewCrowdfundViewModel)await appType.GetInfo(app1);
+ if (info is not null)
+ {
+ info.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, uriResolver, app1.StoreData.GetStoreBlob());
+ info.HubPath = AppHub.GetHubPath(Request);
+ info.SimpleDisplay = Request.Query.ContainsKey("simple");
+ }
return info;
}
}
diff --git a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs
index 81f2225..778d952 100644
--- a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs
+++ b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs
@@ -30,13 +30,14 @@ namespace BTCPayServer.Plugins.Crowdfund
{
public class CrowdfundPlugin : BaseBTCPayServerPlugin
{
+ public const string Area = "Crowdfund";
public override string Identifier => "BTCPayServer.Plugins.Crowdfund";
public override string Name => "Crowdfund";
public override string Description => "Create a self-hosted funding campaign, similar to Kickstarter or Indiegogo. Funds go directly to the creator’s wallet without any fees.";
public override void Execute(IServiceCollection services)
{
- services.AddUIExtension("header-nav", "Crowdfund/NavExtension");
+ services.AddUIExtension("header-nav", "/Plugins/Crowdfund/Views/NavExtension.cshtml");
services.AddSingleton<CrowdfundAppType>();
services.AddSingleton<AppBaseType, CrowdfundAppType>();
@@ -80,7 +81,7 @@ namespace BTCPayServer.Plugins.Crowdfund
public override Task<string> ConfigureLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UICrowdfundController.UpdateCrowdfund),
- "UICrowdfund", new { appId = app.Id }, _options.Value.RootPath)!);
+ "UICrowdfund", new { area = CrowdfundPlugin.Area, appId = app.Id }, _options.Value.RootPath)!);
}
public Task<AppSalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
@@ -187,7 +188,7 @@ namespace BTCPayServer.Plugins.Crowdfund
var store = appData.StoreData;
var formUrl = settings.FormId != null
? _linkGenerator.GetPathByAction(nameof(UICrowdfundController.CrowdfundForm), "UICrowdfund",
- new { appId = appData.Id }, _options.Value.RootPath)
+ new { area = CrowdfundPlugin.Area, appId = appData.Id }, _options.Value.RootPath)
: null;
var vm = new ViewCrowdfundViewModel
{
@@ -264,7 +265,7 @@ namespace BTCPayServer.Plugins.Crowdfund
public override Task<string> ViewLink(AppData app)
{
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UICrowdfundController.ViewCrowdfund),
- "UICrowdfund", new { appId = app.Id }, _options.Value.RootPath)!);
+ "UICrowdfund", new { area = CrowdfundPlugin.Area, appId = app.Id }, _options.Value.RootPath)!);
}
}
}
diff --git a/BTCPayServer/Plugins/Crowdfund/Views/NavExtension.cshtml b/BTCPayServer/Plugins/Crowdfund/Views/NavExtension.cshtml
new file mode 100644
index 0000000..f7362bc
--- /dev/null
+++ b/BTCPayServer/Plugins/Crowdfund/Views/NavExtension.cshtml
@@ -0,0 +1,44 @@
+@using BTCPayServer.Client
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using BTCPayServer.Views.Apps
+@using BTCPayServer.Plugins.Crowdfund
+@using BTCPayServer.Services.Apps
+@inject AppService AppService
+@model BTCPayServer.Components.MainNav.MainNavViewModel
+@{
+ var store = Context.GetStoreData();
+}
+
+@if (store != null)
+{
+ var appType = AppService.GetAppType(CrowdfundAppType.AppType)!;
+ var apps = Model.Apps.Where(app => app.AppType == appType.Type).ToList();
+ <li class="nav-item" permission="@Policies.CanModifyStoreSettings">
+ <a area="@CrowdfundPlugin.Area" layout-menu-item="CreateApp-@appType.Type" asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@store.Id" asp-route-appType="@appType.Type">
+ <vc:icon symbol="nav-crowdfund" />
+ <span text-translate="true">Crowdfund</span>
+ </a>
+ </li>
+ @if (apps.Any())
+ {
+ <li layout-menu-item="CreateApp-@appType.Type" class="nav-item" not-permission="@Policies.CanModifyStoreSettings" permission="@Policies.CanViewStoreSettings">
+ <span class="nav-link">
+ <vc:icon symbol="nav-crowdfund" />
+ <span text-translate="true">Crowdfund</span>
+ </span>
+ </li>
+ }
+ @foreach (var app in apps)
+ {
+ <li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
+ <a asp-area="@CrowdfundPlugin.Area" layout-menu-item="@nameof(CrowdfundPlugin)-@app.Id" asp-controller="UICrowdfund" asp-action="UpdateCrowdfund" asp-route-appId="@app.Id">
+ <span>@app.AppName</span>
+ </a>
+ </li>
+ <li class="nav-item nav-item-sub" not-permission="@Policies.CanViewStoreSettings">
+ <a asp-area="@CrowdfundPlugin.Area" layout-menu-item="@nameof(CrowdfundPlugin)-@app.Id" asp-controller="UICrowdfund" asp-action="ViewCrowdfund" asp-route-appId="@app.Id">
+ <span>@app.AppName</span>
+ </a>
+ </li>
+ }
+}
diff --git a/BTCPayServer/Plugins/Crowdfund/Views/Public/ContributeForm.cshtml b/BTCPayServer/Plugins/Crowdfund/Views/Public/ContributeForm.cshtml
new file mode 100644
index 0000000..fbccf05
--- /dev/null
+++ b/BTCPayServer/Plugins/Crowdfund/Views/Public/ContributeForm.cshtml
@@ -0,0 +1,98 @@
+@using BTCPayServer.Client.Models
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@model BTCPayServer.Plugins.Crowdfund.Models.ContributeToCrowdfund
+
+@{ var vm = Model.ViewCrowdfundViewModel; }
+
+<form method="post">
+ @foreach (var item in vm.Perks)
+ {
+ var hasCount = vm.PerkCount.ContainsKey(item.Id);
+ var hasValue = vm.PerkValue.ContainsKey(item.Id);
+ <div class="card mb-4 perk expanded" id="@item.Id">
+ @if (vm.DisplayPerksRanking && hasCount)
+ {
+ <span class="btn btn-sm rounded-circle px-0 btn-primary perk-badge">#@(Array.IndexOf(vm.Perks, item) + 1)</span>
+ }
+ @if (!string.IsNullOrEmpty(item.Image))
+ {
+ <img class="card-img-top" src="@item.Image" alt="@item.Title" asp-append-version="true" />
+ }
+ <div class="card-body">
+ <div class="card-title d-flex align-items-center justify-content-between mb-1">
+ <label class="h5 d-flex align-items-center">
+ @if (vm.Started && !vm.Ended )
+ {
+ <input type="radio" asp-for="ChoiceKey" value="@item.Id" class="form-check-input mt-0 me-2"/>
+ }
+ @(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
+ </label>
+ <span class="text-muted">
+ @if (item.Price is > 0)
+ {
+ <span>@item.Price.Value</span>
+ <span>@vm.TargetCurrency</span>
+
+ if (item.PriceType == AppItemPriceType.Minimum)
+ {
+ @Safe.Raw(StringLocalizer["or more"])
+ }
+ }
+ else if (item.PriceType == AppItemPriceType.Topup)
+ {
+ @Safe.Raw(StringLocalizer["Any amount"])
+ }
+ else if (item.PriceType == AppItemPriceType.Fixed)
+ {
+ @Safe.Raw(StringLocalizer["Free"])
+ }
+ </span>
+ </div>
+ <p class="card-text overflow-hidden">@Safe.Raw(item.Description)</p>
+ </div>
+ @if (hasCount || hasValue || item.Inventory.HasValue)
+ {
+ <div class="card-footer d-flex flex-wrap justify-content-between">
+ @switch (item.Inventory)
+ {
+ case null:
+ break;
+ case <= 0:
+ <span text-translate="true">Sold out</span>
+ break;
+ default:
+ <span>@StringLocalizer["{0} left", item.Inventory]</span>
+ break;
+ }
+ @if (hasCount)
+ {
+ var count = vm.PerkCount[item.Id];
+ <span>@count Contributor@(count == 1 ? "" : "s")</span>
+ }
+ @if (hasValue)
+ {
+ <span>@vm.PerkValue[item.Id] @vm.TargetCurrency total</span>
+ }
+ </div>
+ }
+ </div>
+ }
+ @if (vm.Started && !vm.Ended)
+ {
+ <div class="form-group">
+ <label asp-for="Email" class="form-label"></label>
+ <input asp-for="Email" type="email" class="form-control"/>
+ <span asp-validation-for="Email" class="text-danger"></span>
+ </div>
+ <div class="form-group">
+ <label asp-for="Amount" class="form-label"></label>
+ <div class="input-group mb-3">
+ <input inputmode="decimal" asp-for="Amount" type="number" step="any" class="form-control"/>
+ <span class="input-group-text">@vm.TargetCurrency.ToUpperInvariant()</span>
+ </div>
+ <span asp-validation-for="Amount" class="text-danger"></span>
+ </div>
+ <input type="hidden" asp-for="RedirectToCheckout"/>
+ <button type="submit" class="btn btn-primary" text-translate="true">Contribute</button>
+ }
+</form>
diff --git a/BTCPayServer/Plugins/Crowdfund/Views/Public/ViewCrowdfund.cshtml b/BTCPayServer/Plugins/Crowdfund/Views/Public/ViewCrowdfund.cshtml
new file mode 100644
index 0000000..6e8e765
--- /dev/null
+++ b/BTCPayServer/Plugins/Crowdfund/Views/Public/ViewCrowdfund.cshtml
@@ -0,0 +1,390 @@
+@model BTCPayServer.Plugins.Crowdfund.Models.ViewCrowdfundViewModel
+@using BTCPayServer.Plugins.Crowdfund.Models
+@inject BTCPayServer.Services.BTCPayServerEnvironment Env
+@inject BTCPayServer.Security.ContentSecurityPolicies Csp
+@{
+ ViewData["Title"] = Model.Title;
+ ViewData["StoreBranding"] = Model.StoreBranding;
+ Layout = null;
+ Csp.UnsafeEval();
+ if (!string.IsNullOrEmpty(Model.DisqusShortname))
+ {
+ Csp.Add("script-src", $"https://{Model.DisqusShortname}.disqus.com");
+ Csp.Add("script-src", "https://c.disquscdn.com");
+ }
+}
+<!DOCTYPE html>
+<html lang="@Model.HtmlLang" class="h-100" @(Env.IsDeveloping ? " data-devenv" : "") id="Crowdfund-@Model.AppId">
+<head>
+ <partial name="LayoutHead" />
+ <link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet" />
+ <link href="~/crowdfund/styles/main.css" asp-append-version="true" rel="stylesheet" />
+ <style>
+ #app { --wrap-max-width: 1320px; }
+ #crowdfund-main-image {
+ border-radius: var(--btcpay-border-radius);
+ object-fit: cover;
+ max-width: 100%;
+ max-height: 40vh;
+ }
+ #crowdfund-body-description {
+ font-size: 16px;
+ }
+ .perk.card .card-img-top{
+ max-height: 210px;
+ object-fit: scale-down;
+ }
+ </style>
+ @this.Safe.Meta(Model.HtmlMetaTags)
+ <vc:ui-extension-point location="crowdfund-head" model="@Model"/>
+</head>
+<body class="min-vh-100 p-2">
+ @if (!Model.Enabled)
+ {
+ <div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert" text-translate="true">
+ This crowdfund page is not publicly viewable!
+ </div>
+ }
+ @if (Model.AnimationsEnabled)
+ {
+ <canvas id="fireworks" class="d-none"></canvas>
+ }
+
+ <div class="public-page-wrap" id="app" @(Model.SimpleDisplay ? "" : "v-cloak")>
+ @if (!string.IsNullOrEmpty(Model.MainImageUrl))
+ {
+ <img v-if="srvModel.mainImageUrl" :src="srvModel.mainImageUrl" :alt="srvModel.title" id="crowdfund-main-image" asp-append-version="true"/>
+ }
+ <div class="d-flex flex-column justify-content-between p-3 text-center" id="crowdfund-header-container">
+ <h1 class="mb-3">{{ srvModel.title }}</h1>
+ @if (!string.IsNullOrEmpty(Model.Tagline))
+ {
+ <h2 class="h3 mb-3 fw-semibold" v-if="srvModel.tagline" v-text="srvModel.tagline"></h2>
+ }
+ @if (Model.TargetAmount.HasValue)
+ {
+ <span v-if="srvModel.targetAmount" class="mt-3" id="crowdfund-header-target">
+ <h3 class="d-inline-block">
+ <span class="badge bg-info px-3" v-text="`${targetAmount} ${targetCurrency}`">@Math.Round(Model.TargetAmount.GetValueOrDefault(0)) @Model.TargetCurrency</span>
+ </h3>
+ @if (Model.ResetEveryAmount > 0 && !Model.NeverReset)
+ {
+ <span v-if="srvModel.resetEvery !== 'Never'"
+ class="h5 ms-2"
+ v-b-tooltip
+ :title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" text-translate="true">
+ Dynamic
+ </span>
+ }
+ @if (Model.EnforceTargetAmount)
+ {
+ <span v-if="srvModel.enforceTargetAmount" class="h5 ms-2" v-b-tooltip title=@StringLocalizer["No contributions allowed after the goal has been reached"] text-translate="true">
+ Hardcap Goal
+ </span>
+ }
+ else
+ {
+ <span v-if="!srvModel.enforceTargetAmount" class="h5 ms-2" v-b-tooltip title=@StringLocalizer["Contributions allowed even after goal is reached"] text-translate="true">
+ Softcap Goal
+ </span>
+ }
+ </span>
+ }
+ @if (!Model.Started && Model.StartDate.HasValue)
+ {
+ <h6 class="text-muted fst-italic mt-3" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" v-text="`Starts in ${startDiff}`" data-test="time-state">
+ @StringLocalizer["Starts {0}", TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)]
+ </h6>
+ }
+ else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
+ {
+ <h6 class="text-muted fst-italic mt-3" v-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" v-text="`Ends in ${endDiff}`" data-test="time-state">
+ @StringLocalizer["Ends {0}", TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)]
+ </h6>
+ }
+ else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
+ {
+ <h6 class="text-muted fst-italic mt-3" v-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" data-test="time-state" text-translate="true">
+ Currently active!
+ </h6>
+ }
+ </div>
+
+ @if (Model.TargetAmount.HasValue)
+ {
+ <div class="progress rounded-pill" v-if="srvModel.targetAmount" id="crowdfund-progress-bar">
+ <div class="progress-bar bg-primary"
+ role="progressbar"
+ style="width:@(Model.Info.ProgressPercentage + "%")"
+ :aria-valuenow="srvModel.info.progressPercentage"
+ v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
+ aria-valuemin="0"
+ id="crowdfund-progress-bar-confirmed-bar"
+ v-b-tooltip
+ :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% contributions'"
+ aria-valuemax="100">
+ </div>
+ <div class="progress-bar bg-warning"
+ role="progressbar"
+ id="crowdfund-progress-bar-pending-bar"
+ style="width:@(Model.Info.PendingProgressPercentage + "%")"
+ :aria-valuenow="srvModel.info.pendingProgressPercentage"
+ v-bind:style="{ width: srvModel.info.pendingProgressPercentage + '%' }"
+ v-b-tooltip
+ :title="parseFloat(srvModel.info.pendingProgressPercentage).toFixed(2) + '% contributions pending confirmation'"
+ aria-valuemin="0"
+ aria-valuemax="100">
+ </div>
+ </div>
+ }
+
+ <div class="row py-2 text-center crowdfund-stats">
+ <div class="col-sm border-end p-3 text-center" id="crowdfund-body-raised-amount">
+ <h3 v-text="`${raisedAmount} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount, Model.CurrencyData.Divisibility) @Model.TargetCurrency</h3>
+ <h5 class="text-muted fst-italic mb-0">Raised</h5>
+ <b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0" class="only-for-js">
+ <ul class="p-0 text-uppercase">
+ <li v-for="stat of paymentStats" class="list-unstyled">
+ {{stat.label}} <span v-if="stat.lightning"><vc:icon symbol="lightning" /></span> {{stat.value}}
+ </li>
+ </ul>
+ </b-tooltip>
+ </div>
+
+ <div class="col-sm border-end p-3 text-center" id="crowdfund-body-goal-raised">
+ <h3 v-text="`${percentageRaisedAmount}%`">@Math.Round(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h3>
+ <h5 class="text-muted fst-italic mb-0">Of Goal</h5>
+ <b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'" class="only-for-js">
+ Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
+ </b-tooltip>
+ </div>
+
+ <div class="col-sm border-end p-3 text-center" id="crowdfund-body-total-contributors">
+ <h3 v-text="new Intl.NumberFormat().format(srvModel.info.totalContributors)">@Model.Info.TotalContributors</h3>
+ <h5 class="text-muted fst-italic mb-0" text-translate="true">Contributors</h5>
+ </div>
+
+ @if (Model.StartDate.HasValue || Model.EndDate.HasValue)
+ {
+ <div class="col-sm border-end p-3 text-center" id="crowdfund-body-campaign-dates">
+ @if (!Model.Started && Model.StartDate.HasValue)
+ {
+ <div v-if="startDiff">
+ <h3 v-text="startDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)</h3>
+ <h5 class="text-muted fst-italic mb-0" v-text="'Left to start'" text-translate="true">Start Date</h5>
+ </div>
+ }
+ else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
+ {
+ <div v-if="!startDiff && endDiff">
+ <h3 v-text="endDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)</h3>
+ <h5 class="text-muted fst-italic mb-0" v-text="'Left'" text-translate="true">End Date</h5>
+ </div>
+ }
+ else if (Model.Ended)
+ {
+ <div v-if="ended">
+ <h3 class="mb-0" text-translate="true">Campaign not active</h3>
+ </div>
+ }
+ <b-tooltip v-if="startDate || endDate" target="crowdfund-body-campaign-dates" class="only-for-js">
+ <ul class="p-0">
+ @if (Model.StartDate.HasValue)
+ {
+ <li v-if="startDate" class="list-unstyled">
+ {{started ? "Started" : "Starts"}} {{startDate}}
+ </li>
+ }
+ @if (Model.EndDate.HasValue)
+ {
+ <li v-if="endDate" class="list-unstyled">
+ {{ended ? "Ended" : "Ends"}} {{endDate}}
+ </li>
+ }
+ </ul>
+ </b-tooltip>
+ </div>
+ }
+ </div>
+
+ <div class="text-center mb-4" id="crowdfund-body-header">
+ <button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary py-2 px-5 only-for-js" v-on:click="contribute" text-translate="true">Contribute</button>
+ </div>
+
+ <div class="row mt-4 justify-content-between gap-5">
+ <div :class="{ 'col-lg-7 col-sm-12': hasPerks, 'col-12': !hasPerks }" id="crowdfund-body-description-container">
+ <template v-if="srvModel.disqusEnabled && srvModel.disqusShortname">
+ <b-tabs>
+ <b-tab title="Details" active>
+ <div class="overflow-hidden pt-3" v-html="srvModel.description" id="crowdfund-body-description">
+ </div>
+ </b-tab>
+ <b-tab title="Discussion">
+ <div id="disqus_thread" class="mt-4"></div>
+ </b-tab>
+ </b-tabs>
+ </template>
+ <template v-else>
+ <div class="overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
+ </div>
+ </template>
+ </div>
+ <div class="col-lg-4 col-sm-12" id="crowdfund-body-contribution-container" v-if="hasPerks">
+ <contribute :target-currency="srvModel.targetCurrency"
+ :loading="loading"
+ :display-perks-ranking="srvModel.displayPerksRanking"
+ :perks-value="srvModel.perksValue"
+ :active="active"
+ :in-modal="false"
+ :perks="perks">
+ </contribute>
+ </div>
+ </div>
+ <noscript v-pre>
+ <div class="row justify-content-between">
+ <div class="col-md-7 col-sm-12">
+ <div class="overflow-hidden">@Safe.Raw(Model.Description)</div>
+ </div>
+ <div class="col-md-4 col-sm-12">
+ <partial name="/Plugins/Crowdfund/Views/Public/ContributeForm.cshtml" model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })" />
+ </div>
+ </div>
+ </noscript>
+ <b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="@StringLocalizer["Close"]" ref="modalContribute">
+ <contribute v-if="contributeModalOpen"
+ :target-currency="srvModel.targetCurrency"
+ :active="active"
+ :perks="srvModel.perks"
+ :loading="loading"
+ :in-modal="true">
+ </contribute>
+ </b-modal>
+ <footer class="store-footer">
+ <p class="text-muted" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</p>
+ <a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
+ <span text-translate="true">Powered by</span> <partial name="_StoreFooterLogo" />
+ </a>
+ </footer>
+ </div>
+
+ <template id="perks-template">
+ <div class="perks-container">
+ <perk v-if="!perks || perks.length === 0"
+ :perk="{title: 'Donate Custom Amount', priceType: 'Topup', price: { type: 'Topup' } }"
+ :target-currency="targetCurrency"
+ :active="active"
+ :loading="loading"
+ :in-modal="inModal">
+ </perk>
+ <perk v-for="(perk, index) in perks"
+ :key="perk.id"
+ :perk="perk"
+ :target-currency="targetCurrency"
+ :active="active"
+ :display-perks-ranking="displayPerksRanking"
+ :perks-value="perksValue"
+ :index="index"
+ :loading="loading"
+ :in-modal="inModal">
+ </perk>
+ </div>
+ </template>
+ <template id="perk-template">
+ <div class="card perk" v-bind:class="{ 'expanded': expanded, 'unexpanded': !expanded, 'mb-4':!inModal }" v-on:click="expand" :id="perk.id">
+ <span v-if="displayPerksRanking && perk.sold"
+ class="btn btn-sm rounded-circle px-0 perk-badge"
+ v-bind:class="{ 'btn-primary': index==0, 'btn-light': index!=0}">
+ #{{index+1}}
+ </span>
+ <div class="perk-zoom" v-if="canExpand">
+ <div class="perk-zoom-bg"></div>
+ <div class="perk-zoom-text w-100 py-2 px-4 text-center text-primary fw-semibold fs-5 lh-sm" text-translate="true">
+ Select this contribution perk
+ </div>
+ </div>
+ <form v-on:submit="onContributeFormSubmit" class="mb-0">
+ <input type="hidden" :value="perk.id" id="choiceKey" />
+ <img v-if="perk.image && perk.image != 'null'" class="card-img-top" :src="perk.image" />
+ <div class="card-body">
+ <div class="card-title d-flex justify-content-between" :class="{ 'mb-0': !perk.description }">
+ <span class="h5" :class="{ 'mb-0': !perk.description }">{{perk.title ? perk.title : perk.id}}</span>
+ <span class="text-muted">
+ <template v-if="perk.priceType === 'Fixed' && amount == 0" text-translate="true">
+ Free
+ </template>
+ <template v-else-if="amount">
+ {{formatAmount(perk.price.noExponents(), srvModel.currencyData.divisibility)}}
+ {{targetCurrency}}
+ <template v-if="perk.price.type === 'Minimum'" text-translate="true">or more</template>
+ </template>
+ <template v-else-if="perk.priceType === 'Topup' || (!amount && perk.priceType === 'Minimum')" text-translate="true">
+ Any amount
+ </template>
+ </span>
+ </div>
+ <p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
+ <div class="input-group mt-3" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
+ <template v-if="perk.priceType !== 'Topup' && !(perk.priceType === 'Fixed' && amount == 0)">
+ <input type="number" class="form-control hide-number-spin"
+ v-model="amount"
+ :disabled="!active"
+ :readonly="perk.priceType === 'Fixed'"
+ :min="perk.price"
+ step="any"
+ placeholder="@StringLocalizer["Contribution Amount"]"
+ required>
+ <span class="input-group-text">{{targetCurrency}}</span>
+ </template>
+ <button class="btn btn-primary d-flex align-items-center"
+ :class="{'btn-disabled': loading}"
+ type="submit">
+ <div v-if="loading" class="spinner-grow spinner-grow-sm me-2" role="status">
+ <span class="visually-hidden" text-translate="true">Loading...</span>
+ </div>
+ {{perk.buyButtonText || 'Continue'}}
+ </button>
+ </div>
+ </div>
+ <div class="card-footer d-flex justify-content-between" v-if="perk.sold || perk.inventory != null">
+ <span v-if="perk.inventory != null && perk.inventory > 0" class="text-center text-muted">{{new Intl.NumberFormat().format(perk.inventory)}} left</span>
+ <span v-if="perk.inventory != null && perk.inventory <= 0" class="text-center text-muted">Sold out</span>
+ <span v-if="perk.sold">{{new Intl.NumberFormat().format(perk.sold)}} Contributor{{perk.sold === 1 ? "": "s"}}</span>
+ <span v-if="perk.value">{{formatAmount(perk.value, srvModel.currencyData.divisibility)}} {{targetCurrency}} total</span>
+ </div>
+ </form>
+ </div>
+ </template>
+
+ <template id="contribute-template">
+ <div>
+ <h3 v-if="!inModal" class="mb-3" text-translate="true">Contribute</h3>
+ <perks :perks="perks"
+ :loading="loading"
+ :in-modal="inModal"
+ :display-perks-ranking="displayPerksRanking"
+ :perks-value="perksValue"
+ :target-currency="targetCurrency"
+ :active="active">
+ </perks>
+ </div>
+ </template>
+
+ @if (!Model.SimpleDisplay)
+ {
+ <script>var srvModel = @Safe.Json(Model);</script>
+ <script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
+ <script src="~/vendor/moment/moment.min.js" asp-append-version="true"></script>
+ <script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
+ <script src="~/vendor/vue-toasted/vue-toasted.min.js" asp-append-version="true"></script>
+ <script src="~/vendor/bootstrap-vue/bootstrap-vue.min.js" asp-append-version="true"></script>
+ <script src="~/vendor/signalr/signalr.js" asp-append-version="true"></script>
+ <script src="~/vendor/animejs/anime.min.js" asp-append-version="true"></script>
+ <script src="~/crowdfund/app.js" asp-append-version="true"></script>
+ <script src="~/crowdfund/services/audioplayer.js" asp-append-version="true"></script>
+ <script src="~/crowdfund/services/fireworks.js" asp-append-version="true"></script>
+ <script src="~/crowdfund/services/listener.js" asp-append-version="true"></script>
+ <script src="~/modal/btcpay.js" asp-append-version="true"></script>
+ }
+<partial name="LayoutFoot"/>
+</body>
+</html>
diff --git a/BTCPayServer/Plugins/Crowdfund/Views/UpdateCrowdfund.cshtml b/BTCPayServer/Plugins/Crowdfund/Views/UpdateCrowdfund.cshtml
new file mode 100644
index 0000000..97e7747
--- /dev/null
+++ b/BTCPayServer/Plugins/Crowdfund/Views/UpdateCrowdfund.cshtml
@@ -0,0 +1,392 @@
+@using System.Globalization
+@using BTCPayServer.Abstractions.Contracts
+@using BTCPayServer.Client
+@using BTCPayServer.TagHelpers
+@using BTCPayServer.Views.Apps
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using BTCPayServer.Forms
+@using BTCPayServer.Plugins.Crowdfund
+@inject FormDataService FormDataService
+@inject IFileService FileService
+@inject BTCPayServer.Security.ContentSecurityPolicies Csp
+@model BTCPayServer.Plugins.Crowdfund.Models.UpdateCrowdfundViewModel
+@{
+ Layout = "_Layout";
+ ViewData.SetLayoutModel(new LayoutModel($"{nameof(CrowdfundPlugin)}-{Model.AppId}", StringLocalizer["Update Crowdfund"]));
+ Csp.UnsafeEval();
+ var canUpload = await FileService.IsAvailable();
+ var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
+}
+
+@section PageHeadContent {
+ <link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true" />
+ <style>.flatpickr-wrapper { flex-grow: 1; }</style>
+}
+
+@section PageFootContent {
+ <partial name="_ValidationScriptsPartial" />
+ <script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
+ <script src="~/crowdfund/admin.js" asp-append-version="true"></script>
+}
+
+<form method="post" enctype="multipart/form-data" permissioned="@Policies.CanModifyStoreSettings">
+ <div class="sticky-header">
+ <h2>@ViewData["Title"]</h2>
+ <div>
+ <button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
+ <a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
+ @if (Model.Archived)
+ {
+ <button type="submit" class="btn btn-outline-secondary" name="Archived" value="False" text-translate="true">Unarchive</button>
+ }
+ else if (Model.ModelWithMinimumData)
+ {
+ <a class="btn btn-secondary" asp-area="@CrowdfundPlugin.Area" asp-controller="UICrowdfund" asp-action="ViewCrowdfund" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank" text-translate="true">View</a>
+ }
+ </div>
+ </div>
+
+ <partial name="_StatusMessage" />
+
+ <input type="hidden" asp-for="StoreId" />
+ <input type="hidden" asp-for="Archived" />
+
+ @if (!ViewContext.ModelState.IsValid)
+ {
+ <div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
+ }
+ <div class="row" style="max-width:540px;">
+ <div class="col-sm-6">
+ <div class="form-group">
+ <label asp-for="AppName" class="form-label" data-required></label>
+ <input asp-for="AppName" class="form-control" required />
+ <span asp-validation-for="AppName" class="text-danger"></span>
+ </div>
+ </div>
+ <div class="col-sm-6">
+ <div class="form-group">
+ <label asp-for="Title" class="form-label" data-required></label>
+ <input asp-for="Title" class="form-control" required />
+ <span asp-validation-for="Title" class="text-danger"></span>
+ </div>
+ </div>
+ <div class="form-group">
+ <label asp-for="Tagline" class="form-label"></label>
+ <input asp-for="Tagline" class="form-control" />
+ <span asp-validation-for="Tagline" class="text-danger"></span>
+ </div>
+ <div class="form-group">
+ <div class="d-flex align-items-center justify-content-between gap-2">
+ <label asp-for="MainImageFile" class="form-label"></label>
+ @if (!string.IsNullOrEmpty(Model.MainImageUrl))
+ {
+ <button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
+ <vc:icon symbol="cross" /> <span text-translate="true">Remove</span>
+ </button>
+ }
+ </div>
+ @if (canUpload)
+ {
+ <div class="d-flex align-items-center gap-3">
+ <input asp-for="MainImageFile" type="file" class="form-control flex-grow">
+ @if (!string.IsNullOrEmpty(Model.MainImageUrl))
+ {
+ <img src="@Model.MainImageUrl" alt="Logo" style="height:2.1rem;max-width:10.5rem;" />
+ }
+ </div>
+ <span asp-validation-for="MainImageFile" class="text-danger"></span>
+ }
+ else
+ {
+ <input asp-for="MainImageFile" type="file" class="form-control" disabled>
+ <div class="form-text">@ViewLocalizer["In order to upload, a {0} must be configured.", Html.ActionLink(StringLocalizer["file storage"], "Files", "UIServer")]</div>
+ }
+ </div>
+ <div class="form-group">
+ <div class="d-flex align-items-center">
+ <input asp-for="Enabled" type="checkbox" class="btcpay-toggle me-3"/>
+ <div>
+ <label asp-for="Enabled" class="form-check-label"></label>
+ <span asp-validation-for="Enabled" class="text-danger"></span>
+ <div class="text-muted" text-translate="true">The crowdfund will be visible to anyone.</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row mt-3">
+ <div class="col-xxl-constrain">
+ <div class="form-group">
+ <label asp-for="Description" class="form-label" data-required></label>
+ <textarea asp-for="Description" rows="20" cols="40" class="form-control richtext"></textarea>
+ <span asp-validation-for="Description" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xl-10 col-xxl-constrain">
+ <h3 class="mt-5 mb-4">Goal</h3>
+ <div class="d-flex flex-wrap gap-3 mb-3">
+ <div class="form-group w-250px mb-0">
+ <label asp-for="TargetAmount" class="form-label"></label>
+ <input inputmode="decimal" asp-for="TargetAmount" class="form-control" />
+ <span asp-validation-for="TargetAmount" class="text-danger"></span>
+ </div>
+ <div class="form-group">
+ <label asp-for="TargetCurrency" class="form-label"></label>
+ <input asp-for="TargetCurrency" class="form-control w-auto" currency-selection />
+ <div class="form-text">@StringLocalizer["Uses the store's default currency ({0}) if empty.", @Model.StoreDefaultCurrency]</div>
+ <span asp-validation-for="TargetCurrency" class="text-danger"></span>
+ </div>
+ </div>
+ <div class="d-flex flex-wrap gap-3 align-items-center mb-4">
+ <div class="form-group mb-0 w-250px">
+ <label asp-for="StartDate" class="form-label"></label>
+ <div class="input-group flex-nowrap">
+ <input type="datetime-local" asp-for="StartDate"
+ value="@(Model.StartDate?.ToString("u", CultureInfo.InvariantCulture))"
+ class="form-control flatdtpicker"
+ placeholder="@StringLocalizer["No start date has been set"]" />
+ <button class="btn btn-secondary input-group-clear px-3" type="button" title="Clear">
+ <vc:icon symbol="close"/>
+ </button>
+ </div>
+ </div>
+ <div class="form-group mb-0 w-250px">
+ <label asp-for="EndDate" class="form-label"></label>
+ <div class="input-group flex-nowrap">
+ <input type="datetime-local" asp-for="EndDate"
+ value="@(Model.EndDate?.ToString("u", CultureInfo.InvariantCulture))"
+ class="form-control flatdtpicker"
+ placeholder="@StringLocalizer["No end date has been set"]" />
+ <button class="btn btn-secondary input-group-clear px-3" type="button" title="Clear">
+ <vc:icon symbol="close"/>
+ </button>
+ </div>
+ </div>
+ <span asp-validation-for="StartDate" class="text-danger"></span>
+ <span asp-validation-for="EndDate" class="text-danger"></span>
+ </div>
+
+ <div class="form-group mt-4" id="ResetRow" hidden="@(Model.StartDate == null)">
+ <div class="d-flex align-items-center mb-3">
+ <input asp-for="IsRecurring" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#ResetEverySettings" aria-expanded="@(Model.IsRecurring)" aria-controls="ResetEverySettings" />
+ <div>
+ <label asp-for="IsRecurring" class="form-check-label" text-translate="true">Recurring Goal</label>
+ <span asp-validation-for="IsRecurring" class="text-danger"></span>
+ <div class="text-muted" text-translate="true">Reset goal after a specific period of time, based on your crowdfund's start date.</div>
+ </div>
+ </div>
+
+ <div class="collapse @(Model.IsRecurring ? "show" : "")" id="ResetEverySettings">
+ <div class="form-group mb-0 pt-2 w-250px">
+ <label asp-for="ResetEveryAmount" class="form-label"></label>
+ <div class="d-flex align-items-center">
+ <input type="number" inputmode="numeric" asp-for="ResetEveryAmount" placeholder="@StringLocalizer["Amount"]" class="form-control me-3" min="0">
+ <select class="form-select w-auto" asp-for="ResetEvery">
+ @foreach (var opt in Model.ResetEveryValues)
+ {
+ <option value="@opt">@opt</option>
+ }
+ </select>
+ </div>
+ <span asp-validation-for="ResetEveryAmount" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="perks" class="row">
+ <div class="col-xxl-constrain">
+ <partial name="TemplateEditor" model="@(nameof(Model.PerksTemplate), Model.PerksTemplate, "Perks", Model.TargetCurrency ?? Model.StoreDefaultCurrency)" />
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xl-8 col-xxl-constrain">
+ <h3 class="mt-5 mb-4" text-translate="true">Contributions</h3>
+ <div class="d-flex mb-3">
+ <input asp-for="SortPerksByPopularity" type="checkbox" class="btcpay-toggle me-3" />
+ <label asp-for="SortPerksByPopularity" class="form-check-label"></label>
+ <span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
+ </div>
+ <div class="d-flex mb-3">
+ <input asp-for="DisplayPerksRanking" type="checkbox" class="btcpay-toggle me-3" />
+ <label asp-for="DisplayPerksRanking" class="form-check-label"></label>
+ <span asp-validation-for="DisplayPerksRanking" class="text-danger"></span>
+ </div>
+ <div class="d-flex mb-3">
+ <input asp-for="DisplayPerksValue" type="checkbox" class="btcpay-toggle me-3" />
+ <label asp-for="DisplayPerksValue" class="form-check-label"></label>
+ <span asp-validation-for="DisplayPerksValue" class="text-danger"></span>
+ </div>
+ <div class="d-flex mb-3">
+ <input asp-for="EnforceTargetAmount" type="checkbox" class="btcpay-toggle me-3" />
+ <label asp-for="EnforceTargetAmount" class="form-check-label"></label>
+ <span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
+ </div>
+
+ <h3 class="mt-5 mb-4" text-translate="true">Crowdfund Behavior</h3>
+ <div class="d-flex">
+ <input asp-for="UseAllStoreInvoices" type="checkbox" class="btcpay-toggle me-3" />
+ <label asp-for="UseAllStoreInvoices" class="form-check-label"></label>
+ <span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
+ </div>
+
+ <h3 class="mt-5 mb-4" text-translate="true">Checkout</h3>
+ <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>
+
+ <h3 class="mt-5 mb-2" text-translate="true">Additional Options</h3>
+ <div class="form-group">
+ <div class="accordion" id="additional">
+
+ <div class="accordion-item">
+ <h2 class="accordion-header" id="additional-htmlheader-header">
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-htmlheader" aria-expanded="false" aria-controls="additional-htmlheader">
+ <span text-translate="true">HTML Headers</span>
+ <vc:icon symbol="caret-down" />
+ </button>
+ </h2>
+ <div id="additional-htmlheader" class="accordion-collapse collapse" aria-labelledby="additional-htmlheader-header">
+ <div class="accordion-body">
+ <div class="form-group">
+ <label asp-for="Language" class="form-label"></label>
+ <input asp-for="Language" class="form-control" maxlength="2" required />
+ <div class="form-text">Fix the HTML page language</div>
+ <span asp-validation-for="Language" class="text-danger"></span>
+ </div>
+ <div class="form-group">
+ <label asp-for="HtmlMetaTags" class="form-label"></label>
+ <textarea asp-for="HtmlMetaTags" rows="5" cols="40" class="form-control"
+ placeholder='<meta name="description" content="Your description">
+<meta name="keywords" content="keyword1, keyword2, keyword3">
+<meta name="author" content="John Doe">
+Please insert valid HTML here. Only meta tags accepted.'>
+ </textarea>
+ <span asp-validation-for="HtmlMetaTags" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="accordion-item">
+ <h2 class="accordion-header" id="additional-sound-header">
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-sound" aria-expanded="false" aria-controls="additional-sound">
+ <span text-translate="true">Sound</span>
+ <vc:icon symbol="caret-down" />
+ </button>
+ </h2>
+ <div id="additional-sound" class="accordion-collapse collapse" aria-labelledby="additional-sound-header">
+ <div class="accordion-body">
+ <div class="form-group mb-0">
+ <div class="d-flex align-items-center">
+ <input asp-for="SoundsEnabled" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SoundsEnabledSettings" aria-expanded="@Model.SoundsEnabled" aria-controls="SoundsEnabledSettings"/>
+ <label asp-for="SoundsEnabled" class="form-check-label"></label>
+ <span asp-validation-for="SoundsEnabled" class="text-danger"></span>
+ </div>
+ </div>
+ <div class="collapse @(Model.SoundsEnabled ? "show" : "")" id="SoundsEnabledSettings">
+ <div class="form-group mb-0 pt-3">
+ <label asp-for="Sounds" class="form-label"></label>
+ <textarea asp-for="Sounds" class="form-control" rows="5"></textarea>
+ <span asp-validation-for="Sounds" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="accordion-item">
+ <h2 class="accordion-header" id="additional-animation-header">
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-animation" aria-expanded="false" aria-controls="additional-animation">
+ <span text-translate="true">Animation</span>
+ <vc:icon symbol="caret-down" />
+ </button>
+ </h2>
+ <div id="additional-animation" class="accordion-collapse collapse" aria-labelledby="additional-animation-header">
+ <div class="accordion-body">
+ <div class="form-group mb-0">
+ <div class="d-flex align-items-center">
+ <input asp-for="AnimationsEnabled" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#AnimationsEnabledSettings" aria-expanded="@Model.AnimationsEnabled" aria-controls="AnimationsEnabledSettings"/>
+ <label asp-for="AnimationsEnabled" class="form-check-label"></label>
+ <span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
+ </div>
+ </div>
+ <div class="collapse @(Model.AnimationsEnabled ? "show" : "")" id="AnimationsEnabledSettings">
+ <div class="form-group mb-0 pt-3">
+ <label asp-for="AnimationColors" class="form-label"></label>
+ <textarea asp-for="AnimationColors" class="form-control" rows="5"></textarea>
+ <span asp-validation-for="AnimationColors" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="accordion-item">
+ <h2 class="accordion-header" id="additional-discussion-header">
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-discussion" aria-expanded="false" aria-controls="additional-discussion">
+ <span text-translate="true">Discussion</span>
+ <vc:icon symbol="caret-down" />
+ </button>
+ </h2>
+ <div id="additional-discussion" class="accordion-collapse collapse" aria-labelledby="additional-discussion-header">
+ <div class="accordion-body">
+ <div class="form-group mb-0">
+ <div class="d-flex align-items-center">
+ <input asp-for="DisqusEnabled" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#DisqusEnabledSettings" aria-expanded="@Model.DisqusEnabled" aria-controls="DisqusEnabledSettings"/>
+ <label asp-for="DisqusEnabled" class="form-check-label"></label>
+ <span asp-validation-for="DisqusEnabled" class="text-danger"></span>
+ </div>
+ </div>
+ <div class="collapse @(Model.DisqusEnabled ? "show" : "")" id="DisqusEnabledSettings">
+ <div class="form-group mb-0 pt-3">
+ <label asp-for="DisqusShortname" class="form-label"></label>
+ <input asp-for="DisqusShortname" class="form-control" />
+ <span asp-validation-for="DisqusShortname" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="accordion-item">
+ <h2 class="accordion-header" id="additional-notification-header">
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notification" aria-expanded="false" aria-controls="additional-notification">
+ <span text-translate="true">Notification URL Callbacks</span>
+ <vc:icon symbol="caret-down" />
+ </button>
+ </h2>
+ <div id="additional-notification" class="accordion-collapse collapse" aria-labelledby="additional-notification-header">
+ <div class="accordion-body">
+ <div class="form-group">
+ <label asp-for="NotificationUrl" class="form-label"></label>
+ <input asp-for="NotificationUrl" class="form-control" />
+ <span asp-validation-for="NotificationUrl" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</form>
+
+<div class="d-grid d-sm-flex flex-wrap gap-3 mt-3">
+ <form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@Model.AppId" permission="@Policies.CanModifyStoreSettings">
+ <button type="submit" class="w-100 btn btn-outline-secondary" id="btn-archive-toggle">
+ @if (Model.Archived)
+ {
+ <span class="text-nowrap">Unarchive this app</span>
+ }
+ else
+ {
+ <span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this app so that it does not appear in the apps list by default">Archive this app</span>
+ }
+ </button>
+ </form>
+ <a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@Model.AppId" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(Model.AppName)</strong> and its settings will be permanently deleted." data-confirm-input="@StringLocalizer["Delete"]" permission="@Policies.CanModifyStoreSettings">Delete this app</a>
+</div>
+
+<partial name="_Confirm" model="@(new ConfirmModel(StringLocalizer["Delete app"], StringLocalizer["This app will be removed from this store."], StringLocalizer["Delete"]))" permission="@Policies.CanModifyStoreSettings" />
+
diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs
index 3331a06..89145b2 100644
--- a/BTCPayServer/Services/Apps/AppService.cs
+++ b/BTCPayServer/Services/Apps/AppService.cs
@@ -343,9 +343,9 @@ namespace BTCPayServer.Services.Apps
return await query.FirstOrDefaultAsync();
}
- public Task<StoreData?> GetStore(AppData app)
+ public async Task<StoreData> GetStore(AppData app)
{
- return _storeRepository.FindStore(app.StoreDataId);
+ return await _storeRepository.FindStore(app.StoreDataId) ?? throw new Exception("App's store is not found");
}
public static string SerializeTemplate(AppItem[] items)
diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs
index 5bf4561..0994c5e 100644
--- a/BTCPayServer/Services/Stores/StoreRepository.cs
+++ b/BTCPayServer/Services/Stores/StoreRepository.cs
@@ -46,8 +46,6 @@ namespace BTCPayServer.Services.Stores
public async Task<StoreData?> FindStore(string storeId)
{
- if (storeId == null)
- return null;
await using var ctx = _ContextFactory.CreateContext();
var result = await ctx.FindAsync<StoreData>(storeId).ConfigureAwait(false);
return result;
diff --git a/BTCPayServer/Views/Shared/Crowdfund/NavExtension.cshtml b/BTCPayServer/Views/Shared/Crowdfund/NavExtension.cshtml
deleted file mode 100644
index 3204cc1..0000000
--- a/BTCPayServer/Views/Shared/Crowdfund/NavExtension.cshtml
+++ /dev/null
@@ -1,44 +0,0 @@
-@using BTCPayServer.Client
-@using Microsoft.AspNetCore.Mvc.TagHelpers
-@using BTCPayServer.Views.Apps
-@using BTCPayServer.Plugins.Crowdfund
-@using BTCPayServer.Services.Apps
-@inject AppService AppService
-@model BTCPayServer.Components.MainNav.MainNavViewModel
-@{
- var store = Context.GetStoreData();
-}
-
-@if (store != null)
-{
- var appType = AppService.GetAppType(CrowdfundAppType.AppType)!;
- var apps = Model.Apps.Where(app => app.AppType == appType.Type).ToList();
- <li class="nav-item" permission="@Policies.CanModifyStoreSettings">
- <a layout-menu-item="CreateApp-@appType.Type" asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@store.Id" asp-route-appType="@appType.Type">
- <vc:icon symbol="nav-crowdfund" />
- <span text-translate="true">Crowdfund</span>
- </a>
- </li>
- @if (apps.Any())
- {
- <li layout-menu-item="CreateApp-@appType.Type" class="nav-item" not-permission="@Policies.CanModifyStoreSettings" permission="@Policies.CanViewStoreSettings">
- <span class="nav-link">
- <vc:icon symbol="nav-crowdfund" />
- <span text-translate="true">Crowdfund</span>
- </span>
- </li>
- }
- @foreach (var app in apps)
- {
- <li class="nav-item nav-item-sub" permission="@Policies.CanViewStoreSettings">
- <a layout-menu-item="@nameof(CrowdfundPlugin)-@app.Id" asp-area="" asp-controller="UICrowdfund" asp-action="UpdateCrowdfund" asp-route-appId="@app.Id">
- <span>@app.AppName</span>
- </a>
- </li>
- <li class="nav-item nav-item-sub" not-permission="@Policies.CanViewStoreSettings">
- <a layout-menu-item="@nameof(CrowdfundPlugin)-@app.Id" asp-area="" asp-controller="UICrowdfund" asp-action="ViewCrowdfund" asp-route-appId="@app.Id">
- <span>@app.AppName</span>
- </a>
- </li>
- }
-}
diff --git a/BTCPayServer/Views/Shared/Crowdfund/Public/ContributeForm.cshtml b/BTCPayServer/Views/Shared/Crowdfund/Public/ContributeForm.cshtml
deleted file mode 100644
index fbccf05..0000000
--- a/BTCPayServer/Views/Shared/Crowdfund/Public/ContributeForm.cshtml
+++ /dev/null
@@ -1,98 +0,0 @@
-@using BTCPayServer.Client.Models
-@using Microsoft.AspNetCore.Mvc.TagHelpers
-@model BTCPayServer.Plugins.Crowdfund.Models.ContributeToCrowdfund
-
-@{ var vm = Model.ViewCrowdfundViewModel; }
-
-<form method="post">
- @foreach (var item in vm.Perks)
- {
- var hasCount = vm.PerkCount.ContainsKey(item.Id);
- var hasValue = vm.PerkValue.ContainsKey(item.Id);
- <div class="card mb-4 perk expanded" id="@item.Id">
- @if (vm.DisplayPerksRanking && hasCount)
- {
- <span class="btn btn-sm rounded-circle px-0 btn-primary perk-badge">#@(Array.IndexOf(vm.Perks, item) + 1)</span>
- }
- @if (!string.IsNullOrEmpty(item.Image))
- {
- <img class="card-img-top" src="@item.Image" alt="@item.Title" asp-append-version="true" />
- }
- <div class="card-body">
- <div class="card-title d-flex align-items-center justify-content-between mb-1">
- <label class="h5 d-flex align-items-center">
- @if (vm.Started && !vm.Ended )
- {
- <input type="radio" asp-for="ChoiceKey" value="@item.Id" class="form-check-input mt-0 me-2"/>
- }
- @(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
- </label>
- <span class="text-muted">
- @if (item.Price is > 0)
- {
- <span>@item.Price.Value</span>
- <span>@vm.TargetCurrency</span>
-
- if (item.PriceType == AppItemPriceType.Minimum)
- {
- @Safe.Raw(StringLocalizer["or more"])
- }
- }
- else if (item.PriceType == AppItemPriceType.Topup)
- {
- @Safe.Raw(StringLocalizer["Any amount"])
- }
- else if (item.PriceType == AppItemPriceType.Fixed)
- {
- @Safe.Raw(StringLocalizer["Free"])
- }
- </span>
- </div>
- <p class="card-text overflow-hidden">@Safe.Raw(item.Description)</p>
- </div>
- @if (hasCount || hasValue || item.Inventory.HasValue)
- {
- <div class="card-footer d-flex flex-wrap justify-content-between">
- @switch (item.Inventory)
- {
- case null:
- break;
- case <= 0:
- <span text-translate="true">Sold out</span>
- break;
- default:
- <span>@StringLocalizer["{0} left", item.Inventory]</span>
- break;
- }
- @if (hasCount)
- {
- var count = vm.PerkCount[item.Id];
- <span>@count Contributor@(count == 1 ? "" : "s")</span>
- }
- @if (hasValue)
- {
- <span>@vm.PerkValue[item.Id] @vm.TargetCurrency total</span>
- }
- </div>
- }
- </div>
- }
- @if (vm.Started && !vm.Ended)
- {
- <div class="form-group">
- <label asp-for="Email" class="form-label"></label>
- <input asp-for="Email" type="email" class="form-control"/>
- <span asp-validation-for="Email" class="text-danger"></span>
- </div>
- <div class="form-group">
- <label asp-for="Amount" class="form-label"></label>
- <div class="input-group mb-3">
- <input inputmode="decimal" asp-for="Amount" type="number" step="any" class="form-control"/>
- <span class="input-group-text">@vm.TargetCurrency.ToUpperInvariant()</span>
- </div>
- <span asp-validation-for="Amount" class="text-danger"></span>
- </div>
- <input type="hidden" asp-for="RedirectToCheckout"/>
- <button type="submit" class="btn btn-primary" text-translate="true">Contribute</button>
- }
-</form>
diff --git a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml
deleted file mode 100644
index 9ecbc75..0000000
--- a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml
+++ /dev/null
@@ -1,390 +0,0 @@
-@model BTCPayServer.Plugins.Crowdfund.Models.ViewCrowdfundViewModel
-@using BTCPayServer.Plugins.Crowdfund.Models
-@inject BTCPayServer.Services.BTCPayServerEnvironment Env
-@inject BTCPayServer.Security.ContentSecurityPolicies Csp
-@{
- ViewData["Title"] = Model.Title;
- ViewData["StoreBranding"] = Model.StoreBranding;
- Layout = null;
- Csp.UnsafeEval();
- if (!string.IsNullOrEmpty(Model.DisqusShortname))
- {
- Csp.Add("script-src", $"https://{Model.DisqusShortname}.disqus.com");
- Csp.Add("script-src", "https://c.disquscdn.com");
- }
-}
-<!DOCTYPE html>
-<html lang="@Model.HtmlLang" class="h-100" @(Env.IsDeveloping ? " data-devenv" : "") id="Crowdfund-@Model.AppId">
-<head>
- <partial name="LayoutHead" />
- <link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet" />
- <link href="~/crowdfund/styles/main.css" asp-append-version="true" rel="stylesheet" />
- <style>
- #app { --wrap-max-width: 1320px; }
- #crowdfund-main-image {
- border-radius: var(--btcpay-border-radius);
- object-fit: cover;
- max-width: 100%;
- max-height: 40vh;
- }
- #crowdfund-body-description {
- font-size: 16px;
- }
- .perk.card .card-img-top{
- max-height: 210px;
- object-fit: scale-down;
- }
- </style>
- @this.Safe.Meta(Model.HtmlMetaTags)
- <vc:ui-extension-point location="crowdfund-head" model="@Model"/>
-</head>
-<body class="min-vh-100 p-2">
- @if (!Model.Enabled)
- {
- <div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert" text-translate="true">
- This crowdfund page is not publicly viewable!
- </div>
- }
- @if (Model.AnimationsEnabled)
- {
- <canvas id="fireworks" class="d-none"></canvas>
- }
-
- <div class="public-page-wrap" id="app" @(Model.SimpleDisplay ? "" : "v-cloak")>
- @if (!string.IsNullOrEmpty(Model.MainImageUrl))
- {
- <img v-if="srvModel.mainImageUrl" :src="srvModel.mainImageUrl" :alt="srvModel.title" id="crowdfund-main-image" asp-append-version="true"/>
- }
- <div class="d-flex flex-column justify-content-between p-3 text-center" id="crowdfund-header-container">
- <h1 class="mb-3">{{ srvModel.title }}</h1>
- @if (!string.IsNullOrEmpty(Model.Tagline))
- {
- <h2 class="h3 mb-3 fw-semibold" v-if="srvModel.tagline" v-text="srvModel.tagline"></h2>
- }
- @if (Model.TargetAmount.HasValue)
- {
- <span v-if="srvModel.targetAmount" class="mt-3" id="crowdfund-header-target">
- <h3 class="d-inline-block">
- <span class="badge bg-info px-3" v-text="`${targetAmount} ${targetCurrency}`">@Math.Round(Model.TargetAmount.GetValueOrDefault(0)) @Model.TargetCurrency</span>
- </h3>
- @if (Model.ResetEveryAmount > 0 && !Model.NeverReset)
- {
- <span v-if="srvModel.resetEvery !== 'Never'"
- class="h5 ms-2"
- v-b-tooltip
- :title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" text-translate="true">
- Dynamic
- </span>
- }
- @if (Model.EnforceTargetAmount)
- {
- <span v-if="srvModel.enforceTargetAmount" class="h5 ms-2" v-b-tooltip title=@StringLocalizer["No contributions allowed after the goal has been reached"] text-translate="true">
- Hardcap Goal
- </span>
- }
- else
- {
- <span v-if="!srvModel.enforceTargetAmount" class="h5 ms-2" v-b-tooltip title=@StringLocalizer["Contributions allowed even after goal is reached"] text-translate="true">
- Softcap Goal
- </span>
- }
- </span>
- }
- @if (!Model.Started && Model.StartDate.HasValue)
- {
- <h6 class="text-muted fst-italic mt-3" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" v-text="`Starts in ${startDiff}`" data-test="time-state">
- @StringLocalizer["Starts {0}", TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)]
- </h6>
- }
- else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
- {
- <h6 class="text-muted fst-italic mt-3" v-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" v-text="`Ends in ${endDiff}`" data-test="time-state">
- @StringLocalizer["Ends {0}", TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)]
- </h6>
- }
- else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
- {
- <h6 class="text-muted fst-italic mt-3" v-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" data-test="time-state" text-translate="true">
- Currently active!
- </h6>
- }
- </div>
-
- @if (Model.TargetAmount.HasValue)
- {
- <div class="progress rounded-pill" v-if="srvModel.targetAmount" id="crowdfund-progress-bar">
- <div class="progress-bar bg-primary"
- role="progressbar"
- style="width:@(Model.Info.ProgressPercentage + "%")"
- :aria-valuenow="srvModel.info.progressPercentage"
- v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
- aria-valuemin="0"
- id="crowdfund-progress-bar-confirmed-bar"
- v-b-tooltip
- :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% contributions'"
- aria-valuemax="100">
- </div>
- <div class="progress-bar bg-warning"
- role="progressbar"
- id="crowdfund-progress-bar-pending-bar"
- style="width:@(Model.Info.PendingProgressPercentage + "%")"
- :aria-valuenow="srvModel.info.pendingProgressPercentage"
- v-bind:style="{ width: srvModel.info.pendingProgressPercentage + '%' }"
- v-b-tooltip
- :title="parseFloat(srvModel.info.pendingProgressPercentage).toFixed(2) + '% contributions pending confirmation'"
- aria-valuemin="0"
- aria-valuemax="100">
- </div>
- </div>
- }
-
- <div class="row py-2 text-center crowdfund-stats">
- <div class="col-sm border-end p-3 text-center" id="crowdfund-body-raised-amount">
- <h3 v-text="`${raisedAmount} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount, Model.CurrencyData.Divisibility) @Model.TargetCurrency</h3>
- <h5 class="text-muted fst-italic mb-0">Raised</h5>
- <b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0" class="only-for-js">
- <ul class="p-0 text-uppercase">
- <li v-for="stat of paymentStats" class="list-unstyled">
- {{stat.label}} <span v-if="stat.lightning"><vc:icon symbol="lightning" /></span> {{stat.value}}
- </li>
- </ul>
- </b-tooltip>
- </div>
-
- <div class="col-sm border-end p-3 text-center" id="crowdfund-body-goal-raised">
- <h3 v-text="`${percentageRaisedAmount}%`">@Math.Round(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h3>
- <h5 class="text-muted fst-italic mb-0">Of Goal</h5>
- <b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'" class="only-for-js">
- Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
- </b-tooltip>
- </div>
-
- <div class="col-sm border-end p-3 text-center" id="crowdfund-body-total-contributors">
- <h3 v-text="new Intl.NumberFormat().format(srvModel.info.totalContributors)">@Model.Info.TotalContributors</h3>
- <h5 class="text-muted fst-italic mb-0" text-translate="true">Contributors</h5>
- </div>
-
- @if (Model.StartDate.HasValue || Model.EndDate.HasValue)
- {
- <div class="col-sm border-end p-3 text-center" id="crowdfund-body-campaign-dates">
- @if (!Model.Started && Model.StartDate.HasValue)
- {
- <div v-if="startDiff">
- <h3 v-text="startDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)</h3>
- <h5 class="text-muted fst-italic mb-0" v-text="'Left to start'" text-translate="true">Start Date</h5>
- </div>
- }
- else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
- {
- <div v-if="!startDiff && endDiff">
- <h3 v-text="endDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)</h3>
- <h5 class="text-muted fst-italic mb-0" v-text="'Left'" text-translate="true">End Date</h5>
- </div>
- }
- else if (Model.Ended)
- {
- <div v-if="ended">
- <h3 class="mb-0" text-translate="true">Campaign not active</h3>
- </div>
- }
- <b-tooltip v-if="startDate || endDate" target="crowdfund-body-campaign-dates" class="only-for-js">
- <ul class="p-0">
- @if (Model.StartDate.HasValue)
- {
- <li v-if="startDate" class="list-unstyled">
- {{started ? "Started" : "Starts"}} {{startDate}}
- </li>
- }
- @if (Model.EndDate.HasValue)
- {
- <li v-if="endDate" class="list-unstyled">
- {{ended ? "Ended" : "Ends"}} {{endDate}}
- </li>
- }
- </ul>
- </b-tooltip>
- </div>
- }
- </div>
-
- <div class="text-center mb-4" id="crowdfund-body-header">
- <button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary py-2 px-5 only-for-js" v-on:click="contribute" text-translate="true">Contribute</button>
- </div>
-
- <div class="row mt-4 justify-content-between gap-5">
- <div :class="{ 'col-lg-7 col-sm-12': hasPerks, 'col-12': !hasPerks }" id="crowdfund-body-description-container">
- <template v-if="srvModel.disqusEnabled && srvModel.disqusShortname">
- <b-tabs>
- <b-tab title="Details" active>
- <div class="overflow-hidden pt-3" v-html="srvModel.description" id="crowdfund-body-description">
- </div>
- </b-tab>
- <b-tab title="Discussion">
- <div id="disqus_thread" class="mt-4"></div>
- </b-tab>
- </b-tabs>
- </template>
- <template v-else>
- <div class="overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
- </div>
- </template>
- </div>
- <div class="col-lg-4 col-sm-12" id="crowdfund-body-contribution-container" v-if="hasPerks">
- <contribute :target-currency="srvModel.targetCurrency"
- :loading="loading"
- :display-perks-ranking="srvModel.displayPerksRanking"
- :perks-value="srvModel.perksValue"
- :active="active"
- :in-modal="false"
- :perks="perks">
- </contribute>
- </div>
- </div>
- <noscript v-pre>
- <div class="row justify-content-between">
- <div class="col-md-7 col-sm-12">
- <div class="overflow-hidden">@Safe.Raw(Model.Description)</div>
- </div>
- <div class="col-md-4 col-sm-12">
- <partial name="Crowdfund/Public/ContributeForm" model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })" />
- </div>
- </div>
- </noscript>
- <b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="@StringLocalizer["Close"]" ref="modalContribute">
- <contribute v-if="contributeModalOpen"
- :target-currency="srvModel.targetCurrency"
- :active="active"
- :perks="srvModel.perks"
- :loading="loading"
- :in-modal="true">
- </contribute>
- </b-modal>
- <footer class="store-footer">
- <p class="text-muted" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</p>
- <a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
- <span text-translate="true">Powered by</span> <partial name="_StoreFooterLogo" />
- </a>
- </footer>
- </div>
-
- <template id="perks-template">
- <div class="perks-container">
- <perk v-if="!perks || perks.length === 0"
- :perk="{title: 'Donate Custom Amount', priceType: 'Topup', price: { type: 'Topup' } }"
- :target-currency="targetCurrency"
- :active="active"
- :loading="loading"
- :in-modal="inModal">
- </perk>
- <perk v-for="(perk, index) in perks"
- :key="perk.id"
- :perk="perk"
- :target-currency="targetCurrency"
- :active="active"
- :display-perks-ranking="displayPerksRanking"
- :perks-value="perksValue"
- :index="index"
- :loading="loading"
- :in-modal="inModal">
- </perk>
- </div>
- </template>
- <template id="perk-template">
- <div class="card perk" v-bind:class="{ 'expanded': expanded, 'unexpanded': !expanded, 'mb-4':!inModal }" v-on:click="expand" :id="perk.id">
- <span v-if="displayPerksRanking && perk.sold"
- class="btn btn-sm rounded-circle px-0 perk-badge"
- v-bind:class="{ 'btn-primary': index==0, 'btn-light': index!=0}">
- #{{index+1}}
- </span>
- <div class="perk-zoom" v-if="canExpand">
- <div class="perk-zoom-bg"></div>
- <div class="perk-zoom-text w-100 py-2 px-4 text-center text-primary fw-semibold fs-5 lh-sm" text-translate="true">
- Select this contribution perk
- </div>
- </div>
- <form v-on:submit="onContributeFormSubmit" class="mb-0">
- <input type="hidden" :value="perk.id" id="choiceKey" />
- <img v-if="perk.image && perk.image != 'null'" class="card-img-top" :src="perk.image" />
- <div class="card-body">
- <div class="card-title d-flex justify-content-between" :class="{ 'mb-0': !perk.description }">
- <span class="h5" :class="{ 'mb-0': !perk.description }">{{perk.title ? perk.title : perk.id}}</span>
- <span class="text-muted">
- <template v-if="perk.priceType === 'Fixed' && amount == 0" text-translate="true">
- Free
- </template>
- <template v-else-if="amount">
- {{formatAmount(perk.price.noExponents(), srvModel.currencyData.divisibility)}}
- {{targetCurrency}}
- <template v-if="perk.price.type === 'Minimum'" text-translate="true">or more</template>
- </template>
- <template v-else-if="perk.priceType === 'Topup' || (!amount && perk.priceType === 'Minimum')" text-translate="true">
- Any amount
- </template>
- </span>
- </div>
- <p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
- <div class="input-group mt-3" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
- <template v-if="perk.priceType !== 'Topup' && !(perk.priceType === 'Fixed' && amount == 0)">
- <input type="number" class="form-control hide-number-spin"
- v-model="amount"
- :disabled="!active"
- :readonly="perk.priceType === 'Fixed'"
- :min="perk.price"
- step="any"
- placeholder="@StringLocalizer["Contribution Amount"]"
- required>
- <span class="input-group-text">{{targetCurrency}}</span>
- </template>
- <button class="btn btn-primary d-flex align-items-center"
- :class="{'btn-disabled': loading}"
- type="submit">
- <div v-if="loading" class="spinner-grow spinner-grow-sm me-2" role="status">
- <span class="visually-hidden" text-translate="true">Loading...</span>
- </div>
- {{perk.buyButtonText || 'Continue'}}
- </button>
- </div>
- </div>
- <div class="card-footer d-flex justify-content-between" v-if="perk.sold || perk.inventory != null">
- <span v-if="perk.inventory != null && perk.inventory > 0" class="text-center text-muted">{{new Intl.NumberFormat().format(perk.inventory)}} left</span>
- <span v-if="perk.inventory != null && perk.inventory <= 0" class="text-center text-muted">Sold out</span>
- <span v-if="perk.sold">{{new Intl.NumberFormat().format(perk.sold)}} Contributor{{perk.sold === 1 ? "": "s"}}</span>
- <span v-if="perk.value">{{formatAmount(perk.value, srvModel.currencyData.divisibility)}} {{targetCurrency}} total</span>
- </div>
- </form>
- </div>
- </template>
-
- <template id="contribute-template">
- <div>
- <h3 v-if="!inModal" class="mb-3" text-translate="true">Contribute</h3>
- <perks :perks="perks"
- :loading="loading"
- :in-modal="inModal"
- :display-perks-ranking="displayPerksRanking"
- :perks-value="perksValue"
- :target-currency="targetCurrency"
- :active="active">
- </perks>
- </div>
- </template>
-
- @if (!Model.SimpleDisplay)
- {
- <script>var srvModel = @Safe.Json(Model);</script>
- <script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
- <script src="~/vendor/moment/moment.min.js" asp-append-version="true"></script>
- <script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
- <script src="~/vendor/vue-toasted/vue-toasted.min.js" asp-append-version="true"></script>
- <script src="~/vendor/bootstrap-vue/bootstrap-vue.min.js" asp-append-version="true"></script>
- <script src="~/vendor/signalr/signalr.js" asp-append-version="true"></script>
- <script src="~/vendor/animejs/anime.min.js" asp-append-version="true"></script>
- <script src="~/crowdfund/app.js" asp-append-version="true"></script>
- <script src="~/crowdfund/services/audioplayer.js" asp-append-version="true"></script>
- <script src="~/crowdfund/services/fireworks.js" asp-append-version="true"></script>
- <script src="~/crowdfund/services/listener.js" asp-append-version="true"></script>
- <script src="~/modal/btcpay.js" asp-append-version="true"></script>
- }
-<partial name="LayoutFoot"/>
-</body>
-</html>
diff --git a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml
deleted file mode 100644
index 462b890..0000000
--- a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml
+++ /dev/null
@@ -1,392 +0,0 @@
-@using System.Globalization
-@using BTCPayServer.Abstractions.Contracts
-@using BTCPayServer.Client
-@using BTCPayServer.TagHelpers
-@using BTCPayServer.Views.Apps
-@using Microsoft.AspNetCore.Mvc.TagHelpers
-@using BTCPayServer.Forms
-@using BTCPayServer.Plugins.Crowdfund
-@inject FormDataService FormDataService
-@inject IFileService FileService
-@inject BTCPayServer.Security.ContentSecurityPolicies Csp
-@model BTCPayServer.Plugins.Crowdfund.Models.UpdateCrowdfundViewModel
-@{
- // layout-menu-item="@nameof(CrowdfundPlugin)-@app.Id"
- ViewData.SetLayoutModel(new LayoutModel($"{nameof(CrowdfundPlugin)}-{Model.AppId}", StringLocalizer["Update Crowdfund"]));
- Csp.UnsafeEval();
- var canUpload = await FileService.IsAvailable();
- var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
-}
-
-@section PageHeadContent {
- <link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true" />
- <style>.flatpickr-wrapper { flex-grow: 1; }</style>
-}
-
-@section PageFootContent {
- <partial name="_ValidationScriptsPartial" />
- <script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
- <script src="~/crowdfund/admin.js" asp-append-version="true"></script>
-}
-
-<form method="post" enctype="multipart/form-data" permissioned="@Policies.CanModifyStoreSettings">
- <div class="sticky-header">
- <h2>@ViewData["Title"]</h2>
- <div>
- <button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
- <a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
- @if (Model.Archived)
- {
- <button type="submit" class="btn btn-outline-secondary" name="Archived" value="False" text-translate="true">Unarchive</button>
- }
- else if (Model.ModelWithMinimumData)
- {
- <a class="btn btn-secondary" asp-controller="UICrowdfund" asp-action="ViewCrowdfund" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank" text-translate="true">View</a>
- }
- </div>
- </div>
-
- <partial name="_StatusMessage" />
-
- <input type="hidden" asp-for="StoreId" />
- <input type="hidden" asp-for="Archived" />
-
- @if (!ViewContext.ModelState.IsValid)
- {
- <div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
- }
- <div class="row" style="max-width:540px;">
- <div class="col-sm-6">
- <div class="form-group">
- <label asp-for="AppName" class="form-label" data-required></label>
- <input asp-for="AppName" class="form-control" required />
- <span asp-validation-for="AppName" class="text-danger"></span>
- </div>
- </div>
- <div class="col-sm-6">
- <div class="form-group">
- <label asp-for="Title" class="form-label" data-required></label>
- <input asp-for="Title" class="form-control" required />
- <span asp-validation-for="Title" class="text-danger"></span>
- </div>
- </div>
- <div class="form-group">
- <label asp-for="Tagline" class="form-label"></label>
- <input asp-for="Tagline" class="form-control" />
- <span asp-validation-for="Tagline" class="text-danger"></span>
- </div>
- <div class="form-group">
- <div class="d-flex align-items-center justify-content-between gap-2">
- <label asp-for="MainImageFile" class="form-label"></label>
- @if (!string.IsNullOrEmpty(Model.MainImageUrl))
- {
- <button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
- <vc:icon symbol="cross" /> <span text-translate="true">Remove</span>
- </button>
- }
- </div>
- @if (canUpload)
- {
- <div class="d-flex align-items-center gap-3">
- <input asp-for="MainImageFile" type="file" class="form-control flex-grow">
- @if (!string.IsNullOrEmpty(Model.MainImageUrl))
- {
- <img src="@Model.MainImageUrl" alt="Logo" style="height:2.1rem;max-width:10.5rem;" />
- }
- </div>
- <span asp-validation-for="MainImageFile" class="text-danger"></span>
- }
- else
- {
- <input asp-for="MainImageFile" type="file" class="form-control" disabled>
- <div class="form-text">@ViewLocalizer["In order to upload, a {0} must be configured.", Html.ActionLink(StringLocalizer["file storage"], "Files", "UIServer")]</div>
- }
- </div>
- <div class="form-group">
- <div class="d-flex align-items-center">
- <input asp-for="Enabled" type="checkbox" class="btcpay-toggle me-3"/>
- <div>
- <label asp-for="Enabled" class="form-check-label"></label>
- <span asp-validation-for="Enabled" class="text-danger"></span>
- <div class="text-muted" text-translate="true">The crowdfund will be visible to anyone.</div>
- </div>
- </div>
- </div>
- </div>
- <div class="row mt-3">
- <div class="col-xxl-constrain">
- <div class="form-group">
- <label asp-for="Description" class="form-label" data-required></label>
- <textarea asp-for="Description" rows="20" cols="40" class="form-control richtext"></textarea>
- <span asp-validation-for="Description" class="text-danger"></span>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="col-xl-10 col-xxl-constrain">
- <h3 class="mt-5 mb-4">Goal</h3>
- <div class="d-flex flex-wrap gap-3 mb-3">
- <div class="form-group w-250px mb-0">
- <label asp-for="TargetAmount" class="form-label"></label>
- <input inputmode="decimal" asp-for="TargetAmount" class="form-control" />
- <span asp-validation-for="TargetAmount" class="text-danger"></span>
- </div>
- <div class="form-group">
- <label asp-for="TargetCurrency" class="form-label"></label>
- <input asp-for="TargetCurrency" class="form-control w-auto" currency-selection />
- <div class="form-text">@StringLocalizer["Uses the store's default currency ({0}) if empty.", @Model.StoreDefaultCurrency]</div>
- <span asp-validation-for="TargetCurrency" class="text-danger"></span>
- </div>
- </div>
- <div class="d-flex flex-wrap gap-3 align-items-center mb-4">
- <div class="form-group mb-0 w-250px">
- <label asp-for="StartDate" class="form-label"></label>
- <div class="input-group flex-nowrap">
- <input type="datetime-local" asp-for="StartDate"
- value="@(Model.StartDate?.ToString("u", CultureInfo.InvariantCulture))"
- class="form-control flatdtpicker"
- placeholder="@StringLocalizer["No start date has been set"]" />
- <button class="btn btn-secondary input-group-clear px-3" type="button" title="Clear">
- <vc:icon symbol="close"/>
- </button>
- </div>
- </div>
- <div class="form-group mb-0 w-250px">
- <label asp-for="EndDate" class="form-label"></label>
- <div class="input-group flex-nowrap">
- <input type="datetime-local" asp-for="EndDate"
- value="@(Model.EndDate?.ToString("u", CultureInfo.InvariantCulture))"
- class="form-control flatdtpicker"
- placeholder="@StringLocalizer["No end date has been set"]" />
- <button class="btn btn-secondary input-group-clear px-3" type="button" title="Clear">
- <vc:icon symbol="close"/>
- </button>
- </div>
- </div>
- <span asp-validation-for="StartDate" class="text-danger"></span>
- <span asp-validation-for="EndDate" class="text-danger"></span>
- </div>
-
- <div class="form-group mt-4" id="ResetRow" hidden="@(Model.StartDate == null)">
- <div class="d-flex align-items-center mb-3">
- <input asp-for="IsRecurring" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#ResetEverySettings" aria-expanded="@(Model.IsRecurring)" aria-controls="ResetEverySettings" />
- <div>
- <label asp-for="IsRecurring" class="form-check-label" text-translate="true">Recurring Goal</label>
- <span asp-validation-for="IsRecurring" class="text-danger"></span>
- <div class="text-muted" text-translate="true">Reset goal after a specific period of time, based on your crowdfund's start date.</div>
- </div>
- </div>
-
- <div class="collapse @(Model.IsRecurring ? "show" : "")" id="ResetEverySettings">
- <div class="form-group mb-0 pt-2 w-250px">
- <label asp-for="ResetEveryAmount" class="form-label"></label>
- <div class="d-flex align-items-center">
- <input type="number" inputmode="numeric" asp-for="ResetEveryAmount" placeholder="@StringLocalizer["Amount"]" class="form-control me-3" min="0">
- <select class="form-select w-auto" asp-for="ResetEvery">
- @foreach (var opt in Model.ResetEveryValues)
- {
- <option value="@opt">@opt</option>
- }
- </select>
- </div>
- <span asp-validation-for="ResetEveryAmount" class="text-danger"></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div id="perks" class="row">
- <div class="col-xxl-constrain">
- <partial name="TemplateEditor" model="@(nameof(Model.PerksTemplate), Model.PerksTemplate, "Perks", Model.TargetCurrency ?? Model.StoreDefaultCurrency)" />
- </div>
- </div>
- <div class="row">
- <div class="col-xl-8 col-xxl-constrain">
- <h3 class="mt-5 mb-4" text-translate="true">Contributions</h3>
- <div class="d-flex mb-3">
- <input asp-for="SortPerksByPopularity" type="checkbox" class="btcpay-toggle me-3" />
- <label asp-for="SortPerksByPopularity" class="form-check-label"></label>
- <span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
- </div>
- <div class="d-flex mb-3">
- <input asp-for="DisplayPerksRanking" type="checkbox" class="btcpay-toggle me-3" />
- <label asp-for="DisplayPerksRanking" class="form-check-label"></label>
- <span asp-validation-for="DisplayPerksRanking" class="text-danger"></span>
- </div>
- <div class="d-flex mb-3">
- <input asp-for="DisplayPerksValue" type="checkbox" class="btcpay-toggle me-3" />
- <label asp-for="DisplayPerksValue" class="form-check-label"></label>
- <span asp-validation-for="DisplayPerksValue" class="text-danger"></span>
- </div>
- <div class="d-flex mb-3">
- <input asp-for="EnforceTargetAmount" type="checkbox" class="btcpay-toggle me-3" />
- <label asp-for="EnforceTargetAmount" class="form-check-label"></label>
- <span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
- </div>
-
- <h3 class="mt-5 mb-4" text-translate="true">Crowdfund Behavior</h3>
- <div class="d-flex">
- <input asp-for="UseAllStoreInvoices" type="checkbox" class="btcpay-toggle me-3" />
- <label asp-for="UseAllStoreInvoices" class="form-check-label"></label>
- <span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
- </div>
-
- <h3 class="mt-5 mb-4" text-translate="true">Checkout</h3>
- <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>
-
- <h3 class="mt-5 mb-2" text-translate="true">Additional Options</h3>
- <div class="form-group">
- <div class="accordion" id="additional">
-
- <div class="accordion-item">
- <h2 class="accordion-header" id="additional-htmlheader-header">
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-htmlheader" aria-expanded="false" aria-controls="additional-htmlheader">
- <span text-translate="true">HTML Headers</span>
- <vc:icon symbol="caret-down" />
- </button>
- </h2>
- <div id="additional-htmlheader" class="accordion-collapse collapse" aria-labelledby="additional-htmlheader-header">
- <div class="accordion-body">
- <div class="form-group">
- <label asp-for="Language" class="form-label"></label>
- <input asp-for="Language" class="form-control" maxlength="2" required />
- <div class="form-text">Fix the HTML page language</div>
- <span asp-validation-for="Language" class="text-danger"></span>
- </div>
- <div class="form-group">
- <label asp-for="HtmlMetaTags" class="form-label"></label>
- <textarea asp-for="HtmlMetaTags" rows="5" cols="40" class="form-control"
- placeholder='<meta name="description" content="Your description">
-<meta name="keywords" content="keyword1, keyword2, keyword3">
-<meta name="author" content="John Doe">
-Please insert valid HTML here. Only meta tags accepted.'>
- </textarea>
- <span asp-validation-for="HtmlMetaTags" class="text-danger"></span>
- </div>
- </div>
- </div>
- </div>
-
- <div class="accordion-item">
- <h2 class="accordion-header" id="additional-sound-header">
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-sound" aria-expanded="false" aria-controls="additional-sound">
- <span text-translate="true">Sound</span>
- <vc:icon symbol="caret-down" />
- </button>
- </h2>
- <div id="additional-sound" class="accordion-collapse collapse" aria-labelledby="additional-sound-header">
- <div class="accordion-body">
- <div class="form-group mb-0">
- <div class="d-flex align-items-center">
- <input asp-for="SoundsEnabled" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SoundsEnabledSettings" aria-expanded="@Model.SoundsEnabled" aria-controls="SoundsEnabledSettings"/>
- <label asp-for="SoundsEnabled" class="form-check-label"></label>
- <span asp-validation-for="SoundsEnabled" class="text-danger"></span>
- </div>
- </div>
- <div class="collapse @(Model.SoundsEnabled ? "show" : "")" id="SoundsEnabledSettings">
- <div class="form-group mb-0 pt-3">
- <label asp-for="Sounds" class="form-label"></label>
- <textarea asp-for="Sounds" class="form-control" rows="5"></textarea>
- <span asp-validation-for="Sounds" class="text-danger"></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="accordion-item">
- <h2 class="accordion-header" id="additional-animation-header">
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-animation" aria-expanded="false" aria-controls="additional-animation">
- <span text-translate="true">Animation</span>
- <vc:icon symbol="caret-down" />
- </button>
- </h2>
- <div id="additional-animation" class="accordion-collapse collapse" aria-labelledby="additional-animation-header">
- <div class="accordion-body">
- <div class="form-group mb-0">
- <div class="d-flex align-items-center">
- <input asp-for="AnimationsEnabled" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#AnimationsEnabledSettings" aria-expanded="@Model.AnimationsEnabled" aria-controls="AnimationsEnabledSettings"/>
- <label asp-for="AnimationsEnabled" class="form-check-label"></label>
- <span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
- </div>
- </div>
- <div class="collapse @(Model.AnimationsEnabled ? "show" : "")" id="AnimationsEnabledSettings">
- <div class="form-group mb-0 pt-3">
- <label asp-for="AnimationColors" class="form-label"></label>
- <textarea asp-for="AnimationColors" class="form-control" rows="5"></textarea>
- <span asp-validation-for="AnimationColors" class="text-danger"></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="accordion-item">
- <h2 class="accordion-header" id="additional-discussion-header">
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-discussion" aria-expanded="false" aria-controls="additional-discussion">
- <span text-translate="true">Discussion</span>
- <vc:icon symbol="caret-down" />
- </button>
- </h2>
- <div id="additional-discussion" class="accordion-collapse collapse" aria-labelledby="additional-discussion-header">
- <div class="accordion-body">
- <div class="form-group mb-0">
- <div class="d-flex align-items-center">
- <input asp-for="DisqusEnabled" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#DisqusEnabledSettings" aria-expanded="@Model.DisqusEnabled" aria-controls="DisqusEnabledSettings"/>
- <label asp-for="DisqusEnabled" class="form-check-label"></label>
- <span asp-validation-for="DisqusEnabled" class="text-danger"></span>
- </div>
- </div>
- <div class="collapse @(Model.DisqusEnabled ? "show" : "")" id="DisqusEnabledSettings">
- <div class="form-group mb-0 pt-3">
- <label asp-for="DisqusShortname" class="form-label"></label>
- <input asp-for="DisqusShortname" class="form-control" />
- <span asp-validation-for="DisqusShortname" class="text-danger"></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="accordion-item">
- <h2 class="accordion-header" id="additional-notification-header">
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notification" aria-expanded="false" aria-controls="additional-notification">
- <span text-translate="true">Notification URL Callbacks</span>
- <vc:icon symbol="caret-down" />
- </button>
- </h2>
- <div id="additional-notification" class="accordion-collapse collapse" aria-labelledby="additional-notification-header">
- <div class="accordion-body">
- <div class="form-group">
- <label asp-for="NotificationUrl" class="form-label"></label>
- <input asp-for="NotificationUrl" class="form-control" />
- <span asp-validation-for="NotificationUrl" class="text-danger"></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-</form>
-
-<div class="d-grid d-sm-flex flex-wrap gap-3 mt-3">
- <form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@Model.AppId" permission="@Policies.CanModifyStoreSettings">
- <button type="submit" class="w-100 btn btn-outline-secondary" id="btn-archive-toggle">
- @if (Model.Archived)
- {
- <span class="text-nowrap">Unarchive this app</span>
- }
- else
- {
- <span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this app so that it does not appear in the apps list by default">Archive this app</span>
- }
- </button>
- </form>
- <a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@Model.AppId" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(Model.AppName)</strong> and its settings will be permanently deleted." data-confirm-input="@StringLocalizer["Delete"]" permission="@Policies.CanModifyStoreSettings">Delete this app</a>
-</div>
-
-<partial name="_Confirm" model="@(new ConfirmModel(StringLocalizer["Delete app"], StringLocalizer["This app will be removed from this store."], StringLocalizer["Delete"]))" permission="@Policies.CanModifyStoreSettings" />
-
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.