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 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,
TargetCurrency = settings.TargetCurrency,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline
};
return View(vm);
}
@@ -62,12 +68,15 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate,
StartDate = vm.StartDate?.ToUniversalTime(),
TargetCurrency = vm.TargetCurrency,
Description = vm.Description,
EndDate = vm.EndDate,
EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline
});
await UpdateAppSettings(app);
StatusMessage = "App updated";

View File

@@ -86,37 +86,46 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[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)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
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]
[Route("/apps/{appId}/crowdfund/contribute")]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)]
[IgnoreAntiforgeryToken]
[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)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var currency = _AppsHelper.GetCurrencyData(settings.TargetCurrency, false);
var store = await _AppsHelper.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
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());
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
{
@@ -208,9 +217,10 @@ namespace BTCPayServer.Controllers
var finalTasks = new List<Task>();
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;
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
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,
RateFetcher rateFetcher, BTCPayNetworkProvider btcPayNetworkProvider)
RateFetcher rateFetcher, BTCPayNetworkProvider btcPayNetworkProvider, string statusMessage= null)
{
var settings = appData.GetSettings<CrowdfundSettings>();
var invoices = await GetPaidInvoicesForApp(appData, invoiceRepository);
@@ -235,13 +245,15 @@ namespace BTCPayServer.Controllers
var paidInvoices = invoices.Length;
var active = (settings.StartDate == null || DateTime.UtcNow >= settings.StartDate) &&
(settings.EndDate == null || DateTime.UtcNow <= settings.EndDate) &&
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount)
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount);
return new ViewCrowdfundViewModel()
{
Title = settings.Title,
Tagline = settings.Tagline,
Description = settings.Description,
CustomCSSLink = settings.CustomCSSLink,
MainImageUrl = settings.MainImageUrl,
StoreId = appData.StoreDataId,
AppId = appData.Id,
StartDate = settings.StartDate,
@@ -249,11 +261,16 @@ namespace BTCPayServer.Controllers
TargetAmount = settings.TargetAmount,
TargetCurrency = settings.TargetCurrency,
EnforceTargetAmount = settings.EnforceTargetAmount,
StatusMessage = statusMessage,
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
{
TotalContributors = paidInvoices,
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())
{
return await ctx.Apps
var query = ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
us.AppType == appType.ToString());
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.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.Hubs;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
@@ -78,7 +79,7 @@ namespace BTCPayServer.Hosting
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer();
services.AddMvc(o =>
{
@@ -198,6 +199,10 @@ namespace BTCPayServer.Hosting
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
});
app.UseSignalR(route =>
{
route.MapHub<CrowdfundHub>("/apps/crowdfund/hub");
});
app.UseWebSockets();
app.UseStatusCodePages();
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]
public string Description { get; set; }
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
[Required]
public bool Enabled { get; set; }
@@ -33,5 +36,7 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
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 Title { get; set; }
public string Description { get; set; }
public string MainImageUrl { get; set; }
public string CustomCSSLink { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
@@ -19,6 +20,7 @@ namespace BTCPayServer.Models.AppViewModels
public bool EnforceTargetAmount { get; set; }
public CrowdfundInfo Info { get; set; }
public string Tagline { get; set; }
public class CrowdfundInfo
@@ -26,14 +28,21 @@ namespace BTCPayServer.Models.AppViewModels
public int TotalContributors { get; set; }
public decimal CurrentAmount { 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 ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
[Required] public decimal Amount { 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" />
<span asp-validation-for="Title" class="text-danger"></span>
</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">
<label asp-for="Description" class="control-label"></label>*
<textarea asp-for="Description" rows="20" cols="40" class="form-control"></textarea>
@@ -36,13 +41,26 @@
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</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">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>
<span asp-validation-for="Enabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EnforceTargetAmount"></label>
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check"/>
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
</div>
<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
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@using System.Security.AccessControl
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
@{
ViewData["Title"] = Model.Title;
@@ -15,7 +13,7 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<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)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
@@ -23,7 +21,7 @@
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
<bundle name="wwwroot/bundles/crowdfund-bundle.min.js"></bundle>
<script>
var srvModel = @Html.Raw(Model);
var srvModel = @Json.Serialize(Model);
</script>
</head>