add minimal crowdfund system and UI

This commit is contained in:
Kukks
2018-12-22 15:02:16 +01:00
parent 8e8615dab8
commit d1ff34d16d
12 changed files with 232 additions and 59 deletions

View File

@@ -22,6 +22,9 @@ namespace BTCPayServer.Controllers
public bool EnforceTargetAmount { get; set; } public bool EnforceTargetAmount { get; set; }
public string CustomCSSLink { get; set; } public string CustomCSSLink { get; set; }
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
} }
@@ -41,9 +44,12 @@ namespace BTCPayServer.Controllers
StartDate = settings.StartDate, StartDate = settings.StartDate,
TargetCurrency = settings.TargetCurrency, TargetCurrency = settings.TargetCurrency,
Description = settings.Description, Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EndDate = settings.EndDate, EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount, TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline
}; };
return View(vm); return View(vm);
} }
@@ -62,12 +68,15 @@ namespace BTCPayServer.Controllers
Title = vm.Title, Title = vm.Title,
Enabled = vm.Enabled, Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount, EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate, StartDate = vm.StartDate?.ToUniversalTime(),
TargetCurrency = vm.TargetCurrency, TargetCurrency = vm.TargetCurrency,
Description = vm.Description, Description = vm.Description,
EndDate = vm.EndDate, EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount, TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline
}); });
await UpdateAppSettings(app); await UpdateAppSettings(app);
StatusMessage = "App updated"; StatusMessage = "App updated";

View File

