Crowdfund : Add Buyer information / Additional information(forms) like POS (#5659)

* Crowfund : Add Buyer information / Additional information(forms) like POS

* PR 5659 - changes

* Cleanups

* fix perk

* Crowdfund form tests

* Add Selenium test for Crowfund

* Selenium update

* update Selenium

* selenium update

* update selenium

* Test fixes and view improvements

* Cleanups

* do not use hacky form element for form detection

---------

Co-authored-by: nisaba <infos@nisaba.solutions>
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: Kukks <evilkukka@gmail.com>
This commit is contained in:
Nisaba
2024-02-21 13:41:21 +00:00
committed by GitHub
parent 4943c84655
commit 04037b3d2d
13 changed files with 944 additions and 619 deletions

1
.gitignore vendored
View File

@@ -300,3 +300,4 @@ Plugins/packed
BTCPayServer/wwwroot/swagger/v1/openapi.json
BTCPayServer/appsettings.dev.json
BTCPayServer.Tests/monero_wallet
/BTCPayServer.Tests/NewBlocks.bat

View File

@@ -1,15 +1,19 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Forms;
using BTCPayServer.Forms.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
@@ -303,5 +307,114 @@ namespace BTCPayServer.Tests
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CrowdfundWithFormNoPerk()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var frmService = tester.PayTester.GetService<FormDataService>();
var appService = tester.PayTester.GetService<AppService>();
var crowdfund = user.GetController<UICrowdfundController>();
var apps = user.GetController<UIAppsController>();
var appData = new AppData { StoreDataId = user.StoreId, Name = "test", AppType = CrowdfundAppType.AppType };
await appService.UpdateOrCreateApp(appData);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(appData);
crowdfund.HttpContext.SetAppData(appData);
var form = new Form
{
Fields =
[
Field.Create("Enter your email", "item1", "test@toto.com", true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Item3", "invoice_item3", 3.ToString(), true, null)
]
};
var frmData = new FormData
{
StoreId = user.StoreId,
Name = "frmTest",
Config = form.ToString()
};
await frmService.AddOrUpdateForm(frmData);
var lstForms = await frmService.GetForms(user.StoreId);
Assert.NotEmpty(lstForms);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.FormId = lstForms[0].Id;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
Assert.IsNotType<NotFoundObjectResult>(res);
Assert.IsNotType<BadRequest>(res);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CrowdfundWithFormAndPerk()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var frmService = tester.PayTester.GetService<FormDataService>();
var appService = tester.PayTester.GetService<AppService>();
var crowdfund = user.GetController<UICrowdfundController>();
var apps = user.GetController<UIAppsController>();
var appData = new AppData { StoreDataId = user.StoreId, Name = "test", AppType = CrowdfundAppType.AppType };
await appService.UpdateOrCreateApp(appData);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var app = appList.Apps[0];
apps.HttpContext.SetAppData(appData);
crowdfund.HttpContext.SetAppData(appData);
var form = new Form
{
Fields =
[
Field.Create("Enter your email", "item1", "test@toto.com", true, null, "email"),
Field.Create("Name", "item2", 2.ToString(), true, null),
Field.Create("Item3", "invoice_item3", 3.ToString(), true, null)
]
};
var frmData = new FormData
{
StoreId = user.StoreId,
Name = "frmTest",
Config = form.ToString()
};
await frmService.AddOrUpdateForm(frmData);
var lstForms = await frmService.GetForms(user.StoreId);
Assert.NotEmpty(lstForms);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.FormId = lstForms[0].Id;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);
Assert.IsNotType<NotFoundObjectResult>(res);
Assert.IsNotType<BadRequest>(res);
}
}
}

View File

