Compare commits

...

106 Commits

Author SHA1 Message Date
c95ef27998 bump 2019-01-11 00:17:58 +09:00
63dfd93834 Merge pull request #511 from Kukks/feature/crowdfund
crowdfund: change text
2019-01-11 00:17:37 +09:00
57610881de change text 2019-01-10 16:12:00 +01:00
7469faf296 Merge pull request #510 from Kukks/feature/crowdfund
fix notif number
2019-01-10 23:58:57 +09:00
55a884a559 fix js number 2019-01-10 15:57:30 +01:00
ee2b3c3d10 bump 2019-01-10 23:41:08 +09:00
e5819a260b Merge pull request #509 from Kukks/feature/crowdfund
missed commit crowdfund
2019-01-10 23:40:52 +09:00
a3ecf48702 fix pos update too 2019-01-10 15:37:50 +01:00
1c0b904cd2 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-10 15:35:19 +01:00
072d8a1728 fix exponents in js product editor 2019-01-10 15:35:03 +01:00
964e541c32 Update translations 2019-01-10 23:33:12 +09:00
78fec4ed22 bump 2019-01-10 23:31:35 +09:00
ef111d36c9 Merge pull request #484 from Kukks/feature/crowdfund
New App: Crowdfunding 🎉 🎉
2019-01-10 23:30:31 +09:00
4f64193e85 add rank badge to minimal and fix css in minimal 2019-01-10 14:54:41 +01:00
89bb6d1268 add validation for ranking 2019-01-10 14:43:47 +01:00
9f4226bf0f remove inline styles and fix checkbox setting text 2019-01-10 14:19:06 +01:00
a87c2a3374 Merge remote-tracking branch 'origin/master' into feature/crowdfund 2019-01-10 09:50:22 +01:00
d7294ba5a0 fix product item template 2019-01-10 09:28:51 +01:00
82d286dc6f Fix test 2019-01-10 14:00:26 +09:00
1fa18ab997 Merge pull request #507 from hubiktomas/patch-1
Typo fix
2019-01-10 13:49:52 +09:00
afc90f32c9 Fix tests 2019-01-10 13:47:21 +09:00
e9cfb7c21e Update link to accounting doc 2019-01-10 13:08:25 +09:00
1af8ea3769 Typo fix 2019-01-09 17:35:32 +01:00
9f7af190f1 fix ranking style 2019-01-09 15:44:16 +01:00
9c703fe94d fix number issue 2019-01-09 12:55:02 +01:00
a7a11a4f13 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-09 12:22:42 +01:00
c32c3bb62b add contribution ranking 2019-01-09 12:22:36 +01:00
e29d1480a6 Add link to doc for export 2019-01-09 17:28:30 +09:00
8f299d7791 Fix build 2019-01-09 17:25:46 +09:00
65fb2e992e Round InvoiceDue and PaidCurrency in export 2019-01-09 17:18:01 +09:00
41f5d677d5 Merge pull request #491 from bitcoinshirt/bitcoinshirt-patch-ny
Update 2019 license
2019-01-09 13:33:56 +09:00
a2b78b8cd9 Merge pull request #506 from britttttk/fix/PasswordLength
Fix registration password length
2019-01-09 13:33:41 +09:00
c93f217033 Fix minimum registration password length 2019-01-08 18:32:07 -07:00
82c47b6e9a fix margin on crowdfund 2019-01-08 21:42:11 +01:00
94fb738c67 fix choice key and currency data 2019-01-08 15:49:07 +01:00
89071e40fc oops 2019-01-08 15:14:06 +01:00
95a90c410e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 15:10:13 +01:00
59fc371cd5 perk count + img fixer 2019-01-08 15:10:05 +01:00
caadfc8641 use bolt icon in view 2019-01-08 13:52:44 +01:00
bffc2e70c1 use summernote instead 2019-01-08 13:52:30 +01:00
ca28c34be0 fix ln payment calculator 2019-01-08 10:32:10 +01:00
196bc3ea00 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 09:50:50 +01:00
04cba61888 add bundle helper 2019-01-07 14:40:51 +01:00
a41e2e1ceb Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:39:08 +01:00
d1d03c98ba pr changes 2019-01-07 14:39:04 +01:00
3e48a54ab5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:25:41 +01:00
f6e389ff62 fix issues 2019-01-07 14:25:35 +01:00
561ec57cc8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 11:13:02 +01:00
c63feb488c Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 10:58:52 +01:00
f2ccc4d963 Add sanity check in loading crowdfun 2019-01-06 14:44:51 +01:00
a92d48efdd move button below help text 2019-01-06 14:37:40 +01:00
b633206b45 add helpful texts 2019-01-06 14:28:53 +01:00
b6f3d2af5e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 14:12:25 +01:00
5ca4494eed reorder options in update crowdfund 2019-01-06 13:51:40 +01:00
de7e419ef4 fix overflow of descriptions 2019-01-06 13:50:30 +01:00
20a6b3fc33 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 10:25:24 +01:00
e6357d2ac8 fix build 2019-01-06 09:29:21 +01:00
1eecd85ceb Merge branch 'master' into feature/crowdfund 2019-01-06 09:26:58 +01:00
d63176da19 Update BTCPayServer.csproj 2019-01-05 22:38:44 +01:00
887da5aa9a new year 2019-01-05 22:22:19 +01:00
6e7f1151bc bug fixes and optimizations 2019-01-05 19:47:39 +01:00
fb6d852827 switc back to regtest 2019-01-05 10:18:01 +01:00
ba17612461 Link to associated invoices 2019-01-05 10:17:52 +01:00
a15c7a0213 change crowdfund app prefix to not break invoice searcher 2019-01-05 09:53:57 +01:00
7e321d4016 expand list invoices search 2019-01-05 09:49:06 +01:00
2ccf007b9a fix permissions 2019-01-05 09:38:27 +01:00
895b8c2c80 ux fixes 2019-01-05 09:18:15 +01:00
493466683c start adding UTs 2019-01-04 16:42:35 +01:00
761c342c51 add validation 2019-01-04 13:47:06 +01:00
5341da28d9 add date time picker 2019-01-04 12:58:29 +01:00
7768f41849 add reset every x amount of time feature 2019-01-04 11:42:37 +01:00
c52a49f747 add minimal crowdfund version 2019-01-02 14:08:30 +01:00
e4b9895ba7 add rich text and options 2019-01-02 12:47:06 +01:00
92a2bb4d32 fixes to computed goal result 2019-01-02 12:04:35 +01:00
bfec722312 protect contrib endpoint when needed 2019-01-02 11:29:47 +01:00
5a3f7b5b70 add in more info and simplify backend model 2019-01-02 10:41:54 +01:00
2aa097be46 fix cache expiration time 2019-01-02 09:45:04 +01:00
8a646d85c6 fix bundles 2019-01-02 09:03:20 +01:00
890b3eaa00 remove api key for disqus 2019-01-02 08:10:42 +01:00
cda28ebf15 ui fixes + toggle sound options 2018-12-31 13:20:00 +01:00
2245027ca3 ux fixes 2018-12-31 12:34:27 +01:00
3dc250f801 Add Disqus & fix ux 2018-12-31 11:38:05 +01:00
66e786a1b0 styles and perks 2018-12-30 20:28:36 +01:00
8bd7ea5bbc start ux for perks 2018-12-29 20:21:23 +01:00
6eb36abe2e start contrib perks 2018-12-29 11:52:07 +01:00
6fced3fab2 better tooltips and icons 2018-12-29 10:19:50 +01:00
774e456e54 cleanup 2018-12-28 23:57:39 +01:00
519859e1c5 ux fixwes 2018-12-28 23:31:41 +01:00
26f0c488e5 hook up proper payments to events and super crazy ux shit 2018-12-28 23:12:16 +01:00
1b0b53fbd0 small ux fixes 2018-12-28 18:23:32 +01:00
35f4ea29f9 more integration 2018-12-28 17:38:20 +01:00
d9426d301d Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-28 12:08:32 +01:00
8bcf7109a3 integrate invoice popup 2018-12-28 12:07:15 +01:00
b11f8acba1 wip 2018-12-28 00:10:03 +01:00
ef9a633aa4 fixes for hub 2018-12-27 20:55:46 +01:00
c7e2f979dd Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-27 20:19:29 +01:00
e97bb9c933 work on vue and signalr fro crowdfund 2018-12-27 20:19:21 +01:00
2b84791391 fix raw html 2018-12-22 21:03:43 +01:00
9a488c60f2 fix some styling 2018-12-22 17:30:54 +01:00
9cb50446f4 update bootstrap to 4.2.1 2018-12-22 16:55:24 +01:00
8c00a2359e better layout 2018-12-22 15:43:40 +01:00
d1ff34d16d add minimal crowdfund system and UI 2018-12-22 15:02:16 +01:00
8e8615dab8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-21 11:51:13 +01:00
27bde55f54 work on building the viewmodel for crowdfund 2018-12-18 16:27:03 +01:00
b5d360594a Merge remote-tracking branch 'origin/master' into feature/crowdfund 2018-12-18 13:29:22 +01:00
7fa1b65af0 initial commit 2018-12-11 16:36:25 +01:00
70 changed files with 10640 additions and 159 deletions