@@ -86,37 +86,46 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("/apps/{appId}/crowdfund")] [Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)] [XFrameOptionsAttribute(null)]
public async Task<IActionResult> ViewCrowdfund(string appId) public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{ {
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund); var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
if (app == null) if (app == null)
return NotFound(); return NotFound();
var settings = app.GetSettings<CrowdfundSettings>(); var settings = app.GetSettings<CrowdfundSettings>();
var currency = _AppsHelper.GetCurrencyData(settings.TargetCurrency, false); var currency = _AppsHelper.GetCurrencyData(settings.TargetCurrency, false);
return View(CrowdfundHelper.GetInfo(app, _invoiceRepository, _rateFetcher, _btcPayNetworkProvider )); return View(await CrowdfundHelper.GetInfo(app, _invoiceRepository, _rateFetcher, _btcPayNetworkProvider, statusMessage ));
} }
[HttpPost] [HttpPost]
[Route("/apps/{appId}/crowdfund/contribute")] [Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)] [XFrameOptionsAttribute(null)]
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)] [EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId,[FromBody]ContributeToCrowdfund request, [FromQuery]bool redirectToCheckout) public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
{ {
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund); var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
if (app == null) if (app == null)
return NotFound(); return NotFound();
var settings = app.GetSettings<CrowdfundSettings>(); var settings = app.GetSettings<CrowdfundSettings>();
var currency = _AppsHelper.GetCurrencyData(settings.TargetCurrency, false);
var store = await _AppsHelper.GetStore(app); var store = await _AppsHelper.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice() var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
{ {
OrderId = appId,
Currency = settings.TargetCurrency,
BuyerEmail = request.Email,
Price = request.Amount,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
}, store, HttpContext.Request.GetAbsoluteRoot()); }, store, HttpContext.Request.GetAbsoluteRoot());
if (redirectToCheckout) if (request.RedirectToCheckout)
{ {
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id }); return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
} }
else else
{ {
@@ -208,9 +217,10 @@ namespace BTCPayServer.Controllers
var finalTasks = new List<Task>(); var finalTasks = new List<Task>();
foreach (var rateTask in ratesTask) foreach (var rateTask in ratesTask)
{ {
finalTasks.Add(rateTask.Value.ContinueWith(task => finalTasks.Add(Task.Run(async () =>
{ {
var rate = task.Result.BidAsk?.Bid; var tResult = await rateTask.Value;
var rate = tResult.BidAsk?.Bid;
if (rate == null) return; if (rate == null) return;
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left); var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value); result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value);
@@ -224,7 +234,7 @@ namespace BTCPayServer.Controllers
} }
public static async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, InvoiceRepository invoiceRepository, public static async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, InvoiceRepository invoiceRepository,
RateFetcher rateFetcher, BTCPayNetworkProvider btcPayNetworkProvider) RateFetcher rateFetcher, BTCPayNetworkProvider btcPayNetworkProvider, string statusMessage= null)
{ {
var settings = appData.GetSettings<CrowdfundSettings>(); var settings = appData.GetSettings<CrowdfundSettings>();
var invoices = await GetPaidInvoicesForApp(appData, invoiceRepository); var invoices = await GetPaidInvoicesForApp(appData, invoiceRepository);
@@ -235,13 +245,15 @@ namespace BTCPayServer.Controllers
var paidInvoices = invoices.Length; var paidInvoices = invoices.Length;
var active = (settings.StartDate == null || DateTime.UtcNow >= settings.StartDate) && var active = (settings.StartDate == null || DateTime.UtcNow >= settings.StartDate) &&
(settings.EndDate == null || DateTime.UtcNow <= settings.EndDate) && (settings.EndDate == null || DateTime.UtcNow <= settings.EndDate) &&
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount) (!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount);
return new ViewCrowdfundViewModel() return new ViewCrowdfundViewModel()
{ {
Title = settings.Title, Title = settings.Title,
Tagline = settings.Tagline,
Description = settings.Description, Description = settings.Description,
CustomCSSLink = settings.CustomCSSLink, CustomCSSLink = settings.CustomCSSLink,
MainImageUrl = settings.MainImageUrl,
StoreId = appData.StoreDataId, StoreId = appData.StoreDataId,
AppId = appData.Id, AppId = appData.Id,
StartDate = settings.StartDate, StartDate = settings.StartDate,
@@ -249,11 +261,16 @@ namespace BTCPayServer.Controllers
TargetAmount = settings.TargetAmount, TargetAmount = settings.TargetAmount,
TargetCurrency = settings.TargetCurrency, TargetCurrency = settings.TargetCurrency,
EnforceTargetAmount = settings.EnforceTargetAmount, EnforceTargetAmount = settings.EnforceTargetAmount,
StatusMessage = statusMessage,
Info = new ViewCrowdfundViewModel.CrowdfundInfo() Info = new ViewCrowdfundViewModel.CrowdfundInfo()
{ {
TotalContributors = paidInvoices, TotalContributors = paidInvoices,
CurrentAmount = currentAmount, CurrentAmount = currentAmount,
Active = active Active = active,
DaysLeft = settings.EndDate.HasValue? (settings.EndDate - DateTime.UtcNow).Value.Days: (int?) null,
DaysLeftToStart = settings.StartDate.HasValue? (settings.StartDate - DateTime.UtcNow).Value.Days: (int?) null,
ShowProgress =active && settings.TargetAmount.HasValue,
ProgressPercentage = currentAmount/ settings.TargetAmount * 100
} }
}; };
} }
@@ -281,14 +298,19 @@ namespace BTCPayServer.Controllers
} }
public async Task<AppData> GetApp(string appId, AppType appType) public async Task<AppData> GetApp(string appId, AppType appType, bool includeStore = false)
{ {
using (var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
return await ctx.Apps var query = ctx.Apps
.Where(us => us.Id == appId && .Where(us => us.Id == appId &&
us.AppType == appType.ToString()) us.AppType == appType.ToString());
.FirstOrDefaultAsync();
if (includeStore)
{
query = query.Include(data => data.StoreData);
}
return await query.FirstOrDefaultAsync();
} }
} }

View File

@@ -38,6 +38,7 @@ using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc.Cors.Internal; using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net; using System.Net;
using BTCPayServer.Hubs;
using Meziantou.AspNetCore.BundleTagHelpers; using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security; using BTCPayServer.Security;
@@ -78,7 +79,7 @@ namespace BTCPayServer.Hosting
services.AddIdentity<ApplicationUser, IdentityRole>() services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer(); services.AddBTCPayServer();
services.AddMvc(o => services.AddMvc(o =>
{ {
@@ -198,6 +199,10 @@ namespace BTCPayServer.Hosting
AppPath = options.GetRootUri(), AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) } Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
}); });
app.UseSignalR(route =>
{
route.MapHub<CrowdfundHub>("/apps/crowdfund/hub");
});
app.UseWebSockets(); app.UseWebSockets();
app.UseStatusCodePages(); app.UseStatusCodePages();
app.UseMvc(routes => app.UseMvc(routes =>

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
namespace BTCPayServer.Hubs
{
public class CrowdfundHub: Hub
{
}
}

View File

@@ -11,6 +11,9 @@ namespace BTCPayServer.Models.AppViewModels
[Required] [Required]
public string Description { get; set; } public string Description { get; set; }
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
[Required] [Required]
public bool Enabled { get; set; } public bool Enabled { get; set; }
@@ -33,5 +36,7 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(500)] [MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")] [Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; } public string CustomCSSLink { get; set; }
public string Tagline { get; set; }
} }
} }

