more integration

This commit is contained in:
Kukks 2018-12-28 17:38:20 +01:00
parent d9426d301d
commit 35f4ea29f9
11 changed files with 241 additions and 104 deletions

View file

@ -124,7 +124,6 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="Views\AppsPublic\Crowdfund\Templates" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
</ItemGroup>

View file

@ -8,6 +8,11 @@ namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class CrowdfundAppUpdated
{
public string AppId { get; set; }
}
public class CrowdfundSettings
{
public string Title { get; set; }
@ -51,7 +56,7 @@ namespace BTCPayServer.Controllers
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline
Tagline = settings.Tagline,
};
return View(vm);
}
@ -70,10 +75,10 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate?.ToUniversalTime(),
StartDate = vm.StartDate,
TargetCurrency = vm.TargetCurrency,
Description = vm.Description,
EndDate = vm.EndDate?.ToUniversalTime(),
EndDate = vm.EndDate,
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
@ -82,6 +87,10 @@ namespace BTCPayServer.Controllers
Tagline = vm.Tagline
});
await UpdateAppSettings(app);
_EventAggregator.Publish(new CrowdfundAppUpdated()
{
AppId = appId
});
StatusMessage = "App updated";
return RedirectToAction(nameof(ListApps));
}

View file

@ -24,17 +24,20 @@ namespace BTCPayServer.Controllers
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;

View file

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Hubs;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Security;
@ -16,6 +17,7 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitpayClient;
@ -28,16 +30,18 @@ namespace BTCPayServer.Controllers
{
public AppsPublicController(AppsHelper appsHelper,
InvoiceController invoiceController,
CrowdfundHubStreamer crowdfundHubStreamer)
CrowdfundHubStreamer crowdfundHubStreamer, UserManager<ApplicationUser> userManager)
{
_AppsHelper = appsHelper;
_InvoiceController = invoiceController;
_CrowdfundHubStreamer = crowdfundHubStreamer;
_UserManager = userManager;
}
private AppsHelper _AppsHelper;
private InvoiceController _InvoiceController;
private readonly CrowdfundHubStreamer _CrowdfundHubStreamer;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
@ -86,8 +90,10 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
if (app == null ||
(!app.GetSettings<CrowdfundSettings>().Enabled &&
_AppsHelper.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) == null))
return NotFound();
return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
@ -101,7 +107,9 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
{
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
if (app == null ||
(!app.GetSettings<CrowdfundSettings>().Enabled &&
_AppsHelper.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) == null))
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var store = await _AppsHelper.GetStore(app);
@ -191,6 +199,12 @@ namespace BTCPayServer.Controllers
}, store, HttpContext.Request.GetAbsoluteRoot());
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
public class AppsHelper
@ -310,5 +324,6 @@ namespace BTCPayServer.Controllers
return app;
}
}
}
}

View file