@@ -1292,20 +1292,22 @@ namespace BTCPayServer.Tests
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
var editUrl = s.Driver.Url;
var appId = editUrl.Split('/')[4];
// CHeck public page
// Check public page
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
var cfUrl = s.Driver.Url;
Assert.Equal("Currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
Assert.Equal("Currently active!", s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
// Contribute
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
TestUtils.Eventually(() =>
{
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
@@ -1313,13 +1315,10 @@ namespace BTCPayServer.Tests
var iframe = s.Driver.SwitchTo().Frame(frameElement);
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
closebutton = iframe.FindElement(By.Id("close"));
Assert.True(closebutton.Displayed);
var closeButton = iframe.FindElement(By.Id("close"));
Assert.True(closeButton.Displayed);
closeButton.Click();
});
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
// Back to admin view
@@ -1343,6 +1342,56 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
// Crowdfund with form
s.GoToUrl(editUrl);
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("test-without-perk@crowdfund.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
s.PayInvoice(true, 10);
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToInvoice(invoiceId);
Assert.Contains("test-without-perk@crowdfund.com", s.Driver.PageSource);
// Crowdfund with perk
s.GoToUrl(editUrl);
s.Driver.ScrollTo(By.Id("btAddItem"));
s.Driver.FindElement(By.Id("btAddItem")).Click();
s.Driver.FindElement(By.Id("EditorTitle")).SendKeys("Perk 1");
s.Driver.FindElement(By.Id("EditorId")).SendKeys("Perk-1");
s.Driver.FindElement(By.Id("EditorAmount")).SendKeys("20");
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.WaitForElement(By.Id("Perk-1")).Click();
s.Driver.WaitForElement(By.CssSelector("#Perk-1 button[type=\"submit\"]")).Submit();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("test-with-perk@crowdfund.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
s.PayInvoice(true, 20);
invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToInvoice(invoiceId);
Assert.Contains("test-with-perk@crowdfund.com", s.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]

View File

@@ -5,11 +5,15 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.Forms.Models;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
@@ -20,7 +24,10 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using NicolasDorier.RateLimits;
using CrowdfundResetEvery = BTCPayServer.Services.Apps.CrowdfundResetEvery;
@@ -37,6 +44,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
StoreRepository storeRepository,
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
FormDataService formDataService,
CrowdfundAppType app)
{
_currencies = currencies;
@@ -46,6 +54,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
_storeRepository = storeRepository;
_eventAggregator = eventAggregator;
_invoiceController = invoiceController;
FormDataService = formDataService;
}
private readonly EventAggregator _eventAggregator;
@@ -55,6 +64,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
private readonly UIInvoiceController _invoiceController;
private readonly UserManager<ApplicationUser> _userManager;
private readonly CrowdfundAppType _app;
public FormDataService FormDataService { get; }
[HttpGet("/")]
[HttpGet("/apps/{appId}/crowdfund")]
@@ -95,7 +105,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(CrowdfundAppType.AppType)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, string formResponse = null, CancellationToken cancellationToken = default)
{
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, true);
@@ -121,6 +131,24 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
return NotFound("Crowdfund is not currently active");
}
JObject formResponseJObject = null;
if (settings.FormId is not null)
{
var formData = await FormDataService.GetForm(settings.FormId);
if (formData is not null)
{
formResponseJObject = TryParseJObject(formResponse) ?? new JObject();
var form = Form.Parse(formData.Config);
FormDataService.SetValues(form, formResponseJObject);
if (!FormDataService.Validate(form, ModelState))
{
// someone tried to bypass validation
return RedirectToAction(nameof(ViewCrowdfund), new { appId });
}
}
}
var store = await _appService.GetStore(app);
var title = settings.Title;
decimal? price = request.Amount;
@@ -203,6 +231,11 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
entity.FullNotifications = true;
entity.ExtendedNotifications = true;
entity.Metadata.OrderUrl = appUrl;
if (formResponseJObject is null)
return;
var meta = entity.Metadata.ToJObject();
meta.Merge(formResponseJObject);
entity.Metadata = InvoiceMetadata.FromJObject(meta);
});
if (request.RedirectToCheckout)
@@ -219,6 +252,108 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
}
}
private JObject TryParseJObject(string posData)
{
try
{
return JObject.Parse(posData);
}
catch
{
}
return null;
}
[HttpGet("/apps/{appId}/crowdfund/form")]
[IgnoreAntiforgeryToken]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> CrowdfundForm(string appId, decimal? amount=0, string choiceKey="")
{
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var formData = await FormDataService.GetForm(settings.FormId);
if (formData is null)
{
return RedirectToAction(nameof(ViewCrowdfund), new { appId });
}
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 storeBlob = store.GetStoreBlob();
var form = Form.Parse(formData.Config);
form.ApplyValuesFromForm(Request.Query);
var vm = new FormViewModel
{
StoreName = store.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob),
FormName = formData.Name,
Form = form,
AspController = controller,
AspAction = nameof(CrowdfundFormSubmit),
RouteParameters = new Dictionary<string, string> { { "appId", appId }, { "amount", amount.ToString() }, { "choiceKey", choiceKey } },
FormParameters = formParameters,
FormParameterPrefix = prefix
};
return View("Views/UIForms/View", vm);
}
[HttpPost("/apps/{appId}/crowdfund/form/submit")]
[IgnoreAntiforgeryToken]
[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);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var formData = await FormDataService.GetForm(settings.FormId);
if (formData is null)
{
return RedirectToAction(nameof(ViewCrowdfund));
}
var form = Form.Parse(formData.Config);
var formFieldNames = form.GetAllFields().Select(tuple => tuple.FullName).Distinct().ToArray();
// For unit testing
if (Request.Headers.Count == 1)
{
Request.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
}
var formParameters = Request.Form
.Where(pair => pair.Key.StartsWith(viewModel.FormParameterPrefix))
.ToDictionary(pair => pair.Key.Replace(viewModel.FormParameterPrefix, string.Empty), pair => pair.Value)
.ToMultiValueDictionary(p => p.Key, p => p.Value.ToString());
form.ApplyValuesFromForm(Request.Form.Where(pair => formFieldNames.Contains(pair.Key)));
if (FormDataService.Validate(form, ModelState))
{
var appInfo = await GetAppInfo(appId);
var req = new ContributeToCrowdfund()
{
RedirectToCheckout = true,
Amount = amount == 0 ? null : amount,
ChoiceKey = choiceKey,
ViewCrowdfundViewModel = appInfo
};
return ContributeToCrowdfund(appId, req, formResponse: FormDataService.GetValues(form).ToString()).Result;
}
viewModel.FormName = formData.Name;
viewModel.Form = form;
viewModel.FormParameters = formParameters;
return View("Views/UIForms/View", viewModel);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
@@ -264,7 +399,8 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
DisplayPerksValue = settings.DisplayPerksValue,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors),
FormId = settings.FormId
};
return View("Crowdfund/UpdateCrowdfund", vm);
}
@@ -373,7 +509,8 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
AnimationColors = parsedAnimationColors,
FormId = vm.FormId
};
app.TagAllInvoices = vm.UseAllStoreInvoices;