View File

@@ -10,6 +10,7 @@ namespace BTCPayServer.Models.AppViewModels
public string AppId { get; set; } public string AppId { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string MainImageUrl { get; set; }
public string CustomCSSLink { get; set; } public string CustomCSSLink { get; set; }
public DateTime? StartDate { get; set; } public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }
@@ -19,6 +20,7 @@ namespace BTCPayServer.Models.AppViewModels
public bool EnforceTargetAmount { get; set; } public bool EnforceTargetAmount { get; set; }
public CrowdfundInfo Info { get; set; } public CrowdfundInfo Info { get; set; }
public string Tagline { get; set; }
public class CrowdfundInfo public class CrowdfundInfo
@@ -26,14 +28,21 @@ namespace BTCPayServer.Models.AppViewModels
public int TotalContributors { get; set; } public int TotalContributors { get; set; }
public decimal CurrentAmount { get; set; } public decimal CurrentAmount { get; set; }
public bool Active { get; set; } public bool Active { get; set; }
public bool ShowProgress { get; set; }
public decimal? ProgressPercentage { get; set; }
public int? DaysLeft{ get; set; }
public int? DaysLeftToStart{ get; set; }
} }
} }
public class ContributeToCrowdfund public class ContributeToCrowdfund
{ {
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
[Required] public decimal Amount { get; set; } [Required] public decimal Amount { get; set; }
public string Email { get; set; } public string Email { get; set; }
public bool RedirectToCheckout { get; set; }
} }
} }

View File

@@ -24,6 +24,11 @@
<input asp-for="Title" class="form-control" /> <input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span> <span asp-validation-for="Title" class="text-danger"></span>
</div> </div>
<div class="form-group">
<label asp-for="Tagline" class="control-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="form-group">
<label asp-for="Description" class="control-label"></label>* <label asp-for="Description" class="control-label"></label>*
<textarea asp-for="Description" rows="20" cols="40" class="form-control"></textarea> <textarea asp-for="Description" rows="20" cols="40" class="form-control"></textarea>
@@ -36,13 +41,26 @@
<input asp-for="CustomCSSLink" class="form-control" /> <input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span> <span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div> </div>
<div class="form-group">
<label asp-for="MainImageUrl" class="control-label"></label>
<input asp-for="MainImageUrl" class="form-control" />
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationUrl" class="control-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="Enabled"></label> <label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/> <input asp-for="Enabled" type="checkbox" class="form-check"/>
<span asp-validation-for="Enabled" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="EnforceTargetAmount"></label> <label asp-for="EnforceTargetAmount"></label>
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check"/> <input asp-for="EnforceTargetAmount" type="checkbox" class="form-check"/>
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@@ -0,0 +1,23 @@
@model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund
<form method="post">
<div class="form-group">
<label asp-for="Email"></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"></label>
<div class="input-group mb-3">
<input asp-for="Amount" type="number" step="any" class="form-control"/>
<div class="input-group-append">
<span class="input-group-text">@Model.ViewCrowdfundViewModel.TargetCurrency.ToUpperInvariant()</span>
</div>
</div>
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<input type="hidden" asp-for="RedirectToCheckout"/>
<button type="submit" class="btn btn-primary">Contribute</button>
</form>

View File

@@ -1,8 +0,0 @@
@model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund
<form method="post" action="contribute">
<input asp-for="Email" type="email"/>
<input asp-for="Amount" type="number" step="any" />
<button type="submit">Contribute</button>
</form>

View File

@@ -1,19 +0,0 @@
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
<div class="container d-flex h-100">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@Model.StatusMessage"/>
</div>
</div>
<header>
<h1>@Model.Title</h1>
</header>
<main>
@Model.Description
<partial name="ContributeForm"/>
</main>
<footer></footer>
<pre> @Html.Raw(Model)</pre>
</div>

View File

@@ -0,0 +1,101 @@
@using BTCPayServer.Models.AppViewModels
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
<div class="container h-100">
<div class="row align-items-center h-100">
<div class="mx-auto card col-lg-8 col-sm-12 col-md-9 p-0">
<partial name="_StatusMessage" for="@Model.StatusMessage"/>
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
<img class="card-img-top" src="@Model.MainImageUrl" alt="Card image cap">
}
@if (Model.Info.ShowProgress)
{
<div class="progress rounded-0 striped" style="min-height: 30px">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="@Model.Info.ProgressPercentage" aria-valuemin="0" aria-valuemax="100">
@if (Model.Info.ProgressPercentage.Value > 0)
{
@(Model.Info.ProgressPercentage + "%")
}
</div>
</div>
}
<div class="card-body">
<div class="card-title row">
<div class="col-9">
<h1 >
@Model.Title
@if (!string.IsNullOrEmpty(Model.Tagline))
{
<h2 class="text-muted">@Model.Tagline</h2>
}
@if (Model.Info.DaysLeftToStart.HasValue && Model.Info.DaysLeftToStart > 0)
{
<small>
@($"{Model.Info.DaysLeftToStart} day{(Model.Info.DaysLeftToStart.Value > 1 ? "s" : "")} left to start")
</small>
}
</h1>
</div>
<ul class="list-group list-group-flush col-3">
<li class="list-group-item">@(Model.EndDate.HasValue? $"Ends {Model.EndDate.Value:dddd, dd MMMM yyyy HH:mm}" : "No specific end date")</li>
<li class="list-group-item">@(Model.TargetAmount.HasValue? $"{Model.TargetAmount:G29} {Model.TargetCurrency.ToUpperInvariant()} Goal" :
"No specific target goal")</li>
<li class="list-group-item">@(Model.EnforceTargetAmount? $"Hardcap Goal" : "Softcap Goal")</li>
</ul>
</div>
@if (Model.Info.Active)
{
<div class="card-deck mb-4 ">
<div class="card shadow">
<div class="card-body">
<h5 class="card-title text-center">@Model.Info.TotalContributors</h5>
<h6 class="card-text text-center"> Contributors</h6>
</div>
</div>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title text-center">@Model.Info.CurrentAmount @Model.TargetCurrency.ToUpperInvariant()</h5>
<h6 class="card-text text-center"> Raised</h6>
</div>
</div>
@if (Model.Info.DaysLeft.HasValue && Model.Info.DaysLeft > 0)
{
<div class="card shadow">
<div class="card-body">
<h5 class="card-title text-center">@Model.Info.DaysLeft</h5>
<h6 class="card-text text-center">Day@(Model.Info.DaysLeft.Value > 1 ? "s" : "") left</h6>
</div>
</div>
}
</div>
}
<div class="card-text"> @Html.Raw(Model.Description)</div>
@if (Model.Info.Active)
{
<hr/>
<h3>Contribute</h3>
<partial name="ContributeForm" model="@(new ContributeToCrowdfund()
{
RedirectToCheckout = true,
ViewCrowdfundViewModel = Model
})"/>
}
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,5 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers @addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager @inject BTCPayServer.HostedServices.CssThemeManager themeManager
@using System.Security.AccessControl
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel @model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
@{ @{
ViewData["Title"] = Model.Title; ViewData["Title"] = Model.Title;
@@ -15,7 +13,7 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/> <link href="@Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/>
@if (Model.CustomCSSLink != null) @if (Model.CustomCSSLink != null)
{ {
<link href="@Model.CustomCSSLink" rel="stylesheet"/> <link href="@Model.CustomCSSLink" rel="stylesheet"/>
@@ -23,7 +21,7 @@
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/> <link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
<bundle name="wwwroot/bundles/crowdfund-bundle.min.js"></bundle> <bundle name="wwwroot/bundles/crowdfund-bundle.min.js"></bundle>
<script> <script>
var srvModel = @Html.Raw(Model); var srvModel = @Json.Serialize(Model);
</script> </script>
</head> </head>