@ -96,7 +96,7 @@ namespace BTCPayServer.Hubs
var token = new CancellationTokenSource();
_CacheTokens.Add(key, token);
entry.AddExpirationToken(new CancellationChangeToken(token.Token));
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(20);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5);
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
var result = await GetInfo(app, _InvoiceRepository, _RateFetcher,
@ -110,6 +110,10 @@ namespace BTCPayServer.Hubs
{
_EventAggregator.Subscribe<InvoiceEvent>(Subscription);
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
{
InvalidateCacheForApp(updated.AppId);
});
}
private string GetCacheKey(string appId)
@ -127,17 +131,22 @@ namespace BTCPayServer.Hubs
switch (invoiceEvent.Name)
{
case InvoiceEvent.ReceivedPayment:
_HubContext.Clients.Group(appId).SendCoreAsync("PaymentReceived", new object[]{ invoiceEvent.Invoice.AmountPaid } );
_HubContext.Clients.Group(appId).SendCoreAsync("PaymentReceived", new object[]{ invoiceEvent.Invoice.CryptoInfo.First().Paid } );
break;
case InvoiceEvent.Completed:
if (_CacheTokens.ContainsKey(appId))
{
_CacheTokens[appId].Cancel();
}
_HubContext.Clients.Group(appId).SendCoreAsync("InfoUpdated", new object[]{} );
InvalidateCacheForApp(appId);
break;
}
}
private void InvalidateCacheForApp(string appId)
{
if (_CacheTokens.ContainsKey(appId))
{
_CacheTokens[appId].Cancel();
}
_HubContext.Clients.Group(appId).SendCoreAsync("InfoUpdated", new object[]{} );
}
private static async Task<decimal> GetCurrentContributionAmount(InvoiceEntity[] invoices, string primaryCurrency,
RateFetcher rateFetcher, RateRules rateRules)
@ -170,19 +179,25 @@ namespace BTCPayServer.Hubs
return result;
}
public static async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, InvoiceRepository invoiceRepository,
private static async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, InvoiceRepository invoiceRepository,
RateFetcher rateFetcher, BTCPayNetworkProvider btcPayNetworkProvider, string statusMessage= null)
{
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
var invoices = await GetPaidInvoicesForApp(appData, invoiceRepository);
var invoices = await GetInvoicesForApp(appData, invoiceRepository);
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(btcPayNetworkProvider);
var currentAmount = await GetCurrentContributionAmount(
invoices,
invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray(),
settings.TargetCurrency, rateFetcher, rateRules);
var paidInvoices = invoices.Length;
var active = (settings.StartDate == null || DateTime.UtcNow >= settings.StartDate) &&
(settings.EndDate == null || DateTime.UtcNow <= settings.EndDate) &&
var currentPendingAmount = await GetCurrentContributionAmount(
invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray(),
settings.TargetCurrency, rateFetcher, rateRules);
var active = (settings.StartDate == null || DateTime.Now >= settings.StartDate) &&
(settings.EndDate == null || DateTime.Now <= settings.EndDate) &&
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount);
return new ViewCrowdfundViewModel()
@ -203,23 +218,29 @@ namespace BTCPayServer.Hubs
StatusMessage = statusMessage,
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
{
TotalContributors = paidInvoices,
TotalContributors = invoices.Length,
CurrentPendingAmount = currentPendingAmount,
CurrentAmount = currentAmount,
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
ShowProgress = settings.TargetAmount.HasValue,
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100
}
};
}
private static async Task<InvoiceEntity[]> GetPaidInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
private static async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
{
return await invoiceRepository.GetInvoices(new InvoiceQuery()
{
OrderId = appData.Id,
Status = new string[]{ InvoiceState.ToString(InvoiceStatus.Complete)}
OrderId = $"{CrowdfundInvoiceOrderIdPrefix}{appData.Id}",
Status = new string[]{
InvoiceState.ToString(InvoiceStatus.New),
InvoiceState.ToString(InvoiceStatus.Paid),
InvoiceState.ToString(InvoiceStatus.Confirmed),
InvoiceState.ToString(InvoiceStatus.Complete)}
});
}

View file

@ -9,6 +9,9 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(30)]
public string Title { get; set; }
[MaxLength(50)]
public string Tagline { get; set; }
[Required]
public string Description { get; set; }
public string MainImageUrl { get; set; }
@ -16,7 +19,7 @@ namespace BTCPayServer.Models.AppViewModels
public string NotificationUrl { get; set; }
[Required]
public bool Enabled { get; set; }
public bool Enabled { get; set; } = false;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
@ -37,7 +40,6 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
public string Tagline { get; set; }
public string EmbeddedCSS { get; set; }
}
}

View file

@ -33,6 +33,8 @@ namespace BTCPayServer.Models.AppViewModels
public decimal? ProgressPercentage { get; set; }
public int? DaysLeft{ get; set; }
public int? DaysLeftToStart{ get; set; }
public decimal CurrentPendingAmount { get; set; }
public decimal? PendingProgressPercentage { get; set; }
}
}

View file

@ -2,97 +2,135 @@
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
<div class="card w-100 p-0 mx-0">
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl">
<div class="progress rounded-0 striped" style="min-height: 30px" v-if="srvModel.info.showProgres">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" :aria-valuenow="srvModel.info.progressPercentage" aria-valuemin="0" aria-valuemax="100">
<div class="progress rounded-0" style="min-height: 30px" v-if="srvModel.info.showProgress">
<div class="progress-bar p" role="progressbar" :aria-valuenow="srvModel.info.progressPercentage" aria-valuemin="0" aria-valuemax="100">
<template v-if="srvModel.info.progressPercentage > 0">
{{srvModel.info.progressPercentage}} %
{{srvModel.info.progressPercentage}} % Funded
</template>
</div>
<div class="progress-bar bg-warning" role="progressbar" :aria-valuenow="srvModel.info.progressPercentage" aria-valuemin="0" aria-valuemax="100">
<template v-if="srvModel.info.progressPercentage > 0">
{{srvModel.info.pendingProgressPercentage}} % Pending
</template>
</div>
</div>
<div class="card-body">
<div class="card-title row">
<div class="col-md-9 col-sm-12">
<div class="col-md-6 col-sm-12">
<h1 >
<span class="h1" >
{{srvModel.title}}
<h2 class="text-muted" v-if="srvModel.tagline">{{srvModel.tagline}}</h2>
<small v-if="srvModel.info.daysLeftToStart && srvModel.info.daysLeftToStart > 0">
{{ srvModel.info.daysLeftToStart }} days left to start
</small>
</h1>
<span class="h6 text-muted" v-if="!started && srvModel.startDate">
Starts {{startDateRelativeTime}}
</span>
<span class="h6 " v-else-if="started && !ended && srvModel.endDate">
Ends {{endDateRelativeTime}}
</span>
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate">
Currently Active!
</span>
</span>
<h2 class="text-muted" v-if="srvModel.tagline">{{srvModel.tagline}}</h2>
<button v-if="srvModel.info.active" class="btn btn-lg btn-primary w-100" v-on:click="contributeModalOpen = true">Contribute</button>
</div>
<ul class="list-group list-group-flush col-md-3 col-sm-12">
<li class="list-group-item" v-if="startDate">
<template>{{started}} {{ended}}</template>
</li>
<li class="list-group-item" v-if="startDate">
<template>Starts {{startDate}}</template>
<ul class="list-group list-group-flush col-md-6 col-sm-12">
<li class="list-group-item border-0">
<div class="d-flex justify-content-between">
<div>
<span class="h5">{{srvModel.info.currentAmount + srvModel.info.currentPendingAmount }} {{targetCurrency}} </span>
<span>raised by {{srvModel.info.totalContributors}} contributors </span>
</div>
</div>
<div class="progress w-100" v-if="srvModel.info.showProgress">
<div class="progress-bar" role="progressbar"
:aria-valuenow="srvModel.info.progressPercentage"
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
aria-valuemin="0"
aria-valuemax="100">
<template v-if="srvModel.info.progressPercentage > 0">
{{srvModel.info.progressPercentage}}% Funded
</template>
</div>
<div class="progress-bar bg-warning" role="progressbar"
:aria-valuenow="srvModel.info.pendingProgressPercentage"
v-bind:style="{ width: srvModel.info.pendingProgressPercentage + '%' }"
aria-valuemin="0"
aria-valuemax="100">
<template v-if="srvModel.info.pendingProgressPercentage > 0">
{{srvModel.info.pendingProgressPercentage}}% Pending
</template>
</div>
</div>
</li>
<li class="list-group-item">
<template v-if="endDate">Ends {{endDate}}</template>
<template v-else>No specific end date</template>
</li>
<li class="list-group-item">
<template v-if="srvModel.targetAmount">{{srvModel.targetAmount}} {{srvModel.targetCurrency}} Goal</template>
<template v-else>No specific target goal</template>
<span v-if="srvModel.targetAmount" class="h5">{{srvModel.targetAmount}} {{targetCurrency}}</span>
<span v-if="srvModel.enforceTargetAmount">Hardcap Goal <small>- No contributions allowed after the goal has been reached</small></span>
<span v-else>Softcap Goal <small>- Contributions allowed after goal is reached</small></span>
</li>
<li class="list-group-item">
<template v-if="srvModel.enforceTargetAmount">Hardcap Goal</template>
<template v-else>Softcap Goal</template>
<template v-if="startDate">
<template v-if="started">
Started {{startDate}}
</template>
<template v-else-if="!started">
Starts {{startDate}}
</template>
&
</template>
<template class="list-group-item" v-if="endDate">
<template v-if="!ended">
Ends {{endDate}}
</template>
<template v-else-if="ended">
Ended {{endDate}}
</template>
</template>
<template class="list-group-item" v-else-if="!endDate">
No specific end date
</template>
</li>
</ul>
</div>
<div class="card-deck mb-4" v-if="srvModel.info.active">
<div class="card shadow">
<div class="card-body">
<h5 class="card-title text-center">{{srvModel.info.totalContributors}}</h5>
<h6 class="card-text text-center"> Contributors</h6>
</div>
<hr/>
<div class="row">
<div class="col-md-8 col-sm-12">
<div class="card-text" v-html="srvModel.description"></div>
</div>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title text-center">{{srvModel.info.currentAmount}} {{srvModel.info.targetCurrency}}</h5>
<h6 class="card-text text-center"> Raised</h6>
</div>
</div>
<div class="card shadow" v-if="srvModel.info.daysLeft && srvModel.info.daysLeft >0">
<div class="card-body">
<h5 class="card-title text-center">Ends {{endDateRelativeTime}}</h5>
</div>
<div class="col-md-4 col-sm-12">
<contribute :target-currency="srvModel.targetCurrency" :active="srvModel.info.active"></contribute>
</div>
</div>
<div class="card-text" v-html="srvModel.description"></div>
<template v-if="srvModel.info.active"></template>
<hr/>
<h3>Contribute</h3>
<form v-on:submit="onContributeFormSubmit">
<div class="form-group">
<label ></label>
<input type="email" class="form-control" v-model="contributionForm.email"/>
</div>
<div class="form-group">
<label ></label>
<div class="input-group mb-3">
<input type="number" step="any" class="form-control" v-model="contributionForm.amount"/>
<div class="input-group-append">
<span class="input-group-text">{{srvModel.targetCurrency}}</span>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Contribute</button>
</form>
</div>
</div>
</div>
<b-modal title="Contribute" v-model="contributeModalOpen" v-on:ok="submitModalContribute" :hide-header-close="true">
<contribute :target-currency="srvModel.targetCurrency" :active="srvModel.info.active" ref="modalContribute" :in-modal="true"></contribute>
<template slot="modal-ok">
Submit
</template>
</b-modal>
<b-modal title="Thank You!" v-model="thankYouModalOpen" :ok-only="true">
Thank you for your contribution.
</b-modal>
</div>

View file

@ -67,6 +67,7 @@
"outputFileName": "wwwroot/bundles/crowdfund-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vue-toasted/vue-toasted.min.js",
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
"wwwroot/vendor/signalr/signalr.js",

View file

@ -1,6 +1,7 @@
var app = null;
var eventAggregator = new Vue();
window.onload = function (ev) {
Vue.use(Toasted);
app = new Vue({
el: '#app',
data: function(){
@ -13,10 +14,15 @@ window.onload = function (ev) {
endDateRelativeTime: "",
started: false,
ended: false,
contributionForm: { email: "", amount: 0}
contributeModalOpen: false,
thankYouModalOpen: false
}
},
computed: {
targetCurrency: function(){
return this.srvModel.targetCurrency.toUpperCase();
}
},
computed: {},
methods: {
updateComputed: function () {
if (this.srvModel.endDate) {
@ -25,7 +31,7 @@ window.onload = function (ev) {
this.endDateRelativeTime = endDateM.fromNow();
this.ended = endDateM.isBefore(moment());
}else{
this.ended = true;
this.ended = false;
}
if (this.srvModel.startDate) {
@ -38,22 +44,27 @@ window.onload = function (ev) {
}
setTimeout(this.updateComputed, 1000);
},
onContributeFormSubmit: function(e){
if(e){
e.preventDefault();
}
eventAggregator.$emit("contribute", this.contributionForm);
submitModalContribute: function(e){
debugger;
this.$refs.modalContribute.onContributeFormSubmit(e);
}
},
mounted: function () {
hubListener.connect();
var self = this;
eventAggregator.$on("invoice-created", function (invoiceId) {
btcpay.setApiUrlPrefix(window.location.origin);
btcpay.showInvoice(invoiceId);
btcpay.showFrame();
});
self.contributeModalOpen = false;
});
btcpay.onModalWillLeave = function(){
self.thankYouModalOpen = true;
};
eventAggregator.$on("payment-received", function (amount) {
console.warn("AAAAAA", amount);
Vue.toasted.show('New payment of ' + amount+ " BTC registered");
});
eventAggregator.$on("info-updated", function (model) {
this.srvModel = model;

View file

@ -1,7 +1,43 @@
Vue.component('contribute', {
props: ["targetCurrency", "active", "inModal"],
data: function () {
return {
email: "",
amount: 0,
choiceKey: ""
}
},
template: ''
methods: {
onContributeFormSubmit: function (e) {
if (e) {
e.preventDefault();
}
if(!this.active){
return;
}
eventAggregator.$emit("contribute", {email: this.email, amount: this.amount, choiceKey: this.choiceKey});
}
},
template: "<div>" +
"<h3 v-if='!inModal'>Contribute</h3>" +
"" +
" <form v-on:submit=\"onContributeFormSubmit\">" +
"" +
" <div class=\"form-group\">" +
" <label ></label>" +
" <input type=\"email\" class=\"form-control\" v-model=\"email\" placeholder='Email'/>" +
" </div>" +
" <div class=\"form-group\">" +
" <label ></label>" +
" <div class=\"input-group mb-3\">" +
" <input type=\"number\" step=\"any\" class=\"form-control\" v-model=\"amount\" placeholder='Contribution Amount'/>" +
" <div class=\"input-group-append\">" +
" <span class=\"input-group-text\">{{targetCurrency}}</span>" +
" </div>" +
" </div>" +
" </div>" +
" <button type=\"submit\" class=\"btn btn-primary\" :disabled='!active' v-if='!inModal'>Contribute</button>" +
" </form>" +
"</div>"
});