View File

@@ -11,7 +11,6 @@ using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
@@ -183,6 +182,10 @@ namespace BTCPayServer.Plugins.Crowdfund
CustomCSSLink = settings.CustomCSSLink,
EmbeddedCSS = settings.EmbeddedCSS
};
var formUrl = settings.FormId != null
? _linkGenerator.GetPathByAction(nameof(UICrowdfundController.CrowdfundForm), "UICrowdfund",
new { appId = appData.Id }, _options.Value.RootPath)
: null;
return new ViewCrowdfundViewModel
{
Title = settings.Title,
@@ -210,6 +213,7 @@ namespace BTCPayServer.Plugins.Crowdfund
PerkCount = perkCount,
PerkValue = perkValue,
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
FormUrl = formUrl,
Sounds = settings.Sounds,
AnimationColors = settings.AnimationColors,
CurrencyData = _currencyNameTable.GetCurrencyData(settings.TargetCurrency, true),

View File

@@ -117,6 +117,10 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
// NOTE: Improve validation if needed
public bool ModelWithMinimumData => Description != null && Title != null && TargetCurrency != null;
[Display(Name = "Request contributor data on checkout")]
public string FormId { get; set; }
public bool Archived { get; set; }
}
}

View File

@@ -36,7 +36,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
public string[] Sounds { get; set; }
public int ResetEveryAmount { get; set; }
public bool NeverReset { get; set; }
public string FormUrl { get; set; }
public Dictionary<string, int> PerkCount { get; set; }
public CurrencyData CurrencyData { get; set; }