View 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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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:

View File

@ -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>

View 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});
}
}
}

View File

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

View File

@ -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;
}
}
}
}

View File

@ -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,

View File

@ -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);

View 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;
}
}
}
}

View 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
});
}
}
}

View File

@ -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

View File

@ -38,6 +38,7 @@ using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.Hubs;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
@ -78,7 +79,7 @@ namespace BTCPayServer.Hosting
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer();
services.AddMvc(o =>
{
@ -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 =>

View File

@ -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
}
}

View File

@ -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
{

View 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; }
}
}

View File

@ -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",

View File

@ -7,6 +7,7 @@ namespace BTCPayServer.Services.Apps
{
public enum AppType
{
PointOfSale
PointOfSale,
Crowdfund
}
}

View File

@ -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,

View File

@ -344,6 +344,7 @@ namespace BTCPayServer.Services.Invoices
InvoiceResponse dto = new InvoiceResponse
{
Id = Id,
StoreId = StoreId,
OrderId = OrderId,
PosData = PosData,
CurrentTime = DateTimeOffset.UtcNow,

View File

@ -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

View 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">&times;</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>
}

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
]
}
]

View File

@ -0,0 +1,8 @@
hljs.initHighlightingOnLoad();
$(document).ready(function() {
$(".richtext").summernote();
$(".datetime").flatpickr({
enableTime: true
});
});

View 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();
}
});
});

View 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;
};

View 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"];

View 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)
};
});

View 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
};
}();

View 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;
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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, '&lt;')
.replace(/>/g, '&gt;')
;
}
}
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

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2018 btcpayserver
Copyright (c) 2017-2019 btcpayserver
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal