Compare commits
106 Commits
Author | SHA1 | Date | |
---|---|---|---|
c95ef27998 | |||
63dfd93834 | |||
57610881de | |||
7469faf296 | |||
55a884a559 | |||
ee2b3c3d10 | |||
e5819a260b | |||
a3ecf48702 | |||
1c0b904cd2 | |||
072d8a1728 | |||
964e541c32 | |||
78fec4ed22 | |||
ef111d36c9 | |||
4f64193e85 | |||
89bb6d1268 | |||
9f4226bf0f | |||
a87c2a3374 | |||
d7294ba5a0 | |||
82d286dc6f | |||
1fa18ab997 | |||
afc90f32c9 | |||
e9cfb7c21e | |||
1af8ea3769 | |||
9f7af190f1 | |||
9c703fe94d | |||
a7a11a4f13 | |||
c32c3bb62b | |||
e29d1480a6 | |||
8f299d7791 | |||
65fb2e992e | |||
41f5d677d5 | |||
a2b78b8cd9 | |||
c93f217033 | |||
82c47b6e9a | |||
94fb738c67 | |||
89071e40fc | |||
95a90c410e | |||
59fc371cd5 | |||
caadfc8641 | |||
bffc2e70c1 | |||
ca28c34be0 | |||
196bc3ea00 | |||
04cba61888 | |||
a41e2e1ceb | |||
d1d03c98ba | |||
3e48a54ab5 | |||
f6e389ff62 | |||
561ec57cc8 | |||
c63feb488c | |||
f2ccc4d963 | |||
a92d48efdd | |||
b633206b45 | |||
b6f3d2af5e | |||
5ca4494eed | |||
de7e419ef4 | |||
20a6b3fc33 | |||
e6357d2ac8 | |||
1eecd85ceb | |||
d63176da19 | |||
887da5aa9a | |||
6e7f1151bc | |||
fb6d852827 | |||
ba17612461 | |||
a15c7a0213 | |||
7e321d4016 | |||
2ccf007b9a | |||
895b8c2c80 | |||
493466683c | |||
761c342c51 | |||
5341da28d9 | |||
7768f41849 | |||
c52a49f747 | |||
e4b9895ba7 | |||
92a2bb4d32 | |||
bfec722312 | |||
5a3f7b5b70 | |||
2aa097be46 | |||
8a646d85c6 | |||
890b3eaa00 | |||
cda28ebf15 | |||
2245027ca3 | |||
3dc250f801 | |||
66e786a1b0 | |||
8bd7ea5bbc | |||
6eb36abe2e | |||
6fced3fab2 | |||
774e456e54 | |||
519859e1c5 | |||
26f0c488e5 | |||
1b0b53fbd0 | |||
35f4ea29f9 | |||
d9426d301d | |||
8bcf7109a3 | |||
b11f8acba1 | |||
ef9a633aa4 | |||
c7e2f979dd | |||
e97bb9c933 | |||
2b84791391 | |||
9a488c60f2 | |||
9cb50446f4 | |||
8c00a2359e | |||
d1ff34d16d | |||
8e8615dab8 | |||
27bde55f54 | |||
b5d360594a | |||
7fa1b65af0 |
251
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
251
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CrowdfundTests
|
||||
{
|
||||
public CrowdfundTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateAndDeleteCrowdfundApp()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
var apps = user.GetController<AppsController>();
|
||||
var apps2 = user2.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
Assert.Null(vm.Name);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanContributeOnlyWhenAllowed()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
//Scenario 1: Not Enabled - Not Allowed
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = false;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
//Scenario 2: Not Enabled But Admin - Allowed
|
||||
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
|
||||
//Scenario 4: Enabled But End Date < Now - Not Allowed
|
||||
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
|
||||
crowdfundViewModel.EndDate= DateTime.Today.AddDays(-1);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
|
||||
|
||||
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
|
||||
crowdfundViewModel.EndDate= DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.TargetAmount = 1;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
}));
|
||||
|
||||
//Scenario 6: Allowed
|
||||
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.05)
|
||||
}));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeCrowdfundModel()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
crowdfundViewModel.TargetAmount = 100;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
|
||||
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount );
|
||||
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate );
|
||||
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate );
|
||||
Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency );
|
||||
Assert.Equal(0m, model.Info.CurrentAmount );
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
|
||||
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
|
||||
|
||||
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress,invoice.BtcDue);
|
||||
Assert.Equal(0m ,model.Info.CurrentAmount );
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal( 0m, model.Info.ProgressPercentage);
|
||||
Assert.Equal(1m, model.Info.PendingProgressPercentage);
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -329,7 +329,7 @@ namespace BTCPayServer.Tests
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m));
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
@ -451,7 +451,7 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
|
||||
await tester.SendLightningPaymentAsync(invoice);
|
||||
await EventuallyAsync(async () =>
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
@ -685,7 +685,7 @@ namespace BTCPayServer.Tests
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = acc.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
|
||||
@ -782,7 +782,7 @@ namespace BTCPayServer.Tests
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
Logs.Tester.LogInformation($"The invoice should be paidOver");
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(payment1, invoice.BtcPaid);
|
||||
@ -811,7 +811,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(tx2, ((NewTransactionEvent)listener.NextEvent(cts.Token)).TransactionData.TransactionHash);
|
||||
}
|
||||
Logs.Tester.LogInformation($"The invoice should now not be paidOver anymore");
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(payment2, invoice.BtcPaid);
|
||||
@ -1058,7 +1058,7 @@ namespace BTCPayServer.Tests
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.1m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
|
||||
@ -1079,7 +1079,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
|
||||
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
@ -1177,7 +1177,7 @@ namespace BTCPayServer.Tests
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.True(invoice.BtcPaid == firstPayment);
|
||||
@ -1219,7 +1219,7 @@ namespace BTCPayServer.Tests
|
||||
firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.True(invoice.BtcPaid == firstPayment);
|
||||
@ -1233,7 +1233,7 @@ namespace BTCPayServer.Tests
|
||||
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(Money.Zero, invoice.BtcDue);
|
||||
@ -1672,7 +1672,7 @@ donation:
|
||||
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var jsonResultPaid = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
|
||||
var paidresult = Assert.IsType<ContentResult>(jsonResultPaid);
|
||||
@ -1744,7 +1744,7 @@ donation:
|
||||
var firstPayment = productPartDue - missingMoney;
|
||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
// Check that for the second payment, network fee are included
|
||||
@ -1769,7 +1769,7 @@ donation:
|
||||
}
|
||||
});
|
||||
cashCow.SendToAddress(invoiceAddress, due);
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
@ -1801,17 +1801,18 @@ donation:
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var exportResultPaid = user.GetController<InvoiceController>().Export("csv").GetAwaiter().GetResult();
|
||||
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
|
||||
Assert.Equal("application/csv", paidresult.ContentType);
|
||||
Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content);
|
||||
Assert.Contains($",\"OnChain\",\"BTC\",\"0.1000999\",\"0.0001\",\"5000.0\"", paidresult.Content);
|
||||
Assert.Contains($",\"USD\",\"0.00050000\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
|
||||
Assert.Contains($",\"OnChain\",\"BTC\",\"0.0991\",\"0.0001\",\"5000.0\"", paidresult.Content);
|
||||
Assert.Contains($",\"USD\",\"5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
|
||||
Assert.Contains($"0\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1879,7 +1880,7 @@ donation:
|
||||
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
|
||||
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
|
||||
Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m });
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
@ -1925,7 +1926,7 @@ donation:
|
||||
|
||||
Money secondPayment = Money.Zero;
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("new", localInvoice.Status);
|
||||
@ -1948,7 +1949,7 @@ donation:
|
||||
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
@ -1962,7 +1963,7 @@ donation:
|
||||
|
||||
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
@ -1970,7 +1971,7 @@ donation:
|
||||
|
||||
cashCow.Generate(5); //Now should be complete
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
@ -1992,7 +1993,7 @@ donation:
|
||||
|
||||
var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
@ -2009,7 +2010,7 @@ donation:
|
||||
|
||||
cashCow.Generate(1);
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
@ -2199,36 +2200,39 @@ donation:
|
||||
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
|
||||
}
|
||||
|
||||
private void Eventually(Action act)
|
||||
public static class TestUtils
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
public static void Eventually(Action act)
|
||||
{
|
||||
try
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
public static async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
try
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
|
||||
services:
|
||||
|
||||
tests:
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.39</Version>
|
||||
<Version>1.0.3.42</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -38,6 +38,7 @@
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.20" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
@ -126,6 +127,7 @@
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
<Folder Include="wwwroot\vendor\summernote" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class AppsController
|
||||
{
|
||||
public class CrowdfundAppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public CrowdfundSettings Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
}
|
||||
|
||||
public class CrowdfundSettings
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
public string TargetCurrency { get; set; }
|
||||
public decimal? TargetAmount { get; set; }
|
||||
|
||||
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; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string PerksTemplate { get; set; }
|
||||
public bool DisqusEnabled { get; set; }= false;
|
||||
public bool SoundsEnabled { get; set; }= true;
|
||||
public string DisqusShortname { get; set; }
|
||||
public bool AnimationsEnabled { get; set; } = true;
|
||||
public bool UseInvoiceAmount { get; set; } = true;
|
||||
public int ResetEveryAmount { get; set; } = 1;
|
||||
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
{
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var vm = new UpdateCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
Description = settings.Description,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
Tagline = settings.Tagline,
|
||||
PerksTemplate = settings.PerksTemplate,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
UseInvoiceAmount = settings.UseInvoiceAmount,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||
UseAllStoreInvoices = settings.UseAllStoreInvoices,
|
||||
AppId = appId,
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
|
||||
{
|
||||
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _AppsHelper.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
||||
try
|
||||
{
|
||||
_AppsHelper.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
|
||||
}
|
||||
|
||||
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var newSettings = new CrowdfundSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
Enabled = vm.Enabled,
|
||||
EnforceTargetAmount = vm.EnforceTargetAmount,
|
||||
StartDate = vm.StartDate,
|
||||
TargetCurrency = vm.TargetCurrency,
|
||||
Description = _AppsHelper.Sanitize( vm.Description),
|
||||
EndDate = vm.EndDate,
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
SoundsEnabled = vm.SoundsEnabled,
|
||||
DisqusShortname = vm.DisqusShortname,
|
||||
AnimationsEnabled = vm.AnimationsEnabled,
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
UseInvoiceAmount = vm.UseInvoiceAmount,
|
||||
UseAllStoreInvoices = vm.UseAllStoreInvoices,
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
};
|
||||
|
||||
app.SetSettings(newSettings);
|
||||
await UpdateAppSettings(app);
|
||||
_EventAggregator.Publish(new CrowdfundAppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
@ -44,7 +47,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await GetAllApps();
|
||||
var apps = await _AppsHelper.GetAllApps(GetUserId());
|
||||
return View(new ListAppsViewModel()
|
||||
{
|
||||
Apps = apps
|
||||
@ -58,7 +61,7 @@ namespace BTCPayServer.Controllers
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
if (await DeleteApp(appData))
|
||||
if (await _AppsHelper.DeleteApp(appData))
|
||||
StatusMessage = "App removed successfully";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
@ -67,7 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp()
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must have created at least one store";
|
||||
@ -82,7 +85,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must own at least one store";
|
||||
@ -117,9 +120,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
CreatedAppId = id;
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
|
||||
switch (appType)
|
||||
{
|
||||
case AppType.PointOfSale:
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
case AppType.Crowdfund:
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = id });
|
||||
default:
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -142,50 +153,7 @@ namespace BTCPayServer.Controllers
|
||||
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
private async Task<StoreData[]> GetOwnedStores()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> DeleteApp(AppData appData)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.Apps.Add(appData);
|
||||
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
|
||||
return await ctx.SaveChangesAsync() == 1;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||
(us, app) =>
|
||||
new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id
|
||||
})
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -7,14 +8,21 @@ using System.Security.Claims;
|
||||
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;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitpayClient;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
@ -22,14 +30,20 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
|
||||
public AppsPublicController(AppsHelper appsHelper,
|
||||
InvoiceController invoiceController,
|
||||
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")]
|
||||
@ -70,6 +84,120 @@ namespace BTCPayServer.Controllers
|
||||
AppId = appId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
||||
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
var isAdmin = await _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
|
||||
if (!hasEnoughSettingsToLoad)
|
||||
{
|
||||
if(!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
if (settings.Enabled) return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
|
||||
if(!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
|
||||
var isAdmin = await _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
if(!isAdmin)
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = await _CrowdfundHubStreamer.GetCrowdfundInfo(appId);
|
||||
|
||||
if(!isAdmin &&
|
||||
|
||||
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
|
||||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
|
||||
(settings.EnforceTargetAmount &&
|
||||
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
|
||||
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
|
||||
{
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var store = await _AppsHelper.GetStore(app);
|
||||
var title = settings.Title;
|
||||
var price = request.Amount;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
var choices = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
|
||||
if (choice == null)
|
||||
return NotFound("Incorrect option provided");
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
if (request.Amount > price)
|
||||
price = request.Amount;
|
||||
}
|
||||
|
||||
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
|
||||
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
|
||||
{
|
||||
return NotFound("Contribution Amount is more than is currently allowed.");
|
||||
}
|
||||
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl,
|
||||
|
||||
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
|
||||
new {invoiceId = invoice.Data.Id});
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(invoice.Data.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
@ -127,33 +255,136 @@ namespace BTCPayServer.Controllers
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
FullNotifications = true,
|
||||
}, 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
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
CurrencyNameTable _Currencies;
|
||||
private HtmlSanitizer _HtmlSanitizer;
|
||||
public CurrencyNameTable Currencies => _Currencies;
|
||||
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
ConfigureSanitizer();
|
||||
}
|
||||
|
||||
private void ConfigureSanitizer()
|
||||
{
|
||||
|
||||
_HtmlSanitizer = new HtmlSanitizer();
|
||||
|
||||
|
||||
_HtmlSanitizer.RemovingAtRule += (sender, args) =>
|
||||
{
|
||||
Debug.WriteLine("");
|
||||
|
||||
};
|
||||
_HtmlSanitizer.RemovingTag += (sender, args) =>
|
||||
{
|
||||
Debug.WriteLine("");
|
||||
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (!args.Tag.ClassList.Contains("img-fluid"))
|
||||
{
|
||||
args.Tag.ClassList.Add("img-fluid");
|
||||
}
|
||||
|
||||
args.Cancel = true;
|
||||
}
|
||||
};
|
||||
|
||||
_HtmlSanitizer.RemovingAttribute += (sender, args) =>
|
||||
{
|
||||
if (args.Tag.TagName.Equals("img",StringComparison.InvariantCultureIgnoreCase) &&
|
||||
args.Attribute.Name.Equals( "src", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
args.Reason == RemoveReason.NotAllowedUrlValue)
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
Debug.WriteLine("");
|
||||
|
||||
};
|
||||
_HtmlSanitizer.RemovingStyle += (sender, args) => { args.Cancel = true; };
|
||||
_HtmlSanitizer.AllowedAttributes.Add("class");
|
||||
_HtmlSanitizer.AllowedTags.Add("iframe");
|
||||
_HtmlSanitizer.AllowedTags.Remove("img");
|
||||
_HtmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
|
||||
_HtmlSanitizer.AllowedAttributes.Add("allowfullscreen");
|
||||
}
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType)
|
||||
public string Sanitize(string raw)
|
||||
{
|
||||
return _HtmlSanitizer.Sanitize(raw);
|
||||
}
|
||||
|
||||
public async Task<StoreData[]> GetOwnedStores(string userId)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteApp(AppData appData)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.Apps.Add(appData);
|
||||
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
|
||||
return await ctx.SaveChangesAsync() == 1;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => (allowNoUser && string.IsNullOrEmpty(userId) ) || us.ApplicationUserId == userId)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||
(us, app) =>
|
||||
new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id
|
||||
})
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType, bool includeStore = false)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var query = ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString());
|
||||
|
||||
if (includeStore)
|
||||
{
|
||||
query = query.Include(data => data.StoreData);
|
||||
}
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,10 +411,10 @@ namespace BTCPayServer.Controllers
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Description = c.GetDetailString("description"),
|
||||
Description = Sanitize(c.GetDetailString("description")),
|
||||
Id = c.Key,
|
||||
Image = c.GetDetailString("image"),
|
||||
Title = c.GetDetailString("title") ?? c.Key,
|
||||
Image = Sanitize(c.GetDetailString("image")),
|
||||
Title = Sanitize(c.GetDetailString("title") ?? c.Key),
|
||||
Price = c.GetDetail("price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
@ -211,6 +442,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public string GetDetailString(string field)
|
||||
{
|
||||
|
||||
return GetDetail(field).FirstOrDefault()?.Value?.Value;
|
||||
}
|
||||
}
|
||||
@ -246,5 +478,6 @@ namespace BTCPayServer.Controllers
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
|
||||
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
|
@ -482,7 +482,9 @@ namespace BTCPayServer.Controllers
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null,
|
||||
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
|
||||
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
|
||||
});
|
||||
|
||||
return list;
|
||||
@ -493,7 +495,7 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Export(string format, string searchTerm = null)
|
||||
{
|
||||
var model = new InvoiceExport(_NetworkProvider);
|
||||
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
|
||||
|
||||
var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue);
|
||||
var res = model.Process(invoices, format);
|
||||
|
55
BTCPayServer/Crowdfund/CrowdfundHub.cs
Normal file
55
BTCPayServer/Crowdfund/CrowdfundHub.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
{
|
||||
public class CrowdfundHub: Hub
|
||||
{
|
||||
public const string InvoiceCreated = "InvoiceCreated";
|
||||
public const string PaymentReceived = "PaymentReceived";
|
||||
public const string InfoUpdated = "InfoUpdated";
|
||||
public const string InvoiceError = "InvoiceError";
|
||||
private readonly AppsPublicController _AppsPublicController;
|
||||
|
||||
public CrowdfundHub(AppsPublicController appsPublicController)
|
||||
{
|
||||
_AppsPublicController = appsPublicController;
|
||||
}
|
||||
public async Task ListenToCrowdfundApp(string appId)
|
||||
{
|
||||
if (Context.Items.ContainsKey("app"))
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["app"].ToString());
|
||||
Context.Items.Remove("app");
|
||||
}
|
||||
Context.Items.Add("app", appId);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, appId);
|
||||
}
|
||||
|
||||
|
||||
public async Task CreateInvoice(ContributeToCrowdfund model)
|
||||
{
|
||||
model.RedirectToCheckout = false;
|
||||
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext();
|
||||
var result = await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
|
||||
switch (result)
|
||||
{
|
||||
case OkObjectResult okObjectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
|
||||
break;
|
||||
case ObjectResult objectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
|
||||
break;
|
||||
default:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
365
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
365
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
@ -0,0 +1,365 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
using YamlDotNet.Core;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
{
|
||||
public class
|
||||
CrowdfundHubStreamer
|
||||
{
|
||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app_";
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly IHubContext<CrowdfundHub> _HubContext;
|
||||
private readonly IMemoryCache _MemoryCache;
|
||||
private readonly AppsHelper _AppsHelper;
|
||||
private readonly RateFetcher _RateFetcher;
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly ILogger<CrowdfundHubStreamer> _Logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string,(string appId, bool useAllStoreInvoices,bool useInvoiceAmount)> _QuickAppInvoiceLookup =
|
||||
new ConcurrentDictionary<string, (string appId, bool useAllStoreInvoices, bool useInvoiceAmount)>();
|
||||
|
||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<CrowdfundHub> hubContext,
|
||||
IMemoryCache memoryCache,
|
||||
AppsHelper appsHelper,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
ILogger<CrowdfundHubStreamer> logger)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
_HubContext = hubContext;
|
||||
_MemoryCache = memoryCache;
|
||||
_AppsHelper = appsHelper;
|
||||
_RateFetcher = rateFetcher;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_Logger = logger;
|
||||
#pragma warning disable 4014
|
||||
InitLookup();
|
||||
#pragma warning restore 4014
|
||||
SubscribeToEvents();
|
||||
}
|
||||
|
||||
private async Task InitLookup()
|
||||
{
|
||||
var apps = await _AppsHelper.GetAllApps(null, true);
|
||||
apps = apps.Where(model => Enum.Parse<AppType>(model.AppType) == AppType.Crowdfund).ToArray();
|
||||
var tasks = new List<Task>();
|
||||
tasks.AddRange(apps.Select(app => Task.Run(async () =>
|
||||
{
|
||||
var fullApp = await _AppsHelper.GetApp(app.Id, AppType.Crowdfund, false);
|
||||
var settings = fullApp.GetSettings<AppsController.CrowdfundSettings>();
|
||||
UpdateLookup(app.Id, app.StoreId, settings);
|
||||
})));
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private void UpdateLookup(string appId, string storeId, AppsController.CrowdfundSettings settings)
|
||||
{
|
||||
_QuickAppInvoiceLookup.AddOrReplace(storeId,
|
||||
(
|
||||
appId: appId,
|
||||
useAllStoreInvoices: settings?.UseAllStoreInvoices ?? false,
|
||||
useInvoiceAmount: settings?.UseInvoiceAmount ?? false
|
||||
));
|
||||
}
|
||||
|
||||
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||
{
|
||||
return _MemoryCache.GetOrCreateAsync(GetCacheKey(appId), async entry =>
|
||||
{
|
||||
_Logger.LogInformation($"GetCrowdfundInfo {appId}");
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
var result = await GetInfo(app);
|
||||
entry.SetValue(result);
|
||||
|
||||
TimeSpan? expire = null;
|
||||
|
||||
if (result.StartDate.HasValue && result.StartDate < DateTime.Now)
|
||||
{
|
||||
expire = result.StartDate.Value.Subtract(DateTime.Now);
|
||||
}
|
||||
else if (result.EndDate.HasValue && result.EndDate > DateTime.Now)
|
||||
{
|
||||
expire = result.EndDate.Value.Subtract(DateTime.Now);
|
||||
}
|
||||
if(!expire.HasValue || expire?.TotalMinutes > 5 || expire?.TotalMilliseconds <= 0)
|
||||
{
|
||||
expire = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = expire;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent);
|
||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
||||
{
|
||||
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
|
||||
InvalidateCacheForApp(updated.AppId);
|
||||
});
|
||||
}
|
||||
|
||||
private string GetCacheKey(string appId)
|
||||
{
|
||||
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
||||
}
|
||||
|
||||
private void OnInvoiceEvent(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
if (!_QuickAppInvoiceLookup.TryGetValue(invoiceEvent.Invoice.StoreId, out var quickLookup) ||
|
||||
(!quickLookup.useAllStoreInvoices &&
|
||||
!string.IsNullOrEmpty(invoiceEvent.Invoice.OrderId) &&
|
||||
!invoiceEvent.Invoice.OrderId.Equals($"{CrowdfundInvoiceOrderIdPrefix}{quickLookup.appId}", StringComparison.InvariantCulture)
|
||||
))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (invoiceEvent.Name)
|
||||
{
|
||||
case InvoiceEvent.ReceivedPayment:
|
||||
var data = invoiceEvent.Payment.GetCryptoPaymentData();
|
||||
_HubContext.Clients.Group(quickLookup.appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
Enum.GetName(typeof(PaymentTypes),
|
||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
} );
|
||||
|
||||
InvalidateCacheForApp(quickLookup.appId);
|
||||
break;
|
||||
case InvoiceEvent.Created:
|
||||
case InvoiceEvent.MarkedInvalid:
|
||||
case InvoiceEvent.MarkedCompleted:
|
||||
if (quickLookup.useInvoiceAmount)
|
||||
{
|
||||
InvalidateCacheForApp(quickLookup.appId);
|
||||
}
|
||||
break;
|
||||
case InvoiceEvent.Completed:
|
||||
InvalidateCacheForApp(quickLookup.appId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateCacheForApp(string appId)
|
||||
{
|
||||
_Logger.LogInformation($"App {appId} cache invalidated");
|
||||
_MemoryCache.Remove(GetCacheKey(appId));
|
||||
|
||||
GetCrowdfundInfo(appId).ContinueWith(task =>
|
||||
{
|
||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
|
||||
}, TaskScheduler.Current);
|
||||
|
||||
}
|
||||
|
||||
private async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
|
||||
{
|
||||
decimal result = 0;
|
||||
|
||||
var ratesTask = _RateFetcher .FetchRates(
|
||||
stats.Keys
|
||||
.Select((x) => new CurrencyPair( primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
|
||||
.Distinct()
|
||||
.ToHashSet(),
|
||||
rateRules);
|
||||
|
||||
var finalTasks = new List<Task>();
|
||||
foreach (var rateTask in ratesTask)
|
||||
{
|
||||
finalTasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var tResult = await rateTask.Value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null) return;
|
||||
|
||||
foreach (var stat in stats)
|
||||
{
|
||||
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, rateTask.Key.Right,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
result += (1m / rate.Value) * stat.Value;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(finalTasks);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool usePaymentData = true)
|
||||
{
|
||||
if(usePaymentData){
|
||||
var payments = invoices.SelectMany(entity => entity.GetPayments());
|
||||
|
||||
var groupedByMethod = payments.GroupBy(entity => entity.GetPaymentMethodId());
|
||||
|
||||
return groupedByMethod.ToDictionary(entities => entities.Key.ToString(),
|
||||
entities => entities.Sum(entity => entity.GetCryptoPaymentData().GetValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
return invoices
|
||||
.GroupBy(entity => entity.ProductInformation.Currency)
|
||||
.ToDictionary(
|
||||
entities => entities.Key,
|
||||
entities => entities.Sum(entity => entity.ProductInformation.Price));
|
||||
}
|
||||
}
|
||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
|
||||
{
|
||||
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
||||
|
||||
var resetEvery = settings.StartDate.HasValue? settings.ResetEvery : CrowdfundResetEvery.Never;
|
||||
DateTime? lastResetDate = null;
|
||||
DateTime? nextResetDate = null;
|
||||
if (resetEvery != CrowdfundResetEvery.Never)
|
||||
{
|
||||
lastResetDate = settings.StartDate.Value;
|
||||
|
||||
nextResetDate = lastResetDate.Value;
|
||||
while (DateTime.Now >= nextResetDate)
|
||||
{
|
||||
lastResetDate = nextResetDate;
|
||||
switch (resetEvery)
|
||||
{
|
||||
case CrowdfundResetEvery.Hour:
|
||||
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
|
||||
break;
|
||||
case CrowdfundResetEvery.Day:
|
||||
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
|
||||
break;
|
||||
case CrowdfundResetEvery.Month:
|
||||
|
||||
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
|
||||
break;
|
||||
case CrowdfundResetEvery.Year:
|
||||
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invoices = await GetInvoicesForApp(settings.UseAllStoreInvoices? null : appData.Id, lastResetDate);
|
||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray();
|
||||
var pendingInvoices = invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray();
|
||||
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||
|
||||
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.UseInvoiceAmount);
|
||||
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.UseInvoiceAmount);
|
||||
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
paymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||
pendingPaymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
|
||||
|
||||
|
||||
|
||||
var perkCount = invoices
|
||||
.Where(entity => !string.IsNullOrEmpty( entity.ProductInformation.ItemCode))
|
||||
.GroupBy(entity => entity.ProductInformation.ItemCode)
|
||||
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
||||
|
||||
var perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
if (settings.SortPerksByPopularity)
|
||||
{
|
||||
var ordered = perkCount.OrderByDescending(pair => pair.Value);
|
||||
var newPerksOrder = ordered
|
||||
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
|
||||
.Where(matchingPerk => matchingPerk != null)
|
||||
.ToList();
|
||||
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
|
||||
newPerksOrder.AddRange(remainingPerks);
|
||||
perks = newPerksOrder.ToArray();
|
||||
}
|
||||
return new ViewCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Tagline = settings.Tagline,
|
||||
Description = settings.Description,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
StoreId = appData.StoreDataId,
|
||||
AppId = appData.Id,
|
||||
StartDate = settings.StartDate?.ToUniversalTime(),
|
||||
EndDate = settings.EndDate?.ToUniversalTime(),
|
||||
TargetAmount = settings.TargetAmount,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Perks = perks,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
PerkCount = perkCount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery),settings.ResetEvery),
|
||||
CurrencyData = _AppsHelper.GetCurrencyData(settings.TargetCurrency, true),
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = invoices.Length,
|
||||
CurrentPendingAmount = currentPendingAmount,
|
||||
CurrentAmount = currentAmount,
|
||||
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
|
||||
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100,
|
||||
LastUpdated = DateTime.Now,
|
||||
PaymentStats = paymentStats,
|
||||
PendingPaymentStats = pendingPaymentStats,
|
||||
LastResetDate = lastResetDate,
|
||||
NextResetDate = nextResetDate
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(string appId, DateTime? startDate = null)
|
||||
{
|
||||
return await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
OrderId = appId == null? null : new []{$"{CrowdfundInvoiceOrderIdPrefix}{appId}"},
|
||||
Status = new string[]{
|
||||
InvoiceState.ToString(InvoiceStatus.New),
|
||||
InvoiceState.ToString(InvoiceStatus.Paid),
|
||||
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
||||
InvoiceState.ToString(InvoiceStatus.Complete)},
|
||||
StartDate = startDate
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -38,6 +38,7 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
@ -74,6 +75,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<CrowdfundHubStreamer>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -158,6 +160,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
services.AddTransient<AppsPublicController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
// bundling
|
||||
|
@ -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 =>
|
||||
{
|
||||
@ -99,7 +100,7 @@ namespace BTCPayServer.Hosting
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequiredLength = 7;
|
||||
options.Password.RequiredLength = 6;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
@ -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 =>
|
||||
|
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
public class UpdateCrowdfundViewModel
|
||||
{
|
||||
[Required] [MaxLength(30)] public string Title { get; set; }
|
||||
|
||||
[MaxLength(50)] public string Tagline { get; set; }
|
||||
|
||||
[Required] public string Description { get; set; }
|
||||
|
||||
[Display(Name = "Featured Image")]
|
||||
public string MainImageUrl { get; set; }
|
||||
|
||||
public string NotificationUrl { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Enable background animations on new payments")]
|
||||
public bool AnimationsEnabled { get; set; } = true;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Enable sounds on new payments")]
|
||||
public bool SoundsEnabled { get; set; } = true;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Enable Disqus Comments")]
|
||||
public bool DisqusEnabled { get; set; } = true;
|
||||
|
||||
[Display(Name = "Disqus Shortname")] public string DisqusShortname { get; set; }
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(5)]
|
||||
[Display(Name = "The primary currency used for targets and stats. (e.g. BTC, LTC, USD, etc.)")]
|
||||
public string TargetCurrency { get; set; } = "BTC";
|
||||
|
||||
[Display(Name = "Set a Target amount ")]
|
||||
public decimal? TargetAmount { get; set; }
|
||||
|
||||
|
||||
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
|
||||
|
||||
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
|
||||
|
||||
public int ResetEveryAmount { get; set; } = 1;
|
||||
|
||||
|
||||
[Display(Name = "Do not allow additional contributions after target has been reached")]
|
||||
public bool EnforceTargetAmount { get; set; }
|
||||
|
||||
[Display(Name = "Contribution Perks Template")]
|
||||
public string PerksTemplate { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom bootstrap CSS file")]
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
|
||||
[Display(Name = "Base the contributed goal amount on the invoice amount and not actual payments")]
|
||||
public bool UseInvoiceAmount { get; set; }
|
||||
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
|
||||
public string AppId { get; set; }
|
||||
[Display(Name = "Sort contribution perks by popularity")]
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
[Display(Name = "Display contribution ranking")]
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
}
|
||||
|
||||
public enum CrowdfundResetEvery
|
||||
{
|
||||
Never,
|
||||
Hour,
|
||||
Day,
|
||||
Month,
|
||||
Year
|
||||
}
|
||||
}
|
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
|
69
BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs
Normal file
69
BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
public class ViewCrowdfundViewModel
|
||||
{
|
||||
public string StatusMessage{ get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string AppId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string MainImageUrl { get; set; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
public string TargetCurrency { get; set; }
|
||||
public decimal? TargetAmount { get; set; }
|
||||
public bool EnforceTargetAmount { get; set; }
|
||||
|
||||
public CrowdfundInfo Info { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public ViewPointOfSaleViewModel.Item[] Perks { get; set; }
|
||||
public bool DisqusEnabled { get; set; }
|
||||
public bool SoundsEnabled { get; set; }
|
||||
public string DisqusShortname { get; set; }
|
||||
public bool AnimationsEnabled { get; set; }
|
||||
public int ResetEveryAmount { get; set; }
|
||||
public string ResetEvery { get; set; }
|
||||
|
||||
public Dictionary<string, int> PerkCount { get; set; }
|
||||
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
|
||||
public class CrowdfundInfo
|
||||
{
|
||||
public int TotalContributors { get; set; }
|
||||
public decimal CurrentPendingAmount { get; set; }
|
||||
public decimal CurrentAmount { get; set; }
|
||||
public decimal? ProgressPercentage { get; set; }
|
||||
public decimal? PendingProgressPercentage { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public Dictionary<string, decimal> PaymentStats { get; set; }
|
||||
public Dictionary<string, decimal> PendingPaymentStats { get; set; }
|
||||
public DateTime? LastResetDate { get; set; }
|
||||
public DateTime? NextResetDate { get; set; }
|
||||
}
|
||||
|
||||
public bool Started => !StartDate.HasValue || DateTime.Now.ToUniversalTime() > StartDate;
|
||||
|
||||
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
}
|
||||
|
||||
public class ContributeToCrowdfund
|
||||
{
|
||||
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
|
||||
[Required] public decimal Amount { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string ChoiceKey { get; set; }
|
||||
public bool RedirectToCheckout { get; set; }
|
||||
public string RedirectUrl { get; set; }
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
|
||||
@ -32,6 +33,7 @@
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||
|
@ -7,6 +7,7 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
public enum AppType
|
||||
{
|
||||
PointOfSale
|
||||
PointOfSale,
|
||||
Crowdfund
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices.Export
|
||||
@ -12,10 +13,12 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
public class InvoiceExport
|
||||
{
|
||||
public BTCPayNetworkProvider Networks { get; }
|
||||
public CurrencyNameTable Currencies { get; }
|
||||
|
||||
public InvoiceExport(BTCPayNetworkProvider networks)
|
||||
public InvoiceExport(BTCPayNetworkProvider networks, CurrencyNameTable currencies)
|
||||
{
|
||||
Networks = networks;
|
||||
Currencies = currencies;
|
||||
}
|
||||
public string Process(InvoiceEntity[] invoices, string fileFormat)
|
||||
{
|
||||
@ -52,6 +55,7 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
|
||||
{
|
||||
var exportList = new List<ExportInvoiceHolder>();
|
||||
var currency = Currencies.GetNumberFormatInfo(invoice.ProductInformation.Currency, true);
|
||||
|
||||
var invoiceDue = invoice.ProductInformation.Price;
|
||||
// in this first version we are only exporting invoices that were paid
|
||||
@ -60,7 +64,6 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
// not accounted payments are payments which got double spent like RBfed
|
||||
if (!payment.Accounted)
|
||||
continue;
|
||||
|
||||
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
|
||||
var pdata = payment.GetCryptoPaymentData();
|
||||
|
||||
@ -77,13 +80,13 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain",
|
||||
Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)),
|
||||
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
|
||||
PaidCurrency = (pdata.GetValue() * pmethod.Rate).ToString(CultureInfo.InvariantCulture),
|
||||
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
|
||||
// Adding NetworkFee because Paid doesn't take into account network fees
|
||||
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
|
||||
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
|
||||
// while looking just at export you could sum Paid and assume merchant "received payments"
|
||||
NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture),
|
||||
InvoiceDue = invoiceDue,
|
||||
InvoiceDue = Math.Round(invoiceDue, currency.NumberDecimalDigits),
|
||||
OrderId = invoice.OrderId,
|
||||
StoreId = invoice.StoreId,
|
||||
InvoiceId = invoice.Id,
|
||||
|
@ -344,6 +344,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
InvoiceResponse dto = new InvoiceResponse
|
||||
{
|
||||
Id = Id,
|
||||
StoreId = StoreId,
|
||||
OrderId = OrderId,
|
||||
PosData = PosData,
|
||||
CurrentTime = DateTimeOffset.UtcNow,
|
||||
|
@ -162,6 +162,8 @@ namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("name should not be empty", nameof(name));
|
||||
if (ownerId == null)
|
||||
throw new ArgumentNullException(nameof(ownerId));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
StoreData store = new StoreData
|
||||
|
243
BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml
Normal file
243
BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml
Normal file
@ -0,0 +1,243 @@
|
||||
@using BTCPayServer.Hubs
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@model UpdateCrowdfundViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Crowdfund";
|
||||
}
|
||||
<section>
|
||||
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Contribution Perks Management</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Title" class="control-label"></label>*
|
||||
<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 richtext"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="TargetCurrency" class="control-label"></label>
|
||||
<input asp-for="TargetCurrency" class="form-control" />
|
||||
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="TargetAmount" class="control-label"></label>
|
||||
<input asp-for="TargetAmount" class="form-control" />
|
||||
<span asp-validation-for="TargetAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="StartDate" class="control-label"></label>
|
||||
<input asp-for="StartDate" class="form-control datetime " />
|
||||
<span asp-validation-for="StartDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ResetEvery" class="control-label"></label>
|
||||
<div class="input-group">
|
||||
|
||||
<input type="number" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control">
|
||||
<select class="custom-select" asp-for="ResetEvery">
|
||||
@foreach (var opt in Model.ResetEveryValues)
|
||||
{
|
||||
<option value="@opt">@opt</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="EndDate" class="control-label"></label>
|
||||
<input asp-for="EndDate" class="form-control datetime" />
|
||||
<span asp-validation-for="EndDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Contribution Perks</label>
|
||||
<div class="mb-3">
|
||||
<a class="js-product-add btn btn-secondary" href="#" data-toggle="modal" data-target="#product-modal"><i class="fa fa-plus fa-fw"></i> Add Perk</a>
|
||||
</div>
|
||||
<div class="js-products bg-light row p-3">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PerksTemplate" class="control-label"></label>
|
||||
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
|
||||
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="control-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<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="EmbeddedCSS" class="control-label"></label>
|
||||
|
||||
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
|
||||
<span asp-validation-for="EmbeddedCSS" 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="SortPerksByPopularity"></label>
|
||||
<input asp-for="SortPerksByPopularity" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisplayPerksRanking"></label>
|
||||
<input asp-for="DisplayPerksRanking" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="DisplayPerksRanking" 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">
|
||||
<label asp-for="UseInvoiceAmount"></label>
|
||||
<input asp-for="UseInvoiceAmount" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="UseInvoiceAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="UseAllStoreInvoices"></label>
|
||||
<input asp-for="UseAllStoreInvoices" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SoundsEnabled"></label>
|
||||
<input asp-for="SoundsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AnimationsEnabled"></label>
|
||||
<input asp-for="AnimationsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisqusEnabled"></label>
|
||||
<input asp-for="DisqusEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="DisqusEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisqusShortname" class="control-label"></label>
|
||||
<input asp-for="DisqusShortname" class="form-control" />
|
||||
<span asp-validation-for="DisqusShortname" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" />
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{Model.AppId}")">Invoices generated by app</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId">View App</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
|
||||
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.css"></bundle>
|
||||
|
||||
<script id="template-product-item" type="text/template">
|
||||
<div class="col-sm-4 col-md-3 mb-3">
|
||||
<div class="card">
|
||||
{image}
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{title}</h6>
|
||||
<a href="#" class="js-product-edit btn btn-primary" data-toggle="modal" data-target="#product-modal">Edit</a>
|
||||
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="template-product-content" type="text/template">
|
||||
<div class="mb-3">
|
||||
<input class="js-product-id" type="hidden" name="id" value="{id}">
|
||||
<input class="js-product-index" type="hidden" name="index" value="{index}">
|
||||
<div class="form-row">
|
||||
<div class="col-sm-6">
|
||||
<label>Title</label>*
|
||||
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input type="number" step="any" class="js-product-price form-control mb-2" value="{price}" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
<select class="js-product-custom form-control">
|
||||
{custom}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Image</label>
|
||||
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Description</label>
|
||||
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
}
|
||||
|
@ -159,7 +159,7 @@
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input type="text" class="js-product-price form-control mb-2" value="{price}" />
|
||||
<input type="number" class="js-product-price form-control mb-2" value="{price}" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
|
@ -0,0 +1,78 @@
|
||||
@using Microsoft.EntityFrameworkCore.Internal
|
||||
@model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund
|
||||
|
||||
<form method="post">
|
||||
@foreach (var item in Model.ViewCrowdfundViewModel.Perks)
|
||||
{
|
||||
<div class="card mb-4 perk expanded" id="@item.Id">
|
||||
@if (Model.ViewCrowdfundViewModel.DisplayPerksRanking && Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
|
||||
{
|
||||
<span class="btn btn-sm rounded-circle px-0 btn-primary perk-badge">#@(Model.ViewCrowdfundViewModel.Perks.IndexOf(item)+1)</span>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(item.Image))
|
||||
{
|
||||
<img class="card-img-top" src="@item.Image"/>
|
||||
}
|
||||
<div class="card-body">
|
||||
<div class="card-title d-flex justify-content-between">
|
||||
<label class="h5">
|
||||
|
||||
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended && (item.Price.Value > 0 || item.Custom))
|
||||
{
|
||||
|
||||
<input type="radio" asp-for="ChoiceKey" value="@item.Id"/>
|
||||
}
|
||||
@(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
|
||||
</label>
|
||||
<span class="text-muted">
|
||||
@if (item.Price.Value > 0)
|
||||
{
|
||||
@item.Price.Value
|
||||
if (item.Custom)
|
||||
{
|
||||
Html.Raw("or more");
|
||||
}
|
||||
}
|
||||
else if (item.Custom)
|
||||
{
|
||||
Html.Raw("Any amount");
|
||||
}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<p class="card-text overflow-hidden">@Html.Raw(item.Description)</p>
|
||||
|
||||
</div>
|
||||
@if (Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
|
||||
{
|
||||
<div class="card-footer d-flex justify-content-between">
|
||||
<span></span>
|
||||
<span> @Model.ViewCrowdfundViewModel.PerkCount[item.Id] Contributors</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended)
|
||||
{
|
||||
<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>
|
154
BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml
Normal file
154
BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml
Normal file
@ -0,0 +1,154 @@
|
||||
@using BTCPayServer.Models.AppViewModels
|
||||
@using Microsoft.CodeAnalysis.CSharp.Syntax
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
|
||||
|
||||
<div class="container p-0">
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
|
||||
{
|
||||
<img class="card-img-top" src="@Model.MainImageUrl"/>
|
||||
}
|
||||
<div class="d-flex justify-content-between px-2">
|
||||
<h1>
|
||||
@Model.Title
|
||||
@if (!Model.Started && Model.StartDate.HasValue)
|
||||
{
|
||||
<span class="h6 text-muted" >
|
||||
Starts @Model.StartDate.Value.Subtract(DateTime.Now.ToUniversalTime())
|
||||
</span>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
|
||||
{
|
||||
<span class="h6 text-muted" >
|
||||
Ends @Model.EndDate.Value.Subtract(DateTime.Now.ToUniversalTime())
|
||||
</span>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
|
||||
{
|
||||
<span class="h6 text-muted" :title="startDate" title="No set end date">
|
||||
Currently Active!
|
||||
</span>
|
||||
}
|
||||
|
||||
</h1>
|
||||
@if (Model.TargetAmount.HasValue)
|
||||
{
|
||||
<span class="mt-3">
|
||||
<span class="h5">@Model.TargetAmount @Model.TargetCurrency</span>
|
||||
@if (Model.ResetEveryAmount > 0 && Model.ResetEvery != nameof(CrowdfundResetEvery.Never))
|
||||
{
|
||||
<span> Dynamic</span>
|
||||
}
|
||||
@if (Model.EnforceTargetAmount)
|
||||
{
|
||||
<span >Hardcap Goal <span class="fa fa-question-circle" ></span></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span >Softcap Goal <span class="fa fa-question-circle" ></span> </span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@if (Model.TargetAmount.HasValue)
|
||||
{
|
||||
<div class="progress w-100 rounded-0 " >
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width:@(Model.Info.ProgressPercentage + "%")"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
style="width:@(Model.Info.PendingProgressPercentage + "%")"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<div class="row py-2 text-center">
|
||||
<div class="col-sm border-right" id="raised-amount">
|
||||
<h5>@(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount) @Model.TargetCurrency </h5>
|
||||
<h5 class="text-muted">Raised</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right">
|
||||
<h5>@(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h5>
|
||||
<h5 class="text-muted">Of Goal</h5>
|
||||
</div>
|
||||
<div class="col-sm text-right">
|
||||
<h5>
|
||||
@Model.Info.TotalContributors
|
||||
</h5>
|
||||
<h5 class="text-muted">Contributors</h5>
|
||||
</div>
|
||||
@if (Model.Started && !Model.Ended)
|
||||
{
|
||||
<div class="col-sm">
|
||||
<h5>
|
||||
@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
|
||||
|
||||
</h5>
|
||||
<h5 class="text-muted">Ends</h5>
|
||||
</div>
|
||||
} else if (!Model.Started)
|
||||
{
|
||||
<div class="col-sm">
|
||||
<h5>
|
||||
@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
|
||||
</h5>
|
||||
<h5 class="text-muted">Starts</h5>
|
||||
</div>
|
||||
}else if (Model.Ended)
|
||||
{
|
||||
|
||||
<div class="col-sm" id="inactive-campaign">
|
||||
<h5>
|
||||
Campaign
|
||||
</h5>
|
||||
<h5 >not active</h5>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-title">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 ">
|
||||
<h2 class="text-muted" >@Model.Tagline</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden">@Html.Raw(Model.Description)</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<partial
|
||||
name="Crowdfund/ContributeForm"
|
||||
model="@(new ContributeToCrowdfund()
|
||||
{
|
||||
ViewCrowdfundViewModel = Model,
|
||||
RedirectToCheckout = true,
|
||||
|
||||
})"></partial>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-muted d-flex" >
|
||||
|
||||
<div class="align-self-end pr-4">Updated @Model.Info.LastUpdated</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
306
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal file
306
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal file
@ -0,0 +1,306 @@
|
||||
<div class="container p-0" id="app" v-cloak>
|
||||
<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="d-flex justify-content-between px-2">
|
||||
<h1>
|
||||
{{srvModel.title}}
|
||||
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate">
|
||||
Starts {{startDateRelativeTime}}
|
||||
</span>
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate">
|
||||
Ends {{endDateRelativeTime}}
|
||||
</span>
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date">
|
||||
Currently Active!
|
||||
</span>
|
||||
|
||||
</h1>
|
||||
|
||||
<span v-if="srvModel.targetAmount" class="mt-3">
|
||||
<span class="h5">{{srvModel.targetAmount}} {{targetCurrency}}</span>
|
||||
<span v-if="srvModel.resetEvery !== 'Never'" v-b-tooltip
|
||||
:title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" >Dynamic </span>
|
||||
<span v-if="srvModel.enforceTargetAmount">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
|
||||
<span v-else>Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
:aria-valuenow="srvModel.info.progressPercentage"
|
||||
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
|
||||
aria-valuemin="0"
|
||||
v-b-tooltip :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% contributions'"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
: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="card-body">
|
||||
|
||||
|
||||
<div class="row py-2 text-center">
|
||||
<div class="col-sm border-right" id="raised-amount">
|
||||
<h5>{{ raisedAmount }} {{targetCurrency}} </h5>
|
||||
<h5 class="text-muted">Raised</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right" id="goal-raised">
|
||||
<h5>{{ percentageRaisedAmount }}%</h5>
|
||||
<h5 class="text-muted">Of Goal</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right">
|
||||
<h5>
|
||||
{{srvModel.info.totalContributors}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Contributors</h5>
|
||||
</div>
|
||||
<div class="col-sm" v-if="endDiff" id="campaign-dates-started">
|
||||
<h5>
|
||||
{{endDiff}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Left</h5>
|
||||
<b-tooltip target="campaign-dates-started" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="col-sm" v-if="startDiff" id="campaign-dates-not-started">
|
||||
<h5>
|
||||
{{startDiff}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Left to start</h5>
|
||||
|
||||
<b-tooltip target="campaign-dates-ended" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="col-sm" v-if="ended" id="campaign-dates-ended">
|
||||
<h5>
|
||||
Campaign
|
||||
</h5>
|
||||
<h5 >not active</h5>
|
||||
|
||||
<b-tooltip target="campaign-dates-not-started" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-tooltip target="raised-amount" v-if="paymentStats && paymentStats.length > 0">
|
||||
<ul class="p-0 text-uppercase">
|
||||
<li v-for="stat of paymentStats" class="list-unstyled">
|
||||
|
||||
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
<b-tooltip target="goal-raised" v-if="srvModel.resetEvery !== 'Never'">
|
||||
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
|
||||
</b-tooltip>
|
||||
|
||||
|
||||
<div class="card-title">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-lg-9">
|
||||
|
||||
|
||||
<h2 class="text-muted" v-if="srvModel.tagline">{{srvModel.tagline}}</h2>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4 col-lg-3">
|
||||
<button v-if="active" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="srvModel.disqusEnabled">
|
||||
<b-tabs>
|
||||
<b-tab title="Details"active>
|
||||
<div class="row ">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:active="active"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
|
||||
</contribute>
|
||||
</div>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab title="Discussion" >
|
||||
<div id="disqus_thread" class=" mt-2"></div>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
</template>
|
||||
<template v-else>
|
||||
<hr/>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:active="active"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
|
||||
</contribute>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-footer text-muted d-flex" v-if="srvModel.animationsEnabled || srvModel.soundsEnabled">
|
||||
|
||||
<div class="align-self-end pr-4">Updated {{lastUpdated}}</div>
|
||||
<div class="form-check mx-1" v-if="srvModel.animationsEnabled || animation">
|
||||
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">
|
||||
<label class="form-check-label" for="cbAnime">
|
||||
Animations
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mx-1" v-if="srvModel.soundsEnabled|| sound">
|
||||
<input class="form-check-input" type="checkbox" id="cbSounds" v-model="sound">
|
||||
<label class="form-check-label" for="cbSounds">
|
||||
Sounds
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
|
||||
|
||||
<contribute v-if="contributeModalOpen"
|
||||
:target-currency="srvModel.targetCurrency"
|
||||
:active="active"
|
||||
:perks="srvModel.perks"
|
||||
:in-modal="true">
|
||||
</contribute>
|
||||
</b-modal>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/x-template" id="perks-template">
|
||||
<div>
|
||||
<perk v-if="!perks || perks.length ===0" :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
|
||||
:in-modal="inModal">
|
||||
</perk>
|
||||
<perk v-for="(perk, index) in perks" :perk="perk" :target-currency="targetCurrency" :active="active" :display-perks-ranking="displayPerksRanking" :index="index"
|
||||
:in-modal="inModal">
|
||||
</perk>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="perk-template">
|
||||
<div class="card mb-4 perk" v-bind:class="{ 'expanded': expanded, 'unexpanded': !expanded }" 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-secondary': index!=0}">#{{index+1}}</span>
|
||||
|
||||
|
||||
<div class="perk-zoom " v-if="canExpand">
|
||||
<div class="perk-zoom-bg bg-primary"> </div>
|
||||
<div class="perk-zoom-text w-100 text-center text-white font-weight-bold">
|
||||
Select this contribution perk
|
||||
</div>
|
||||
</div>
|
||||
<form v-on:submit='onContributeFormSubmit'>
|
||||
<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" >
|
||||
<span class="h5">{{perk.title? perk.title : perk.id}} </span>
|
||||
<span class="text-muted" >
|
||||
<template v-if="perk.price.value">{{perk.price.value.noExponents()}}
|
||||
{{targetCurrency}}
|
||||
<template v-if="perk.custom">or more</template>
|
||||
</template>
|
||||
<template v-else-if="!perk.price.value && perk.custom">
|
||||
Any amount
|
||||
</template>
|
||||
|
||||
</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">
|
||||
<input
|
||||
:disabled="!active"
|
||||
:readonly="!perk.custom"
|
||||
class="form-control"
|
||||
type="number"
|
||||
v-model="amount"
|
||||
:min="perk.price.value"
|
||||
step="any"
|
||||
placeholder="Contribution Amount"
|
||||
required >
|
||||
<div class="input-group-append">
|
||||
<span class='input-group-text'>{{targetCurrency}}</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!active"
|
||||
type="submit">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between" v-if="perk.sold">
|
||||
<span ></span>
|
||||
<span x >{{perk.sold}} Contributor{{perk.sold > 1? "s": ""}}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-template" id="contribute-template">
|
||||
<div>
|
||||
<h3 v-if="!inModal" class="mb-3">Contribute</h3>
|
||||
<perks
|
||||
:perks="perks"
|
||||
:in-modal="inModal"
|
||||
:display-perks-ranking="displayPerksRanking"
|
||||
:target-currency="targetCurrency"
|
||||
:active="active">
|
||||
</perks>
|
||||
</div>
|
||||
</script>
|
58
BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml
Normal file
58
BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml
Normal file
@ -0,0 +1,58 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Title;
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<title>@Model.Title</title>
|
||||
<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="@Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
}
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
|
||||
}
|
||||
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
@if (Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
}
|
||||
else
|
||||
{
|
||||
<noscript>
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
</noscript>
|
||||
|
||||
if (Model.AnimationsEnabled)
|
||||
{
|
||||
<canvas id="fireworks"></canvas>
|
||||
}
|
||||
@await Html.PartialAsync("Crowdfund/VueCrowdfund", Model)
|
||||
}
|
||||
|
||||
</body>
|
||||
</html>
|
@ -49,6 +49,7 @@
|
||||
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
</a>
|
||||
<a href="https://docs.btcpayserver.org/features/accounting" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||
<a asp-action="Export" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">CSV</a>
|
||||
<a asp-action="Export" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">JSON</a>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<p>
|
||||
<span>SSH servies are used by the maintenance operations<br /></span>
|
||||
<span>SSH services are used by the maintenance operations<br /></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -53,6 +53,7 @@
|
||||
"wwwroot/checkout/**/*.js"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/lightning-node-info-bundle.min.js",
|
||||
"inputFiles": [
|
||||
@ -71,5 +72,57 @@
|
||||
"wwwroot/cart/js/cart.js",
|
||||
"wwwroot/cart/js/cart.jquery.js"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle-1.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/vuejs/vue.min.js",
|
||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||
"wwwroot/vendor/vue-toasted/vue-toasted.min.js",
|
||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
|
||||
"wwwroot/vendor/signalr/signalr.js",
|
||||
"wwwroot/vendor/animejs/anime.min.js",
|
||||
"wwwroot/modal/btcpay.js",
|
||||
"wwwroot/crowdfund/**/*.js"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle-2.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/moment/moment.js"
|
||||
],
|
||||
"minify": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-admin-bundle.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/highlightjs/highlight.min.js",
|
||||
"wwwroot/vendor/summernote/summernote-bs4.js",
|
||||
"wwwroot/vendor/flatpickr/flatpickr.js",
|
||||
"wwwroot/crowdfund-admin/**/*.js",
|
||||
"wwwroot/products/js/products.js",
|
||||
"wwwroot/products/js/products.jquery.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-admin-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/highlightjs/default.min.css",
|
||||
"wwwroot/vendor/summernote/summernote-bs4.css",
|
||||
"wwwroot/vendor/flatpickr/flatpickr.min.css",
|
||||
"wwwroot/crowdfund-admin/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/font-awesome/css/font-awesome.min.css",
|
||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
|
||||
"wwwroot/crowdfund/**/*.css"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
8
BTCPayServer/wwwroot/crowdfund-admin/main.js
Normal file
8
BTCPayServer/wwwroot/crowdfund-admin/main.js
Normal file
@ -0,0 +1,8 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function() {
|
||||
|
||||
$(".richtext").summernote();
|
||||
$(".datetime").flatpickr({
|
||||
enableTime: true
|
||||
});
|
||||
});
|
298
BTCPayServer/wwwroot/crowdfund/app.js
Normal file
298
BTCPayServer/wwwroot/crowdfund/app.js
Normal file
@ -0,0 +1,298 @@
|
||||
var app = null;
|
||||
var eventAggregator = new Vue();
|
||||
|
||||
function addLoadEvent(func) {
|
||||
var oldonload = window.onload;
|
||||
if (typeof window.onload != 'function') {
|
||||
window.onload = func;
|
||||
} else {
|
||||
window.onload = function() {
|
||||
if (oldonload) {
|
||||
oldonload();
|
||||
}
|
||||
func();
|
||||
}
|
||||
}
|
||||
}
|
||||
addLoadEvent(function (ev) {
|
||||
Vue.use(Toasted);
|
||||
|
||||
Vue.component('contribute', {
|
||||
props: ["targetCurrency", "active", "perks", "inModal", "displayPerksRanking"],
|
||||
template: "#contribute-template"
|
||||
});
|
||||
|
||||
Vue.component('perks', {
|
||||
props: ["perks", "targetCurrency", "active", "inModal","displayPerksRanking"],
|
||||
template: "#perks-template"
|
||||
});
|
||||
|
||||
Vue.component('perk', {
|
||||
props: ["perk", "targetCurrency", "active", "inModal", "displayPerksRanking", "index"],
|
||||
template: "#perk-template",
|
||||
data: function () {
|
||||
return {
|
||||
amount: null,
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canExpand: function(){
|
||||
return !this.expanded && this.active && (this.perk.price.value || this.perk.custom)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onContributeFormSubmit: function (e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if(!this.active){
|
||||
return;
|
||||
}
|
||||
|
||||
eventAggregator.$emit("contribute", {amount: this.amount, choiceKey: this.perk.id});
|
||||
},
|
||||
expand: function(){
|
||||
if(this.canExpand){
|
||||
this.expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
this.amount = this.perk.price.value;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
app = new Vue({
|
||||
el: '#app',
|
||||
data: function(){
|
||||
return {
|
||||
srvModel: window.srvModel,
|
||||
connectionStatus: "",
|
||||
endDate: "",
|
||||
startDate: "",
|
||||
startDateRelativeTime: "",
|
||||
endDateRelativeTime: "",
|
||||
started: false,
|
||||
ended: false,
|
||||
contributeModalOpen: false,
|
||||
endDiff: "",
|
||||
startDiff: "",
|
||||
active: true,
|
||||
animation: true,
|
||||
sound: true,
|
||||
lastUpdated:""
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
raisedAmount: function(){
|
||||
return parseFloat(this.srvModel.info.currentAmount + this.srvModel.info.currentPendingAmount ).toFixed(this.srvModel.currencyData.divisibility) ;
|
||||
},
|
||||
percentageRaisedAmount: function(){
|
||||
return parseFloat(this.srvModel.info.progressPercentage + this.srvModel.info.pendingProgressPercentage ).toFixed(2);
|
||||
},
|
||||
targetCurrency: function(){
|
||||
return this.srvModel.targetCurrency.toUpperCase();
|
||||
},
|
||||
paymentStats: function(){
|
||||
var result= [];
|
||||
|
||||
var combinedStats = {};
|
||||
|
||||
|
||||
var keys = Object.keys(this.srvModel.info.paymentStats);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if(combinedStats[keys[i]]){
|
||||
combinedStats[keys[i]] +=this.srvModel.info.paymentStats[keys[i]];
|
||||
}else{
|
||||
combinedStats[keys[i]] =this.srvModel.info.paymentStats[keys[i]];
|
||||
}
|
||||
}
|
||||
|
||||
keys = Object.keys(this.srvModel.info.pendingPaymentStats);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if(combinedStats[keys[i]]){
|
||||
combinedStats[keys[i]] +=this.srvModel.info.pendingPaymentStats[keys[i]];
|
||||
}else{
|
||||
combinedStats[keys[i]] =this.srvModel.info.pendingPaymentStats[keys[i]];
|
||||
}
|
||||
}
|
||||
|
||||
keys = Object.keys(combinedStats);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var newItem = {key:keys[i], value: combinedStats[keys[i]], label: keys[i].replace("_","")};
|
||||
result.push(newItem);
|
||||
|
||||
}
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var current = result[i];
|
||||
if(current.label.endsWith("LightningLike")){
|
||||
current.label = current.label.substr(0,current.label.indexOf("LightningLike"));
|
||||
current.lightning = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
perks: function(){
|
||||
var result = [];
|
||||
for (var i = 0; i < this.srvModel.perks.length; i++) {
|
||||
var currentPerk = this.srvModel.perks[i];
|
||||
if(this.srvModel.perkCount.hasOwnProperty(currentPerk.id)){
|
||||
currentPerk.sold = this.srvModel.perkCount[currentPerk.id];
|
||||
}
|
||||
result.push(currentPerk);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateComputed: function () {
|
||||
if (this.srvModel.endDate) {
|
||||
var endDateM = moment(this.srvModel.endDate);
|
||||
this.endDate = endDateM.format('MMMM Do YYYY');
|
||||
this.endDateRelativeTime = endDateM.fromNow();
|
||||
this.ended = endDateM.isBefore(moment());
|
||||
|
||||
}else{
|
||||
this.ended = false;
|
||||
}
|
||||
|
||||
if (this.srvModel.startDate) {
|
||||
var startDateM = moment(this.srvModel.startDate);
|
||||
this.startDate = startDateM.format('MMMM Do YYYY');
|
||||
this.startDateRelativeTime = startDateM.fromNow();
|
||||
this.started = startDateM.isBefore(moment());
|
||||
}else{
|
||||
this.started = true;
|
||||
}
|
||||
if(this.started && !this.ended && this.srvModel.endDate){
|
||||
var mDiffD = moment(this.srvModel.endDate).diff(moment(), "days");
|
||||
var mDiffH = moment(this.srvModel.endDate).diff(moment(), "hours");
|
||||
var mDiffM = moment(this.srvModel.endDate).diff(moment(), "minutes");
|
||||
var mDiffS = moment(this.srvModel.endDate).diff(moment(), "seconds");
|
||||
this.endDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
|
||||
}
|
||||
if(!this.started && this.srvModel.startDate){
|
||||
var mDiffD = moment(this.srvModel.startDate).diff(moment(), "days");
|
||||
var mDiffH = moment(this.srvModel.startDate).diff(moment(), "hours");
|
||||
var mDiffM = moment(this.srvModel.startDate).diff(moment(), "minutes");
|
||||
var mDiffS = moment(this.srvModel.startDate).diff(moment(), "seconds");
|
||||
this.startDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
|
||||
}
|
||||
this.lastUpdated = moment(this.srvModel.info.lastUpdated).calendar();
|
||||
this.active = this.started && !this.ended;
|
||||
setTimeout(this.updateComputed, 1000);
|
||||
},
|
||||
submitModalContribute: function(e){
|
||||
debugger;
|
||||
this.$refs.modalContribute.onContributeFormSubmit(e);
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
hubListener.connect();
|
||||
var self = this;
|
||||
this.sound = this.srvModel.soundsEnabled;
|
||||
this.animation = this.srvModel.animationsEnabled;
|
||||
eventAggregator.$on("invoice-created", function (invoiceId) {
|
||||
btcpay.setApiUrlPrefix(window.location.origin);
|
||||
btcpay.showInvoice(invoiceId);
|
||||
btcpay.showFrame();
|
||||
|
||||
self.contributeModalOpen = false;
|
||||
});
|
||||
eventAggregator.$on("invoice-error", function(error){
|
||||
var msg = "";
|
||||
if(typeof error === "string"){
|
||||
msg = error;
|
||||
}else if(!error){
|
||||
msg = "Unknown Error";
|
||||
}else{
|
||||
msg = JSON.stringify(error);
|
||||
}
|
||||
|
||||
Vue.toasted.show("Error creating invoice: " + msg, {
|
||||
iconPack: "fontawesome",
|
||||
icon: "exclamation-triangle",
|
||||
fullWidth: false,
|
||||
theme: "bubble",
|
||||
type: "error",
|
||||
position: "top-center",
|
||||
duration: 10000
|
||||
} );
|
||||
});
|
||||
eventAggregator.$on("payment-received", function (amount, cryptoCode, type) {
|
||||
var onChain = type.toLowerCase() === "btclike";
|
||||
if(self.sound) {
|
||||
playRandomQuakeSound();
|
||||
}
|
||||
if(self.animation) {
|
||||
fireworks();
|
||||
}
|
||||
amount = parseFloat(amount).noExponents();
|
||||
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 "), {
|
||||
iconPack: "fontawesome",
|
||||
icon: "bolt",
|
||||
duration: 10000
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
if(srvModel.disqusEnabled){
|
||||
window.disqus_config = function () {
|
||||
// Replace PAGE_URL with your page's canonical URL variable
|
||||
this.page.url = window.location.href;
|
||||
|
||||
// Replace PAGE_IDENTIFIER with your page's unique identifier variable
|
||||
this.page.identifier = self.srvModel.appId;
|
||||
};
|
||||
|
||||
(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.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;
|
||||
s.setAttribute('data-timestamp', +new Date());
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
}
|
||||
eventAggregator.$on("info-updated", function (model) {
|
||||
console.warn("UPDATED", self.srvModel, arguments);
|
||||
self.srvModel = model;
|
||||
});
|
||||
eventAggregator.$on("connection-pending", function () {
|
||||
self.connectionStatus = "pending";
|
||||
});
|
||||
eventAggregator.$on("connection-failed", function () {
|
||||
self.connectionStatus = "failed";
|
||||
});
|
||||
eventAggregator.$on("connection-lost", function () {
|
||||
self.connectionStatus = "connection lost";
|
||||
});
|
||||
this.updateComputed();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
17
BTCPayServer/wwwroot/crowdfund/helpers/math.js
Normal file
17
BTCPayServer/wwwroot/crowdfund/helpers/math.js
Normal file
@ -0,0 +1,17 @@
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
65
BTCPayServer/wwwroot/crowdfund/services/audioplayer.js
Normal file
65
BTCPayServer/wwwroot/crowdfund/services/audioplayer.js
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
|
||||
function playSound(path) {
|
||||
// audio supported?
|
||||
if (typeof window.Audio === 'function') {
|
||||
var audioElem = new Audio(path);
|
||||
|
||||
audioElem.play().catch(function(){
|
||||
debugger;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function playQuakeSound (name){
|
||||
// var path = window.location.protocol +"://github.com/ClaudiuHKS/AdvancedQuakeSounds/blob/master/sound/QuakeSounds/"+name+"?raw=true"
|
||||
var path = window.location.protocol + "//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/" + name;
|
||||
playSound(path);
|
||||
}
|
||||
|
||||
function playRandomQuakeSound(){
|
||||
playQuakeSound(quake[Math.floor((Math.random() * quake.length) )]);
|
||||
}
|
||||
|
||||
var quake = [
|
||||
"dominating.wav"
|
||||
,"doublekill.wav"
|
||||
,"doublekill2.wav"
|
||||
,"eagleeye.wav"
|
||||
,"firstblood.wav"
|
||||
,"firstblood2.wav"
|
||||
,"firstblood3.wav"
|
||||
,"flawless.wav"
|
||||
,"godlike.wav"
|
||||
,"hattrick.wav"
|
||||
,"headhunter.wav"
|
||||
,"headshot.wav"
|
||||
,"headshot2.wav"
|
||||
,"headshot3.wav"
|
||||
,"holyshit.wav"
|
||||
,"killingspree.wav"
|
||||
,"knife.wav"
|
||||
,"knife2.wav"
|
||||
,"knife3.wav"
|
||||
,"ludicrouskill.wav"
|
||||
,"megakill.wav"
|
||||
,"monsterkill.wav"
|
||||
,"multikill.wav"
|
||||
,"nade.wav"
|
||||
,"ownage.wav"
|
||||
,"payback.wav"
|
||||
,"prepare.wav"
|
||||
,"prepare2.wav"
|
||||
,"prepare3.wav"
|
||||
,"prepare4.wav"
|
||||
,"rampage.wav"
|
||||
,"suicide.wav"
|
||||
,"suicide2.wav"
|
||||
,"suicide3.wav"
|
||||
,"suicide4.wav"
|
||||
,"teamkiller.wav"
|
||||
,"triplekill.wav"
|
||||
,"ultrakill.wav"
|
||||
,"unstoppable.wav"
|
||||
,"whickedsick.wav"];
|
231
BTCPayServer/wwwroot/crowdfund/services/fireworks.js
Normal file
231
BTCPayServer/wwwroot/crowdfund/services/fireworks.js
Normal file
@ -0,0 +1,231 @@
|
||||
addLoadEvent(function(){
|
||||
var c = document.getElementById("fireworks");
|
||||
var ctx = c.getContext("2d");
|
||||
var cH;
|
||||
var cW;
|
||||
var bgColor = "#FF6138";
|
||||
var animations = [];
|
||||
var circles = [];
|
||||
|
||||
var colorPicker = (function() {
|
||||
var colors = ["#FF6138", "#FFBE53", "#2980B9", "#282741"];
|
||||
var index = 0;
|
||||
function next() {
|
||||
index = index++ < colors.length-1 ? index : 0;
|
||||
return colors[index];
|
||||
}
|
||||
function current() {
|
||||
return colors[index]
|
||||
}
|
||||
return {
|
||||
next: next,
|
||||
current: current
|
||||
}
|
||||
})();
|
||||
|
||||
function removeAnimation(animation) {
|
||||
var index = animations.indexOf(animation);
|
||||
if (index > -1) animations.splice(index, 1);
|
||||
}
|
||||
|
||||
function calcPageFillRadius(x, y) {
|
||||
var l = Math.max(x - 0, cW - x);
|
||||
var h = Math.max(y - 0, cH - y);
|
||||
return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
|
||||
}
|
||||
|
||||
function addClickListeners() {
|
||||
document.addEventListener("touchstart", handleEvent);
|
||||
document.addEventListener("mousedown", handleEvent);
|
||||
};
|
||||
|
||||
function handleEvent(e) {
|
||||
if (e.touches) {
|
||||
e.preventDefault();
|
||||
e = e.touches[0];
|
||||
}
|
||||
var currentColor = colorPicker.current();
|
||||
var nextColor = colorPicker.next();
|
||||
var targetR = calcPageFillRadius(e.pageX, e.pageY);
|
||||
var rippleSize = Math.min(200, (cW * .4));
|
||||
var minCoverDuration = 750;
|
||||
|
||||
var pageFill = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: nextColor
|
||||
});
|
||||
var fillAnimation = anime({
|
||||
targets: pageFill,
|
||||
r: targetR,
|
||||
duration: Math.max(targetR / 2 , minCoverDuration ),
|
||||
easing: "easeOutQuart",
|
||||
complete: function(){
|
||||
bgColor = pageFill.fill;
|
||||
removeAnimation(fillAnimation);
|
||||
}
|
||||
});
|
||||
|
||||
var ripple = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: currentColor,
|
||||
stroke: {
|
||||
width: 3,
|
||||
color: currentColor
|
||||
},
|
||||
opacity: 1
|
||||
});
|
||||
var rippleAnimation = anime({
|
||||
targets: ripple,
|
||||
r: rippleSize,
|
||||
opacity: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: 900,
|
||||
complete: removeAnimation
|
||||
});
|
||||
|
||||
var particles = [];
|
||||
for (var i=0; i<32; i++) {
|
||||
var particle = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
fill: currentColor,
|
||||
r: anime.random(24, 48)
|
||||
})
|
||||
particles.push(particle);
|
||||
}
|
||||
var particlesAnimation = anime({
|
||||
targets: particles,
|
||||
x: function(particle){
|
||||
return particle.x + anime.random(rippleSize, -rippleSize);
|
||||
},
|
||||
y: function(particle){
|
||||
return particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15);
|
||||
},
|
||||
r: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: anime.random(1000,1300),
|
||||
complete: removeAnimation
|
||||
});
|
||||
animations.push(fillAnimation, rippleAnimation, particlesAnimation);
|
||||
}
|
||||
|
||||
function extend(a, b){
|
||||
for(var key in b) {
|
||||
if(b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
var Circle = function(opts) {
|
||||
extend(this, opts);
|
||||
}
|
||||
|
||||
Circle.prototype.draw = function() {
|
||||
ctx.globalAlpha = this.opacity || 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
|
||||
if (this.stroke) {
|
||||
ctx.strokeStyle = this.stroke.color;
|
||||
ctx.lineWidth = this.stroke.width;
|
||||
ctx.stroke();
|
||||
}
|
||||
if (this.fill) {
|
||||
ctx.fillStyle = this.fill;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
var animate = anime({
|
||||
duration: Infinity,
|
||||
update: function() {
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, cW, cH);
|
||||
animations.forEach(function(anim) {
|
||||
anim.animatables.forEach(function(animatable) {
|
||||
animatable.target.draw();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var resizeCanvas = function() {
|
||||
cW = window.innerWidth;
|
||||
cH = window.innerHeight;
|
||||
c.width = cW * devicePixelRatio;
|
||||
c.height = cH * devicePixelRatio;
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||||
};
|
||||
|
||||
(function init() {
|
||||
resizeCanvas();
|
||||
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
// addClickListeners();
|
||||
|
||||
// handleInactiveUser();
|
||||
})();
|
||||
|
||||
function handleInactiveUser() {
|
||||
var inactive = setTimeout(function(){
|
||||
fauxClick(cW/2, cH/2);
|
||||
}, 2000);
|
||||
|
||||
function clearInactiveTimeout() {
|
||||
clearTimeout(inactive);
|
||||
document.removeEventListener("mousedown", clearInactiveTimeout);
|
||||
document.removeEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", clearInactiveTimeout);
|
||||
document.addEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
function startFauxClicking() {
|
||||
setTimeout(function(){
|
||||
fauxClick(anime.random( cW * .2, cW * .8), anime.random(cH * .2, cH * .8));
|
||||
startFauxClicking();
|
||||
}, anime.random(200, 900));
|
||||
}
|
||||
|
||||
function fauxClick(x, y) {
|
||||
var fauxClick = new Event("mousedown");
|
||||
fauxClick.pageX = x;
|
||||
fauxClick.pageY = y;
|
||||
document.dispatchEvent(fauxClick);
|
||||
}
|
||||
|
||||
window.fireworks = function(){
|
||||
var fauxClick = new Event("mousedown");
|
||||
fauxClick.pageX =anime.random( 0, cW );
|
||||
fauxClick.pageY = anime.random(0, cH );
|
||||
|
||||
var middleSpaceX = cW * 0.6;
|
||||
var middleSpaceY = cH * 0.6;
|
||||
var middleSpaceX1 = middleSpaceX /2;
|
||||
var middleSpaceX2 = middleSpaceX1 + middleSpaceX;
|
||||
var middleSpaceY1 = middleSpaceY /2;
|
||||
var middleSpaceY2 = middleSpaceY1 + middleSpaceY;
|
||||
while(true){
|
||||
if(fauxClick.pageX > middleSpaceX1 && fauxClick.pageX < middleSpaceX2){
|
||||
fauxClick.pageX =anime.random( 0, cW );
|
||||
continue;
|
||||
}
|
||||
if(fauxClick.pageY > middleSpaceY1 && fauxClick.pageY < middleSpaceY2){
|
||||
fauxClick.pageY =anime.random( 0, cH );
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
handleEvent(fauxClick)
|
||||
};
|
||||
|
||||
|
||||
});
|
50
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal file
50
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
var hubListener = function(){
|
||||
|
||||
var connection = new signalR.HubConnectionBuilder().withUrl("/apps/crowdfund/hub").build();
|
||||
|
||||
connection.onclose(function(){
|
||||
eventAggregator.$emit("connection-lost");
|
||||
console.error("Connection was closed. Attempting reconnect in 2s");
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
connection.on("PaymentReceived", function(amount, cryptoCode, type){
|
||||
eventAggregator.$emit("payment-received", amount,cryptoCode, type);
|
||||
});
|
||||
connection.on("InvoiceCreated", function(invoiceId){
|
||||
eventAggregator.$emit("invoice-created", invoiceId);
|
||||
});
|
||||
connection.on("InvoiceError", function(error){
|
||||
eventAggregator.$emit("invoice-error", error);
|
||||
});
|
||||
connection.on("InfoUpdated", function(model){
|
||||
eventAggregator.$emit("info-updated", model);
|
||||
});
|
||||
|
||||
function connect(){
|
||||
|
||||
eventAggregator.$emit("connection-pending");
|
||||
connection
|
||||
.start()
|
||||
.then(function(){
|
||||
connection.invoke("ListenToCrowdfundApp", srvModel.appId);
|
||||
|
||||
})
|
||||
.catch(function (err) {
|
||||
eventAggregator.$emit("connection-failed");
|
||||
console.error("Could not connect to backend. Retrying in 2s", err );
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
eventAggregator.$on("contribute", function(model){
|
||||
connection.invoke("CreateInvoice", model);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
connect: connect
|
||||
};
|
||||
}();
|
||||
|
55
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal file
55
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal file
@ -0,0 +1,55 @@
|
||||
[v-cloak] > * {
|
||||
display: none
|
||||
}
|
||||
|
||||
[v-cloak]::before {
|
||||
content: "loading…"
|
||||
}
|
||||
|
||||
canvas#fireworks {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.perk-zoom {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.perk-zoom-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.perk-zoom-text {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.perk:hover .perk-zoom {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.perk-badge {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
top: -15px;
|
||||
padding-top: 5px;
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Faktura",
|
||||
"Node Info": "Info o uzlu",
|
||||
"txCount": "{{count}} transakce",
|
||||
"txCount_plural": "{{count}} transakcí"
|
||||
"txCount_plural": "{{count}} transakcí",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Rechnung",
|
||||
"Node Info": "Netzwerkknoten Info",
|
||||
"txCount": "{{count}} transaktion",
|
||||
"txCount_plural": "{{count}} transaktionen"
|
||||
"txCount_plural": "{{count}} transaktionen",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Invoice",
|
||||
"Node Info": "Node Info",
|
||||
"txCount": "{{count}} transaction",
|
||||
"txCount_plural": "{{count}} transactions"
|
||||
"txCount_plural": "{{count}} transactions",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
"Address": "Dirección",
|
||||
"Copied": "Copiado",
|
||||
"ConversionTab_BodyTop": "Puedes pagar {{btcDue}} {{cryptoCode}} usando altcoins que este comercio no soporta directamente.",
|
||||
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros envían los fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
|
||||
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros re-enviarán tus fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
|
||||
"ConversionTab_CalculateAmount_Error": "Reintentar",
|
||||
"ConversionTab_LoadCurrencies_Error": "Reintentar",
|
||||
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
|
||||
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Factura BOLT 11",
|
||||
"Node Info": "Información del nodo",
|
||||
"txCount": "{{count}} transacción",
|
||||
"txCount_plural": "{{count}} transacciones"
|
||||
"txCount_plural": "{{count}} transacciones",
|
||||
"Pay with CoinSwitch": "Pagar con CoinSwitch",
|
||||
"Pay with Changelly": "Pagar con Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Facture BOLT 11",
|
||||
"Node Info": "Informations sur le nœud",
|
||||
"txCount": "{{count}} transaction",
|
||||
"txCount_plural": "{{count}} transactions"
|
||||
"txCount_plural": "{{count}} transactions",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 चालान",
|
||||
"Node Info": "नोड जानकारी",
|
||||
"txCount": "लेनदेन",
|
||||
"txCount_plural": "लेनदेनों"
|
||||
"txCount_plural": "लेनदेनों",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Invoice",
|
||||
"Node Info": "Node Info",
|
||||
"txCount": "{{count}} transaction",
|
||||
"txCount_plural": "{{count}} transactions"
|
||||
"txCount_plural": "{{count}} transactions",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Reikningur",
|
||||
"Node Info": "Nótu upplýsingar",
|
||||
"txCount": "{{count}} reikningur",
|
||||
"txCount_plural": "{{count}} reikningar"
|
||||
"txCount_plural": "{{count}} reikningar",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Fattura BOLT 11",
|
||||
"Node Info": "Informazioni sul Nodo",
|
||||
"txCount": "{{count}} transazione",
|
||||
"txCount_plural": "{{count}} transazioni"
|
||||
"txCount_plural": "{{count}} transazioni",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "お支払いコード",
|
||||
"Node Info": "接続情報",
|
||||
"txCount": "取引 {{count}} 個",
|
||||
"txCount_plural": "取引 {{count}} 個"
|
||||
"txCount_plural": "取引 {{count}} 個",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Шот-фактура",
|
||||
"Node Info": "Node анықтамасы",
|
||||
"txCount": "{{count}} Транзакция",
|
||||
"txCount_plural": "{{count}} Транзакциялар"
|
||||
"txCount_plural": "{{count}} Транзакциялар",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Factuur",
|
||||
"Node Info": "Node Info",
|
||||
"txCount": "{{count}} transactie",
|
||||
"txCount_plural": "{{count}} transacties"
|
||||
"txCount_plural": "{{count}} transacties",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 इनभ्वाइस",
|
||||
"Node Info": "नोड जानकारी",
|
||||
"txCount": "{{count}} लेनदेन",
|
||||
"txCount_plural": "{{count}} लेनदेन"
|
||||
"txCount_plural": "{{count}} लेनदेन",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Płatność BOLT 11",
|
||||
"Node Info": "Informacja o węźle",
|
||||
"txCount": "{{count}} transakcja",
|
||||
"txCount_plural": "{{count}} transakcji"
|
||||
"txCount_plural": "{{count}} transakcji",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Fatura BOLT 11",
|
||||
"Node Info": "Informação de nó",
|
||||
"txCount": "{{count}} transação",
|
||||
"txCount_plural": "{{count}} transações"
|
||||
"txCount_plural": "{{count}} transações",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Fatura BOLT 11",
|
||||
"Node Info": "Informação do Nó",
|
||||
"txCount": "{{count}} transação",
|
||||
"txCount_plural": "{{count}} transações"
|
||||
"txCount_plural": "{{count}} transações",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "счет BOLT 11",
|
||||
"Node Info": "Информация об устройстве",
|
||||
"txCount": "{{count}} Сделка",
|
||||
"txCount_plural": "{{count}} операции"
|
||||
"txCount_plural": "{{count}} операции",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Faktúra BOLT 11",
|
||||
"Node Info": "Informácia o uzle",
|
||||
"txCount": "{{count}} transakcia",
|
||||
"txCount_plural": "{{count}} transakcií"
|
||||
"txCount_plural": "{{count}} transakcií",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Račun",
|
||||
"Node Info": "Informacije Node-a",
|
||||
"txCount": "{{count}} transakcija",
|
||||
"txCount_plural": "{{count}} transakcije"
|
||||
"txCount_plural": "{{count}} transakcije",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 Faturası",
|
||||
"Node Info": "Düğüm Bilgisi",
|
||||
"txCount": "{{count}} işlem",
|
||||
"txCount_plural": "{{count}} işlem"
|
||||
"txCount_plural": "{{count}} işlem",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Рахунок BOLT 11",
|
||||
"Node Info": "Інформація щодо вузла",
|
||||
"txCount": "транзакція {{count}}",
|
||||
"txCount_plural": "транзакції {{count}}"
|
||||
"txCount_plural": "транзакції {{count}}",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "Hóa đơn BOLT 11",
|
||||
"Node Info": "Nút Thông tin",
|
||||
"txCount": "{{count}} giao dịch",
|
||||
"txCount_plural": "{{count}} các giao dịch"
|
||||
"txCount_plural": "{{count}} các giao dịch",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -43,5 +43,7 @@
|
||||
"BOLT 11 Invoice": "BOLT 11 订单",
|
||||
"Node Info": "节点信息(Node Info)",
|
||||
"txCount": "{{count}}笔交易",
|
||||
"txCount_plural": "{{count}}笔交易"
|
||||
"txCount_plural": "{{count}}笔交易",
|
||||
"Pay with CoinSwitch": "Pay with CoinSwitch",
|
||||
"Pay with Changelly": "Pay with Changelly"
|
||||
}
|
@ -29,7 +29,7 @@ Products.prototype.loadFromTemplate = function() {
|
||||
}
|
||||
|
||||
if (productProperty.indexOf('price:') !== -1) {
|
||||
price = parseFloat(productProperty.replace('price:', '').trim());
|
||||
price = parseFloat(productProperty.replace('price:', '').trim()).noExponents();
|
||||
}
|
||||
if (productProperty.indexOf('title:') !== -1) {
|
||||
title = productProperty.replace('title:', '').trim();
|
||||
@ -68,13 +68,13 @@ Products.prototype.saveTemplate = function() {
|
||||
var product = this.products[key],
|
||||
id = product.id,
|
||||
title = product.title,
|
||||
price = product.price,
|
||||
price = product.price? product.price : 0,
|
||||
image = product.image
|
||||
description = product.description,
|
||||
custom = product.custom;
|
||||
|
||||
template += id + '\n' +
|
||||
' price: ' + price + '\n' +
|
||||
' price: ' + parseFloat(price).noExponents() + '\n' +
|
||||
' title: ' + title + '\n';
|
||||
|
||||
if (description) {
|
||||
@ -158,7 +158,7 @@ Products.prototype.itemContent = function(index) {
|
||||
var template = this.template($('#template-product-content'), {
|
||||
'id': product != null ? this.escape(product.id) : '',
|
||||
'index': isNaN(index) ? '' : this.escape(index),
|
||||
'price': product != null ? this.escape(product.price) : '',
|
||||
'price': product != null ? parseFloat(this.escape(product.price)).noExponents() : '',
|
||||
'title': product != null ? this.escape(product.title) : '',
|
||||
'description': product != null ? this.escape(product.description) : '',
|
||||
'image': product != null ? this.escape(product.image) : '',
|
||||
@ -183,4 +183,22 @@ Products.prototype.escape = function(input) {
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
BTCPayServer/wwwroot/vendor/summernote/font/summernote.eot
vendored
Normal file
BIN
BTCPayServer/wwwroot/vendor/summernote/font/summernote.eot
vendored
Normal file
Binary file not shown.
BIN
BTCPayServer/wwwroot/vendor/summernote/font/summernote.ttf
vendored
Normal file
BIN
BTCPayServer/wwwroot/vendor/summernote/font/summernote.ttf
vendored
Normal file
Binary file not shown.
BIN
BTCPayServer/wwwroot/vendor/summernote/font/summernote.woff
vendored
Normal file
BIN
BTCPayServer/wwwroot/vendor/summernote/font/summernote.woff
vendored
Normal file
Binary file not shown.
1
BTCPayServer/wwwroot/vendor/summernote/summernote-bs4.css
vendored
Normal file
1
BTCPayServer/wwwroot/vendor/summernote/summernote-bs4.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7574
BTCPayServer/wwwroot/vendor/summernote/summernote-bs4.js
vendored
Normal file
7574
BTCPayServer/wwwroot/vendor/summernote/summernote-bs4.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
BTCPayServer/wwwroot/vendor/summernote/summernote-bs4.min.js
vendored
Normal file
3
BTCPayServer/wwwroot/vendor/summernote/summernote-bs4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user