View File

@@ -46,7 +46,7 @@ namespace BTCPayServer.Services.Apps
{
var result =
await _crowdfundController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, Context.ConnectionAborted);
await _crowdfundController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, cancellationToken: Context.ConnectionAborted);
switch (result)
{
case OkObjectResult okObjectResult:

View File

@@ -45,6 +45,9 @@ namespace BTCPayServer.Services.Apps
public bool DisplayPerksRanking { get; set; }
public bool DisplayPerksValue { get; set; }
public bool SortPerksByPopularity { get; set; }
public string FormId { get; set; } = null;
public string[] AnimationColors { get; set; } =
{
"#FF6138", "#FFBE53", "#2980B9", "#282741"

View File

@@ -16,9 +16,9 @@
<!DOCTYPE html>
<html class="h-100" @(Env.IsDeveloping ? " data-devenv" : "")>
<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"/>
<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 {
@@ -38,18 +38,18 @@
<vc:ui-extension-point location="crowdfund-head" model="@Model"/>
</head>
<body class="min-vh-100 p-2">
@if (!Model.Enabled)
{
@if (!Model.Enabled)
{
<div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert">
This crowdfund page is not publically viewable!
</div>
}
@if (Model.AnimationsEnabled)
{
}
@if (Model.AnimationsEnabled)
{
<canvas id="fireworks" class="d-none"></canvas>
}
}
<div class="public-page-wrap" id="app" @(Model.SimpleDisplay ? "" : "v-cloak")>
<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"/>
@@ -267,9 +267,9 @@
Powered by <partial name="_StoreFooterLogo" />
</a>
</footer>
</div>
</div>
<template id="perks-template">
<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' } }"
@@ -290,8 +290,8 @@
:in-modal="inModal">
</perk>
</div>
</template>
<template id="perk-template">
</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"
@@ -305,8 +305,8 @@
</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"/>
<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>
@@ -327,25 +327,20 @@
</span>
</div>
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
<div class="input-group" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
<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
<input type="number" class="form-control hide-number-spin"
v-model="amount"
:disabled="!active"
:readonly="perk.priceType === 'Fixed'"
class="form-control hide-number-spin"
type="number"
v-model="amount"
:min="perk.price"
step="any"
placeholder="Contribution Amount"
required>
<span class="input-group-text">{{targetCurrency}}</span>
</template>
<button
class="btn btn-primary d-flex align-items-center "
v-bind:class="{ 'btn-disabled': loading}"
:disabled="!active || loading"
<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">Loading...</span>
@@ -362,13 +357,12 @@
</div>
</form>
</div>
</template>
</template>
<template id="contribute-template">
<template id="contribute-template">
<div>
<h3 v-if="!inModal" class="mb-3">Contribute</h3>
<perks
:perks="perks"
<perks :perks="perks"
:loading="loading"
:in-modal="inModal"
:display-perks-ranking="displayPerksRanking"
@@ -377,10 +371,10 @@
:active="active">
</perks>
</div>
</template>
</template>
@if (!Model.SimpleDisplay)
{
@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>
@@ -394,7 +388,7 @@
<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>

View File

@@ -4,11 +4,14 @@
@using BTCPayServer.TagHelpers
@using BTCPayServer.Views.Apps
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Forms
@inject FormDataService FormDataService
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@model BTCPayServer.Plugins.Crowdfund.Models.UpdateCrowdfundViewModel
@{
ViewData.SetActivePage(AppsNavPages.Update, "Update Crowdfund", Model.AppId);
Csp.UnsafeEval();
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
}
@section PageHeadContent {
@@ -202,6 +205,13 @@
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
</div>
<h3 class="mt-5 mb-4">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">Additional Options</h3>
<div class="form-group">
<div class="accordion" id="additional">

View File

@@ -109,7 +109,7 @@
</button>
</div>
</div>
<button type="button" class="btn btn-link py-0 px-2 mt-2 mb-2 gap-1 add fw-semibold d-inline-flex align-items-center" v-on:click.stop="$emit('add-item', $event)">
<button type="button" id="btAddItem" class="btn btn-link py-0 px-2 mt-2 mb-2 gap-1 add fw-semibold d-inline-flex align-items-center" v-on:click.stop="$emit('add-item', $event)">
<vc:icon symbol="new" />
Add Item
</button>

View File

@@ -41,8 +41,13 @@ document.addEventListener("DOMContentLoaded",function (ev) {
if (!this.active || this.loading){
return;
}
eventAggregator.$emit("contribute", {amount: parseFloat(this.amount), choiceKey: this.perk.id});
const formUrl = this.$root.srvModel.formUrl;
if (formUrl) {
location.href = formUrl + "?amount=" + this.amount + "&choiceKey=" + this.perk.id;
return;
} else {
eventAggregator.$emit("contribute", { amount: parseFloat(this.amount), choiceKey: this.perk.id });
}
},
expand: function(){
if(this.canExpand){
@@ -68,10 +73,10 @@ document.addEventListener("DOMContentLoaded",function (ev) {
this.setAmount(newValue.price);
}
}
}
}
});
app = new Vue({
app = new Vue({
el: '#app',
data: function(){
return {
@@ -221,10 +226,15 @@ document.addEventListener("DOMContentLoaded",function (ev) {
contribute() {
if (!this.active || this.loading) return;
if (this.hasPerks){
if (this.hasPerks) {
this.contributeModalOpen = true
} else {
eventAggregator.$emit("contribute", {amount: null, choiceKey: null});
if (this.srvModel.formUrl) {
window.location.href = this.srvModel.formUrl;
return;
} else {
eventAggregator.$emit("contribute", { amount: null, choiceKey: null });
}
}
}
},
@@ -267,34 +277,34 @@ document.addEventListener("DOMContentLoaded",function (ev) {
type: "error",
position: "top-center",
duration: 10000
} );
});
});
eventAggregator.$on("payment-received", function (amount, cryptoCode, type) {
var onChain = type.toLowerCase() !== "lightninglike";
if(self.sound) {
if (self.sound) {
playRandomSound();
}
if(self.animation) {
if (self.animation) {
fireworks();
}
amount = parseFloat(amount).noExponents();
if(onChain){
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
if (onChain) {
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
iconPack: "fontawesome",
icon: "plus",
duration: 10000
} );
}else{
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
});
} else {
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
iconPack: "fontawesome",
icon: "bolt",
duration: 10000
} );
});
}
});
if(srvModel.disqusEnabled){
if (srvModel.disqusEnabled) {
window.disqus_config = function () {
// Replace PAGE_URL with your page's canonical URL variable
this.page.url = window.location.href;
@@ -303,18 +313,18 @@ document.addEventListener("DOMContentLoaded",function (ev) {
this.page.identifier = self.srvModel.appId;
};
(function() { // REQUIRED CONFIGURATION VARIABLE: EDIT THE SHORTNAME BELOW
(function () { // REQUIRED CONFIGURATION VARIABLE: EDIT THE SHORTNAME BELOW
var d = document, s = d.createElement('script');
// IMPORTANT: Replace EXAMPLE with your forum shortname!
s.src = "https://"+self.srvModel.disqusShortname+".disqus.com/embed.js";
s.async= true;
s.src = "https://" + self.srvModel.disqusShortname + ".disqus.com/embed.js";
s.async = true;
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
var s2 = d.createElement('script');
s2.src="//"+self.srvModel.disqusShortname+".disqus.com/count.js";
s2.async= true;
s2.src = "//" + self.srvModel.disqusShortname + ".disqus.com/count.js";
s2.async = true;
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
@@ -334,7 +344,7 @@ document.addEventListener("DOMContentLoaded",function (ev) {
});
this.updateComputed();
}
});
});
});
/**